"""A migrate request must carry the design's markup, and the plan must say what it really preserves.

A real run (Royal Funding Group) sent a prose brief, a claude.ai artifact link P2HS cannot open, and one
data file (assets/js/data.js) as a "migrate". The plan still claimed to preserve ui/routes/styling/
animations, the build reported success, and the agent recreated a different site from the description.
"""

from __future__ import annotations

from types import SimpleNamespace
from unittest.mock import AsyncMock, patch
from uuid import uuid4

import pytest
from src.core.exceptions import ValidationError as CoreValidationError, to_http_exception
from src.domain.agent_access.operations import StartBuild
from src.domain.agent_access.tools import AgentToolService
from src.domain.codegen.source_adoption import (
    analyze_imported_source,
    build_source_adoption_plan,
    has_ui_markup,
)
from tests.test_agent_tool_audit import _get_build_result

DATA_ONLY = [{"path": "assets/js/data.js", "content": "window.DATA = {};"}]
FULL = DATA_ONLY + [
    {"path": "index.html", "content": "<!doctype html><h1>Fund your business</h1>"},
    {"path": "assets/css/site.css", "content": "body{margin:0}"},
]


def build(**overrides):
    body = {"request_key": "k" * 8, "project_id": uuid4(), "prompt": "Convert my prototype"}
    body.update(overrides)
    return StartBuild(**body)


# ---- refused up front, with a machine code the callers can pass on -------------------------------


async def start_build(**overrides):
    """Drive the real tool dispatcher; submit() is patched so a refusal is provably before queuing."""
    arguments = {"request_key": "k" * 8, "project_id": str(uuid4()), "prompt": "Convert my prototype"}
    arguments.update(overrides)
    access = SimpleNamespace(redis=None, settings=SimpleNamespace(app_url="https://dashboard.p2hs.com"))
    submit = AsyncMock(return_value={"status": "queued"})
    with patch("src.domain.agent_access.tools.AgentOperationService") as service:
        service.return_value.submit = submit
        result = await AgentToolService(access, SimpleNamespace()).execute(
            SimpleNamespace(id=uuid4(), scopes=["projects.write"]), "start_build", arguments
        )
    return result, submit


@pytest.mark.asyncio
async def test_a_migrate_with_only_a_data_file_is_refused_before_anything_is_queued():
    with pytest.raises(CoreValidationError) as caught:
        await start_build(source_adoption="migrate", attachments=DATA_ONLY)
    error = caught.value
    assert error.public_error == "ui_source_required"
    message = error.message
    assert "assets/js/data.js" in message  # what it actually received
    assert "cannot open links" in message and "claude.ai artifact" in message
    assert "request_asset_upload" in message and "omit source_adoption" in message
    http = to_http_exception(error)
    assert http.status_code == 422 and http.detail["error"] == "ui_source_required"


@pytest.mark.asyncio
async def test_the_refusal_never_reaches_the_operation_queue():
    submit = AsyncMock()
    with patch("src.domain.agent_access.tools.AgentOperationService") as service:
        service.return_value.submit = submit
        with pytest.raises(CoreValidationError):
            await AgentToolService(SimpleNamespace(redis=None), SimpleNamespace()).execute(
                SimpleNamespace(id=uuid4(), scopes=["projects.write"]), "start_build",
                {"request_key": "k" * 8, "project_id": str(uuid4()), "prompt": "x",
                 "source_adoption": "migrate", "attachments": DATA_ONLY},
            )
    submit.assert_not_awaited()  # so no operation row exists and no credits can be reserved


@pytest.mark.asyncio
async def test_a_migrate_with_no_attachments_at_all_is_refused():
    with pytest.raises(CoreValidationError, match="no files"):
        await start_build(source_adoption="migrate")


@pytest.mark.parametrize(
    "path", ["index.html", "INDEX.HTM", "site/pages/home.html", "src/App.vue", "src/App.tsx",
             "src/App.jsx", "src/routes/+page.svelte", "src/pages/index.astro"],
)
def test_a_migrate_is_accepted_once_any_markup_file_is_attached(path):
    build(source_adoption="migrate", attachments=[
        {"path": "assets/js/data.js", "content": "x"}, {"path": path, "content": "<p/>"},
    ]).ensure_ui_source()


