"""AC-2 POST/GET /projects/{id}/ai-messages 端点（内存替身，无 DB/无网络）。

docs/design/ai-chat-history-plan.md §3.3 / §6。覆盖：
- POST 201 + 回显持久化视图（seq/id/created_at）+ 端点 commit 边界；
- append→list 回环；项目级(chapter_no null) vs 本章 union；
- 校验 → 422（非法 role、超上限 content、空 messages、超大 meta）；
- 未知项目 → 404；响应 key 集合不泄漏意外字段。

owner_id 全程 stub（单用户原型）。AI 一律不触达（纯 CRUD）。
"""

from __future__ import annotations

import os
import uuid
from datetime import UTC, datetime, timedelta

import httpx
import pytest
from cryptography.fernet import Fernet
from fakes_projects import FakeProjectRepo, FakeSession
from ww_core.domain.ai_message_repo import AiMessageView, AiTurnRow
from ww_core.domain.project_repo import ProjectView

# 期望的响应 bubble 字段集合（防泄漏意外字段）。
_EXPECTED_KEYS = {
    "id",
    "project_id",
    "chapter_no",
    "thread_id",
    "seq",
    "kind",
    "tool_key",
    "role",
    "content",
    "meta",
    "created_at",
}


class _FakeAiMessageRepo:
    """镜像 `SqlAiMessageRepo` 语义的内存版（批内 seq、newest-first union）。"""

    def __init__(self) -> None:
        self.rows: list[AiMessageView] = []
        self._clock = 0

    async def append(
        self,
        project_id: uuid.UUID,
        *,
        thread_id: uuid.UUID,
        chapter_no: int | None,
        kind: str,
        tool_key: str | None,
        rows: list[AiTurnRow],
    ) -> list[AiMessageView]:
        self._clock += 1
        created = datetime(2026, 7, 9, tzinfo=UTC) + timedelta(seconds=self._clock)
        out: list[AiMessageView] = []
        for i, r in enumerate(rows):
            view = AiMessageView(
                id=uuid.uuid4(),
                project_id=project_id,
                chapter_no=chapter_no,
                thread_id=thread_id,
                seq=i,
                kind=kind,
                tool_key=tool_key,
                role=r.role,
                content=r.content,
                meta=dict(r.meta),
                created_at=created,
            )
            self.rows.append(view)
            out.append(view)
        return out

    async def list_for_chapter(
        self,
        project_id: uuid.UUID,
        chapter_no: int | None,
        *,
        kind: str | None = None,
        limit: int,
        offset: int,
    ) -> list[AiMessageView]:
        matched = [r for r in self.rows if r.project_id == project_id]
        if chapter_no is not None:
            matched = [r for r in matched if r.chapter_no in (chapter_no, None)]
        else:
            matched = [r for r in matched if r.chapter_no is None]
        if kind is not None:
            matched = [r for r in matched if r.kind == kind]
        matched.sort(key=lambda r: (r.created_at, r.seq), reverse=True)
        return matched[offset : offset + limit]


def _make_client() -> tuple[httpx.AsyncClient, _FakeAiMessageRepo, FakeProjectRepo, FakeSession]:
    os.environ.setdefault("CREDENTIAL_ENC_KEY", Fernet.generate_key().decode())
    from ww_api.main import create_app
    from ww_api.services.credentials import STUB_OWNER_ID
    from ww_api.services.project_deps import get_ai_message_repo, get_project_repo
    from ww_db import get_session

    repo = _FakeAiMessageRepo()
    project_repo = FakeProjectRepo()
    session = FakeSession()

    # 一个已存在的项目（owner = stub），供 404 对照——直接种入 fake 的行表（避开事件循环）。
    project_id = uuid.uuid4()
    project_repo.rows[project_id] = (STUB_OWNER_ID, ProjectView(id=project_id, title="AC-2"))

    app = create_app()
    app.dependency_overrides[get_ai_message_repo] = lambda: repo
    app.dependency_overrides[get_project_repo] = lambda: project_repo
    app.dependency_overrides[get_session] = lambda: session
    transport = httpx.ASGITransport(app=app)
    client = httpx.AsyncClient(transport=transport, base_url="http://test")
    client.project_id = project_id  # type: ignore[attr-defined]
    return client, repo, project_repo, session


