"""The dashboard import through the facade and the API layer: who may do it, caps, and ordering.

The facade tests pin that an import started by a person reuses the AI-connection path unchanged and is recorded in the
project's prompt history; the API tests pin permissions (viewers cannot import), the upload cap, and that a build is
committed before it is enqueued."""

from __future__ import annotations

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

import pytest
from src.core.dependencies import require_active_billing_for_project
from src.core.exceptions import ForbiddenError, NotFoundError, ValidationError
from src.domain.codegen import router as codegen_router
from src.domain.codegen.agent_facade import _chat_display
from src.domain.codegen.source_import_service import SourceImportStartBody
from tests.test_agent_attachments import _build, _facade
from tests.test_mcp_prompt_history import Recorder

MANIFEST = {"import_id": "a" * 32, "files": [{"path": "index.html", "size": 5}], "rejected": []}


def attachment(path, content="<p/>"):
    return SimpleNamespace(path=path, content=content)


# ---- facade: a person starting a build from a stored import -------------------------------------------------


async def start_as_person(attachments=None, manifest=MANIFEST):
    recorder = Recorder()
    facade = _facade()
    project = SimpleNamespace(id=uuid4(), workspace_id=uuid4(), type="nuxt_app", discovery_json=None,
                              active_session_id=None)
    facade.projects.get_by_id = AsyncMock(return_value=project)
    facade.projects.update = AsyncMock()
    facade._resolve_attachments = AsyncMock(side_effect=AssertionError("must not re-resolve"))
    facade._snapshot_attachments = AsyncMock(side_effect=AssertionError("must not re-snapshot"))

    @asynccontextmanager
    async def savepoint():
        yield

    facade._db = SimpleNamespace(begin_nested=savepoint, flush=AsyncMock())
    run = SimpleNamespace(id=uuid4(), prompt_id=None)
    facade.service.create_run = AsyncMock(return_value=run)
    actor = SimpleNamespace(id=None, created_by=uuid4())
    a, b, c = recorder.patches()
    body = _build(prompt="Turn this into my app", source_adoption="migrate",
                  attachments=[{"path": "index.html", "content": "<p/>"}])
    with a, b, c:
        result = await facade.start(
            actor, body, prompt_source="dashboard_import",
            attachments=attachments or [attachment("index.html")], import_snapshot=manifest)
    return SimpleNamespace(result=result, run=run, actor=actor, facade=facade, rec=recorder, project=project)


@pytest.mark.asyncio
async def test_a_stored_import_is_used_as_it_is_not_stored_or_resolved_again():
    out = await start_as_person()
    kwargs = out.facade.service.create_run.await_args.kwargs
    assert kwargs["created_by"] == out.actor.created_by and kwargs["initiating_grant_id"] is None
    assert kwargs["prompt_source"] == "dashboard_import"
    assert kwargs["generation_request"]["import_snapshot"] == MANIFEST
    assert kwargs["generation_request"]["source_adoption"] == "migrate"
    assert "## Imported source analysis and bounded adoption plan" in kwargs["user_prompt"]
    assert out.result is out.run


@pytest.mark.asyncio
async def test_the_build_appears_in_the_prompt_history_as_sent_from_the_dashboard():
    out = await start_as_person()
    [prompt] = out.rec.prompts
    assert prompt.user_id == out.actor.created_by and out.run.prompt_id == prompt.id
    assert "[Sent from the dashboard with 1 attached source file: index.html]" in prompt.content
    assert "AI connection" not in prompt.content
    assert prompt.context["source"] == "dashboard_import" and prompt.context["grant_id"] is None


def test_the_history_line_names_the_origin():
    files = [attachment("a.html"), attachment("b.css")]
    assert "Sent from an AI connection with 2 attached source files" in _chat_display("x", files)
    assert "Sent from the dashboard with 2 attached source files" in _chat_display("x", files, "dashboard_import")
    assert "Sent from an AI connection" in _chat_display("x", files, "mcp_agent")


# ---- API: permissions ---------------------------------------------------------------------------------------


def project_row():
    return SimpleNamespace(id=uuid4(), workspace_id=uuid4())


def patched_access(row, allowed=True):
    membership = SimpleNamespace(has_permission=AsyncMock(return_value=allowed))
    return (
        patch.object(codegen_router, "ProjectRepository",
                     lambda db: SimpleNamespace(get_by_id=AsyncMock(return_value=row))),
        patch("src.domain.workspaces.repository.MembershipRepository", lambda db: membership),
        membership,
    )


@pytest.mark.asyncio
async def test_a_member_who_can_run_builds_may_import():
    row = project_row()
    a, b, membership = patched_access(row)
    user = SimpleNamespace(id=uuid4())
    with a, b:
        assert await codegen_router._project_for_source_import(row.id, user, object()) is row
    membership.has_permission.assert_awaited_once_with(row.workspace_id, user.id, "codegen.run")