def test_markup_delivered_by_a_zip_upload_counts_by_its_path():
    # asset_id-backed attachments have no text yet; the path from the archive entry is enough
    build(source_adoption="migrate", attachments=[
        {"path": "site/index.html", "asset_id": str(uuid4())},
        {"path": "site/assets/js/app.js", "asset_id": str(uuid4())},
    ]).ensure_ui_source()


@pytest.mark.parametrize("mode", [None, "add", "merge", "inject", "replace"])
def test_other_modes_are_not_blocked_because_a_data_or_script_file_can_be_a_legitimate_change(mode):
    build(source_adoption=mode, attachments=DATA_ONLY).ensure_ui_source()


@pytest.mark.asyncio
async def test_a_valid_migrate_is_queued_as_before():
    result, submit = await start_build(source_adoption="migrate", attachments=FULL)
    assert result == {"status": "queued"}
    submit.assert_awaited_once()


# ---- the plan is honest about what it can preserve ----------------------------------------------


def plan(operation, files):
    analyses = [analyze_imported_source([f]) for f in files]
    return build_source_adoption_plan(
        analyses=analyses, operation=operation, deployment_intent="modify_and_deploy"
    )


def test_the_analysis_records_whether_any_markup_was_received():
    assert analyze_imported_source(DATA_ONLY)["ui_markup"] is False
    assert analyze_imported_source(FULL)["ui_markup"] is True
    assert has_ui_markup(["a/B.HTML"]) and not has_ui_markup(["app.js", "site.css", "data.json"])


@pytest.mark.parametrize("operation", ["migrate", "replace"])
def test_a_data_only_conversion_no_longer_claims_to_preserve_the_ui(operation):
    result = plan(operation, DATA_ONLY)
    assert result["preserve"] == ["external_integration_semantics"]
    assert result["source_fidelity"] == "content_only"
    assert result["missing_source"] == ["ui_markup"]


def test_a_full_import_still_preserves_everything():
    result = plan("migrate", FULL)
    assert result["source_fidelity"] == "full" and result["missing_source"] == []
    assert result["preserve"] == [
        "ui", "routes", "styling", "animations", "responsive_behavior", "external_integration_semantics",
    ]


def test_adding_a_data_file_to_an_existing_project_keeps_the_existing_preserve_list():
    result = plan("merge", DATA_ONLY)
    assert "ui" in result["preserve"]  # the project's own UI is what stays; nothing narrowed
    assert result["source_fidelity"] == "content_only"


# ---- the caller is told after the build ---------------------------------------------------------


def run_with(plan_fields):
    return {"plan": {"context": {"source_adoption_plan": plan_fields}}}


@pytest.mark.asyncio
async def test_get_build_tells_the_caller_the_design_was_recreated_not_adopted():
    result = await _get_build_result(
        status="completed", error_message=None, **run_with(plan("migrate", DATA_ONLY))
    )
    fidelity = result["source_fidelity"]
    assert fidelity["level"] == "content_only" and fidelity["missing"] == ["ui_markup"]
    assert "recreated from the description" in fidelity["notice"]
    assert "every source file" in fidelity["notice"]


@pytest.mark.asyncio
async def test_get_build_reports_full_fidelity_and_stays_silent_for_runs_without_adoption():
    full = await _get_build_result(status="completed", error_message=None,
                                   **run_with(plan("migrate", FULL)))
    assert full["source_fidelity"] == {"level": "full"}
    none = await _get_build_result(status="completed", error_message=None, plan={"context": {}})
    assert none["source_fidelity"] is None
    merge = await _get_build_result(status="completed", error_message=None,
                                    **run_with(plan("merge", DATA_ONLY)))
    assert merge["source_fidelity"] is None  # a data-only merge is not a failed conversion

