"""A build started from an AI connection appears in the project's prompt history.

It used to have no Prompt, no chat row and no session, so the dashboard showed nothing; and because
assistant replies are only written for a run that has a prompt, no replies either. Recording is
best-effort in a savepoint: a history problem never fails the build."""

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.domain.codegen.agent_facade import _chat_display
from tests.test_agent_attachments import _build, _facade, _grant

SECRET = "sk_live_abcdefghijklmnop123456"


class Recorder:
    """In-memory stand-ins for the three repositories the history uses."""

    def __init__(self, fail=False):
        self.sessions, self.prompts, self.messages, self.session_rows = [], [], [], {}
        self.fail = fail

    def patches(self):
        rec = self

        class Sessions:
            def __init__(self, db):
                pass

            async def get_by_id(self, sid):
                return rec.session_rows.get(sid)

            async def create(self, project_id, user_id, title):
                if rec.fail:
                    raise RuntimeError("db exploded")
                row = SimpleNamespace(id=uuid4(), project_id=project_id, user_id=user_id)
                rec.session_rows[row.id] = row
                rec.sessions.append(row)
                return row

        class Prompts:
            def __init__(self, db):
                pass

            async def create(self, session_id, content, user_id=None, context=None):
                row = SimpleNamespace(id=uuid4(), session_id=session_id, content=content,
                                      user_id=user_id, context=context)
                rec.prompts.append(row)
                return row

        class Messages:
            def __init__(self, db):
                pass

            async def create(self, **kwargs):
                rec.messages.append(kwargs)

        return (
            patch("src.domain.instructions.repository.PromptSessionRepository", Sessions),
            patch("src.domain.instructions.repository.PromptRepository", Prompts),
            patch("src.domain.codegen.run_chat_repository.RunChatMessageRepository", Messages),
        )


async def start(prompt="Convert my prototype", attachments=None, *, project="default", recorder=None,
                grant=None):
    recorder = recorder or Recorder()
    facade = _facade()
    if project == "default":
        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._snapshot_attachments = AsyncMock(return_value=None)

    @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)
    grant = grant or _grant()
    a, b, c = recorder.patches()
    with a, b, c:
        result = await facade.start(grant, _build(prompt=prompt, attachments=attachments or []))
    return SimpleNamespace(result=result, run=run, rec=recorder, facade=facade, project=project, grant=grant)


FILES = [{"path": "index.html", "content": "<h1>x</h1>"}, {"path": "assets/css/site.css", "content": "a{}"}]


@pytest.mark.asyncio
async def test_the_prompt_appears_in_the_history_and_the_run_is_linked_to_it():
    out = await start(attachments=FILES)
    [prompt] = out.rec.prompts
    assert prompt.content.startswith("Convert my prototype")
    assert prompt.user_id == out.grant.created_by  # the person who authorized the connection
    assert out.run.prompt_id == prompt.id  # so assistant replies for this run can be persisted too
    [message] = out.rec.messages
    assert message["role"] == "user" and message["prompt_id"] == prompt.id
    assert message["run_id"] == out.run.id and message["project_id"] == out.project.id
    assert message["session_id"] == prompt.session_id and message["content"] == prompt.content
    assert out.result is out.run  # start still returns the run


@pytest.mark.asyncio
async def test_a_project_without_a_session_gets_one_and_it_becomes_the_active_session():
    out = await start()
    [session] = out.rec.sessions
    assert session.project_id == out.project.id and session.user_id == out.grant.created_by
    out.facade.projects.update.assert_awaited_once_with(out.project.id, active_session_id=session.id)


@pytest.mark.asyncio
async def test_the_projects_active_session_is_reused_so_the_history_stays_one_conversation():
    recorder = Recorder()
    project = SimpleNamespace(id=uuid4(), workspace_id=uuid4(), type="nuxt_app", discovery_json=None,
                              active_session_id=uuid4())
    recorder.session_rows[project.active_session_id] = SimpleNamespace(
        id=project.active_session_id, project_id=project.id)
    out = await start(project=project, recorder=recorder)
    assert out.rec.sessions == []  # nothing new was created
    assert out.rec.prompts[0].session_id == project.active_session_id
    out.facade.projects.update.assert_not_awaited()


@pytest.mark.asyncio
async def test_an_active_session_that_belongs_to_another_project_is_never_used():
    recorder = Recorder()
    project = SimpleNamespace(id=uuid4(), workspace_id=uuid4(), type="nuxt_app", discovery_json=None,
                              active_session_id=uuid4())
    recorder.session_rows[project.active_session_id] = SimpleNamespace(
        id=project.active_session_id, project_id=uuid4())  # someone else's session
    out = await start(project=project, recorder=recorder)
    assert len(out.rec.sessions) == 1 and out.rec.prompts[0].session_id != project.active_session_id


@pytest.mark.asyncio
async def test_the_stored_prompt_records_where_it_came_from_and_which_files_came_with_it():
    out = await start(attachments=FILES)
    context = out.rec.prompts[0].context
    assert context["source"] == "mcp_agent" and context["grant_id"] == str(out.grant.id)
    assert context["attachment_paths"] == ["index.html", "assets/css/site.css"]


# ---- what the dashboard shows -------------------------------------------------------------------------


def test_attached_files_are_summarized_not_dumped():
    files = [SimpleNamespace(path=f"page{i}.html") for i in range(12)]
    shown = _chat_display("Convert this", files)
    assert shown.startswith("Convert this")
    assert "[Sent from an AI connection with 12 attached source files: page0.html" in shown
    assert "page7.html and 4 more]" in shown and "page11.html" not in shown
    assert "attached source file: only.html]" in _chat_display("x", [SimpleNamespace(path="only.html")])
    assert _chat_display("just text", []) == "just text"  # nothing to add without files


@pytest.mark.asyncio
async def test_a_secret_in_the_prompt_is_redacted_before_it_is_stored_or_shown():
    out = await start(prompt=f"Use the key {SECRET} for payments")
    assert SECRET not in out.rec.prompts[0].content and SECRET not in out.rec.messages[0]["content"]
    assert "<P2HS_SECRET_REDACTED>" in out.rec.prompts[0].content


# ---- it can never fail the build ---------------------------------------------------------------------


@pytest.mark.asyncio
async def test_a_history_failure_does_not_fail_or_alter_the_build():
    out = await start(recorder=Recorder(fail=True))
    assert out.result is out.run  # the build was still created
    assert out.run.prompt_id is None and out.rec.prompts == [] and out.rec.messages == []


@pytest.mark.asyncio
async def test_without_a_project_row_nothing_is_recorded():
    out = await start(project=None)
    assert out.rec.prompts == [] and out.rec.sessions == [] and out.rec.messages == []