def _refine_batch(chapter_no: int | None = 5) -> dict[str, object]:
    return {
        "thread_id": str(uuid.uuid4()),
        "chapter_no": chapter_no,
        "kind": "refine",
        "messages": [
            {"role": "author", "content": "原选段"},
            {"role": "ai", "content": "重写段", "meta": {"version_no": 1, "segment": "原选段"}},
        ],
    }


@pytest.mark.asyncio
async def test_append_returns_201_persisted_views_and_commits() -> None:
    client, repo, _pr, session = _make_client()
    pid = client.project_id  # type: ignore[attr-defined]
    async with client:
        resp = await client.post(f"/projects/{pid}/ai-messages", json=_refine_batch(5))
    assert resp.status_code == 201, resp.text
    body = resp.json()
    msgs = body["messages"]
    assert [m["seq"] for m in msgs] == [0, 1]
    assert [m["role"] for m in msgs] == ["author", "ai"]
    assert msgs[1]["meta"] == {"version_no": 1, "segment": "原选段"}
    assert all(set(m.keys()) == _EXPECTED_KEYS for m in msgs)
    assert all(m["id"] and m["created_at"] for m in msgs)
    assert session.commits == 1
    assert len(repo.rows) == 2


@pytest.mark.asyncio
async def test_append_then_list_roundtrip() -> None:
    client, _repo, _pr, _session = _make_client()
    pid = client.project_id  # type: ignore[attr-defined]
    async with client:
        await client.post(f"/projects/{pid}/ai-messages", json=_refine_batch(5))
        resp = await client.get(f"/projects/{pid}/ai-messages", params={"chapter_no": 5})
    assert resp.status_code == 200
    msgs = resp.json()["messages"]
    # newest-first：批内 seq DESC → ai(seq1) 先于 author(seq0)。
    assert [m["content"] for m in msgs] == ["重写段", "原选段"]


@pytest.mark.asyncio
async def test_list_chapter_union_includes_project_level() -> None:
    client, _repo, _pr, _session = _make_client()
    pid = client.project_id  # type: ignore[attr-defined]
    async with client:
        await client.post(f"/projects/{pid}/ai-messages", json=_refine_batch(5))
        # 项目级工具箱生成（chapter_no 省略 → NULL）。
        await client.post(
            f"/projects/{pid}/ai-messages",
            json={
                "thread_id": str(uuid.uuid4()),
                "kind": "generator",
                "tool_key": "expand",
                "messages": [{"role": "ai", "content": "扩写预览"}],
            },
        )
        chapter_view = await client.get(f"/projects/{pid}/ai-messages", params={"chapter_no": 5})
        toolbox_view = await client.get(f"/projects/{pid}/ai-messages")
    chapter_contents = {m["content"] for m in chapter_view.json()["messages"]}
    toolbox_contents = {m["content"] for m in toolbox_view.json()["messages"]}
    assert chapter_contents == {"原选段", "重写段", "扩写预览"}  # 本章 ∪ 项目级
    assert toolbox_contents == {"扩写预览"}  # 仅项目级


@pytest.mark.asyncio
async def test_list_kind_filter() -> None:
    client, _repo, _pr, _session = _make_client()
    pid = client.project_id  # type: ignore[attr-defined]
    async with client:
        await client.post(f"/projects/{pid}/ai-messages", json=_refine_batch(2))
        await client.post(
            f"/projects/{pid}/ai-messages",
            json={
                "thread_id": str(uuid.uuid4()),
                "chapter_no": 2,
                "kind": "rewrite",
                "messages": [{"role": "ai", "content": "整章新版"}],
            },
        )
        resp = await client.get(
            f"/projects/{pid}/ai-messages", params={"chapter_no": 2, "kind": "rewrite"}
        )
    msgs = resp.json()["messages"]
    assert [m["kind"] for m in msgs] == ["rewrite"]
    assert [m["content"] for m in msgs] == ["整章新版"]