@pytest.mark.asyncio
async def test_a_viewer_cannot_import_source():
    row = project_row()
    a, b, _ = patched_access(row, allowed=False)
    with a, b, pytest.raises(ForbiddenError, match="permission to run builds"):
        await codegen_router._project_for_source_import(row.id, SimpleNamespace(id=uuid4()), object())


@pytest.mark.asyncio
async def test_a_missing_project_is_not_found():
    a, b, _ = patched_access(None)
    with a, b, pytest.raises(NotFoundError):
        await codegen_router._project_for_source_import(uuid4(), SimpleNamespace(id=uuid4()), object())


# ---- API: upload ---------------------------------------------------------------------------------------------


class FakeUpload:
    def __init__(self, filename, data):
        self.filename, self._data = filename, data

    async def read(self):
        return self._data


@pytest.mark.asyncio
async def test_the_upload_reads_every_file_and_stores_them_for_the_project():
    row = project_row()
    a, b, _ = patched_access(row)
    user = SimpleNamespace(id=uuid4())
    store = AsyncMock(return_value={"import_id": "x"})
    with a, b, patch("src.domain.codegen.source_import_service.store_upload", store):
        result = await codegen_router.upload_source_import(
            files=[FakeUpload("index.html", b"<p/>"), FakeUpload("assets/a.css", b"a{}"), FakeUpload(None, b"x")],
            project=row.id, user=user, db=object())
    assert result == {"import_id": "x"}
    _, project_arg, user_arg, uploads = store.await_args.args
    assert project_arg is row and user_arg is user
    assert uploads == [("index.html", b"<p/>"), ("assets/a.css", b"a{}"), ("", b"x")]


@pytest.mark.asyncio
async def test_an_upload_over_the_size_cap_is_refused_before_it_is_stored(monkeypatch):
    monkeypatch.setattr("src.domain.codegen.source_import.MAX_UPLOAD_BYTES", 10)
    row = project_row()
    a, b, _ = patched_access(row)
    store = AsyncMock()
    with a, b, patch("src.domain.codegen.source_import_service.store_upload", store), \
            pytest.raises(ValidationError, match="larger than"):
        await codegen_router.upload_source_import(
            files=[FakeUpload("a.css", b"x" * 6), FakeUpload("b.css", b"x" * 6)], project=row.id,
            user=SimpleNamespace(id=uuid4()), db=object())
    store.assert_not_awaited()


@pytest.mark.asyncio
async def test_a_viewer_cannot_upload():
    row = project_row()
    a, b, _ = patched_access(row, allowed=False)
    with a, b, pytest.raises(ForbiddenError):
        await codegen_router.upload_source_import(
            files=[FakeUpload("a.css", b"a{}")], project=row.id, user=SimpleNamespace(id=uuid4()), db=object())


# ---- API: start ----------------------------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_a_started_build_is_committed_before_it_is_queued():
    row = project_row()
    a, b, _ = patched_access(row)
    events = []
    run = SimpleNamespace(id=uuid4(), status="queued")
    db = SimpleNamespace(commit=AsyncMock(side_effect=lambda: events.append("commit")))
    start = AsyncMock(return_value=run)
    enqueue = AsyncMock(side_effect=lambda run_id: events.append(f"enqueue {run_id}"))
    with a, b, patch("src.domain.codegen.source_import_service.start_from_import", start), \
            patch("src.workers.enqueue.enqueue_codegen_run", enqueue):
        result = await codegen_router.start_build_from_source_import(
            SourceImportStartBody(source_adoption="migrate"), import_id="a" * 32, project=row.id,
            user=SimpleNamespace(id=uuid4()), db=db)
    assert events == ["commit", f"enqueue {run.id}"]  # never queue what is not yet saved
    assert result == {"run_id": str(run.id), "status": "queued", "import_id": "a" * 32}


@pytest.mark.asyncio
async def test_a_refused_start_queues_nothing():
    row = project_row()
    a, b, _ = patched_access(row)
    enqueue = AsyncMock()
    db = SimpleNamespace(commit=AsyncMock())
    with a, b, patch("src.domain.codegen.source_import_service.start_from_import",
                     AsyncMock(side_effect=ValidationError("nope"))), \
            patch("src.workers.enqueue.enqueue_codegen_run", enqueue), pytest.raises(ValidationError):
        await codegen_router.start_build_from_source_import(
            SourceImportStartBody(source_adoption="migrate"), import_id="a" * 32, project=row.id,
            user=SimpleNamespace(id=uuid4()), db=db)
    enqueue.assert_not_awaited()
    db.commit.assert_not_awaited()


# ---- API: wiring ---------------------------------------------------------------------------------------------


def routes_for(suffix):
    return [r for r in codegen_router.router.routes if r.path.endswith(suffix)]


@pytest.mark.parametrize("suffix,method", [
    ("/source-imports", "POST"), ("/source-imports", "GET"), ("/source-imports/{import_id}/start", "POST"),
])
def test_every_endpoint_requires_an_active_paid_project_member(suffix, method):
    route = next(r for r in routes_for(suffix) if method in r.methods)
    assert require_active_billing_for_project in [d.call for d in route.dependant.dependencies]