@pytest.mark.asyncio
async def test_append_unknown_project_returns_404() -> None:
    client, repo, _pr, session = _make_client()
    async with client:
        resp = await client.post(f"/projects/{uuid.uuid4()}/ai-messages", json=_refine_batch(5))
    assert resp.status_code == 404
    assert resp.json()["error"]["code"] == "NOT_FOUND"
    assert session.commits == 0
    assert repo.rows == []


@pytest.mark.asyncio
async def test_list_unknown_project_returns_404() -> None:
    client, _repo, _pr, _session = _make_client()
    async with client:
        resp = await client.get(f"/projects/{uuid.uuid4()}/ai-messages", params={"chapter_no": 1})
    assert resp.status_code == 404
    assert resp.json()["error"]["code"] == "NOT_FOUND"


@pytest.mark.asyncio
async def test_append_invalid_role_returns_422() -> None:
    client, _repo, _pr, session = _make_client()
    pid = client.project_id  # type: ignore[attr-defined]
    async with client:
        resp = await client.post(
            f"/projects/{pid}/ai-messages",
            json={
                "thread_id": str(uuid.uuid4()),
                "chapter_no": 5,
                "kind": "refine",
                "messages": [{"role": "system", "content": "x"}],
            },
        )
    assert resp.status_code == 422
    assert session.commits == 0


@pytest.mark.asyncio
async def test_append_invalid_kind_returns_422() -> None:
    client, _repo, _pr, _session = _make_client()
    pid = client.project_id  # type: ignore[attr-defined]
    async with client:
        resp = await client.post(
            f"/projects/{pid}/ai-messages",
            json={
                "thread_id": str(uuid.uuid4()),
                "kind": "translate",  # 非五类枚举 → 422
                "messages": [{"role": "ai", "content": "x"}],
            },
        )
    assert resp.status_code == 422


@pytest.mark.asyncio
async def test_append_empty_messages_returns_422() -> None:
    client, _repo, _pr, _session = _make_client()
    pid = client.project_id  # type: ignore[attr-defined]
    async with client:
        resp = await client.post(
            f"/projects/{pid}/ai-messages",
            json={"thread_id": str(uuid.uuid4()), "kind": "refine", "messages": []},
        )
    assert resp.status_code == 422


@pytest.mark.asyncio
async def test_append_empty_content_returns_422() -> None:
    client, _repo, _pr, _session = _make_client()
    pid = client.project_id  # type: ignore[attr-defined]
    async with client:
        resp = await client.post(
            f"/projects/{pid}/ai-messages",
            json={
                "thread_id": str(uuid.uuid4()),
                "kind": "refine",
                "messages": [{"role": "ai", "content": ""}],
            },
        )
    assert resp.status_code == 422


@pytest.mark.asyncio
async def test_append_content_over_cap_returns_422() -> None:
    client, _repo, _pr, _session = _make_client()
    pid = client.project_id  # type: ignore[attr-defined]
    async with client:
        resp = await client.post(
            f"/projects/{pid}/ai-messages",
            json={
                "thread_id": str(uuid.uuid4()),
                "kind": "rewrite",
                "messages": [{"role": "ai", "content": "x" * 200_001}],
            },
        )
    assert resp.status_code == 422


@pytest.mark.asyncio
async def test_append_oversize_meta_returns_422() -> None:
    client, _repo, _pr, _session = _make_client()
    pid = client.project_id  # type: ignore[attr-defined]
    async with client:
        resp = await client.post(
            f"/projects/{pid}/ai-messages",
            json={
                "thread_id": str(uuid.uuid4()),
                "kind": "refine",
                "messages": [{"role": "ai", "content": "x", "meta": {"blob": "y" * 60_000}}],
            },
        )
    assert resp.status_code == 422
