"""T2.5 review API + 历史 / T2.4 验收事务 + 冲突 gate（内存替身，无 DB/无网络）。

覆盖：review SSE 帧序列（section/conflict/done）、reviews 历史、accept happy path
（晋升 + digest + 裁决）、冲突 gate 拦截（缺判 → CONFLICT_UNRESOLVED 不写库）、
事务回滚（某步失败全回滚不 commit）。审稿/digest 网关均注 mock（产 parsed），绝不联网。
"""

from __future__ import annotations

import uuid

import httpx
import pytest
from cryptography.fernet import Fernet
from fakes_projects import (
    FakeChapterRepo,
    FakeDigestAppendRepo,
    FakeReviewGateway,
    FakeReviewRepo,
    FakeSession,
    FakeSessionFactory,
)
from test_projects import _empty_memory_repos
from ww_agents import ContinuityReview
from ww_agents.schemas import Conflict
from ww_shared import ErrorCode


def _make_client(
    *,
    chapter_repo: FakeChapterRepo | None = None,
    review_repo: FakeReviewRepo | None = None,
    digest_repo: FakeDigestAppendRepo | None = None,
    review_gateway: FakeReviewGateway | None = None,
    digest_gateway: FakeReviewGateway | None = None,
    session: FakeSession | None = None,
) -> tuple[
    httpx.AsyncClient,
    FakeChapterRepo,
    FakeReviewRepo,
    FakeDigestAppendRepo,
    FakeSession,
]:
    import os

    os.environ.setdefault("CREDENTIAL_ENC_KEY", Fernet.generate_key().decode())
    from ww_api.main import create_app
    from ww_api.services.project_deps import (
        get_chapter_repo,
        get_digest_append_repo,
        get_digest_gateway,
        get_memory_repos,
        get_review_gateway,
        get_review_repo,
        get_session_factory,
    )
    from ww_db import get_session

    chapter_repo = chapter_repo or FakeChapterRepo()
    review_repo = review_repo or FakeReviewRepo()
    digest_repo = digest_repo or FakeDigestAppendRepo()
    review_gateway = review_gateway or FakeReviewGateway(parsed=ContinuityReview())
    digest_gateway = digest_gateway or FakeReviewGateway(parsed=ContinuityReview())
    session = session or FakeSession()

    app = create_app()
    app.dependency_overrides[get_chapter_repo] = lambda: chapter_repo
    app.dependency_overrides[get_review_repo] = lambda: review_repo
    app.dependency_overrides[get_digest_append_repo] = lambda: digest_repo
    app.dependency_overrides[get_memory_repos] = _empty_memory_repos
    app.dependency_overrides[get_review_gateway] = lambda: review_gateway
    app.dependency_overrides[get_digest_gateway] = lambda: digest_gateway
    app.dependency_overrides[get_session] = lambda: session
    app.dependency_overrides[get_session_factory] = lambda: FakeSessionFactory()
    transport = httpx.ASGITransport(app=app)
    client = httpx.AsyncClient(transport=transport, base_url="http://test")
    return client, chapter_repo, review_repo, digest_repo, session


# ---- T2.5 review SSE ----


@pytest.mark.asyncio
async def test_review_stream_yields_section_and_done() -> None:
    gw = FakeReviewGateway(parsed=ContinuityReview())
    client, _, review_repo, _, session = _make_client(review_gateway=gw)
    pid = uuid.uuid4()
    async with client:
        resp = await client.post(
            f"/projects/{pid}/chapters/1/review", json={"draft": "本章草稿正文。"}
        )
    assert resp.status_code == 200
    assert resp.headers["content-type"].startswith("text/event-stream")
    text = resp.text
    assert "event: section" in text
    assert '"name": "continuity"' in text
    assert '"status": "done"' in text
    assert "event: done" in text
    # collect 落了一行留痕；端点流耗尽后 commit。
    assert len(review_repo.rows) == 1
    assert session.commits == 1


@pytest.mark.asyncio
async def test_review_stream_emits_conflict_events() -> None:
    review = ContinuityReview(
        conflicts=[
            Conflict(type="性格漂移", where="第3段", refs=["第2章"], suggestion="改回冷静"),
        ]
    )
    gw = FakeReviewGateway(parsed=review)
    client, _, review_repo, _, _ = _make_client(review_gateway=gw)
    pid = uuid.uuid4()
    async with client:
        resp = await client.post(f"/projects/{pid}/chapters/1/review", json={"draft": "草稿"})
    text = resp.text
    assert "event: conflict" in text
    assert '"type": "性格漂移"' in text
    # 冲突落进 chapter_reviews 留痕。
    assert review_repo.rows[0].conflicts[0]["type"] == "性格漂移"


@pytest.mark.asyncio
async def test_review_without_draft_falls_back_to_saved() -> None:
    chapter_repo = FakeChapterRepo()
    pid = uuid.uuid4()
    await chapter_repo.save_draft(pid, 1, text="已存草稿")
    client, _, _, _, _ = _make_client(chapter_repo=chapter_repo)
    async with client:
        resp = await client.post(f"/projects/{pid}/chapters/1/review", json={})
    assert resp.status_code == 200
    assert "event: done" in resp.text


@pytest.mark.asyncio
async def test_review_without_any_draft_404() -> None:
    client, _, _, _, _ = _make_client()
    pid = uuid.uuid4()
    async with client:
        resp = await client.post(f"/projects/{pid}/chapters/9/review", json={})
    assert resp.status_code == 404
    assert resp.json()["error"]["code"] == ErrorCode.NOT_FOUND


# ---- T2.5 reviews 历史 ----


@pytest.mark.asyncio
async def test_list_reviews_returns_history_newest_first() -> None:
    review_repo = FakeReviewRepo()
    pid = uuid.uuid4()
    await review_repo.record(pid, 1, conflicts=[{"type": "设定违例"}])
    await review_repo.record(pid, 1, conflicts=[])
    client, _, _, _, _ = _make_client(review_repo=review_repo)
    async with client:
        resp = await client.get(f"/projects/{pid}/chapters/1/reviews")
    assert resp.status_code == 200
    rows = resp.json()["reviews"]
    assert len(rows) == 2
    # 新→旧：最近 record 的（空冲突）在前。
    assert rows[0]["conflicts"] == []


# ---- T2.4 accept happy path ----


async def _seed_review(
    review_repo: FakeReviewRepo, pid: uuid.UUID, conflicts: list[dict[str, object]]
) -> None:
    await review_repo.record(pid, 1, conflicts=conflicts)


@pytest.mark.asyncio
async def test_accept_promotes_appends_digest_records_decisions() -> None:
    review_repo = FakeReviewRepo()
    pid = uuid.uuid4()
    await _seed_review(
        review_repo, pid, [{"type": "性格漂移", "where": "x", "refs": [], "suggestion": "y"}]
    )
    from ww_api.services.digest_extraction import ChapterDigestFacts

    digest_gw = FakeReviewGateway(parsed=ChapterDigestFacts(summary="本章主线", events=["开战"]))
    client, chapter_repo, _, digest_repo, session = _make_client(
        review_repo=review_repo, digest_gateway=digest_gw
    )
    async with client:
        resp = await client.post(
            f"/projects/{pid}/chapters/1/accept",
            json={
                "final_text": "作者改稿后的终稿正文。",
                "decisions": [{"conflict_index": 0, "verdict": "accept"}],
            },
        )
    assert resp.status_code == 200
    body = resp.json()
    assert body["accepted_version"] == 1
    assert body["digest_added"] is True
    assert body["decisions_recorded"] == 1
    # 晋升落 accepted v1，终稿正文。
    accepted = await chapter_repo.latest_accepted(pid, 1)
    assert accepted is not None
    assert accepted.content == "作者改稿后的终稿正文。"
    # digest 从终稿提炼（不变量 #4）。
    assert digest_repo.rows[0][2]["summary"] == "本章主线"
    # 裁决写回留痕。
    assert review_repo.rows[0].decisions is not None
    assert review_repo.rows[0].decisions["items"][0]["verdict"] == "accept"
    # 单事务一次 commit。
    assert session.commits == 1


@pytest.mark.asyncio
async def test_characterization_does_not_block_accept() -> None:
    """灵感③ advisory：人物塑造问题（零 continuity 冲突）不阻断验收（D2）。

    留痕带 characterization issues 但 conflicts=[] → 验收 gate 只读 continuity conflicts，
    直通晋升；characterization 从不进 conflicts、从不产 decisions。
    """
    from ww_api.services.digest_extraction import ChapterDigestFacts

    review_repo = FakeReviewRepo()
    pid = uuid.uuid4()
    # 零 continuity 冲突，但有 advisory 人物塑造问题。
    await review_repo.record(
        pid,
        1,
        conflicts=[],
        characterization={
            "issues": [
                {
                    "character": "沈砚",
                    "aspect": "动机一致性",
                    "where": "第3段",
                    "quote": "他忽然转身就走。",
                    "diagnosis": "与既定护妹动机断裂",
                    "suggestion": "退缩扣回护妹动机",
                    "confidence": 0.82,
                }
            ]
        },
    )
    digest_gw = FakeReviewGateway(parsed=ChapterDigestFacts(summary="s"))
    client, chapter_repo, _, digest_repo, session = _make_client(
        review_repo=review_repo, digest_gateway=digest_gw
    )
    async with client:
        resp = await client.post(
            f"/projects/{pid}/chapters/1/accept",
            # 无需对人物塑造做任何裁决（advisory 无 conflict 通道）。
            json={"final_text": "作者终稿正文。", "decisions": []},
        )
    assert resp.status_code == 200
    assert resp.json()["accepted_version"] == 1
    assert session.commits == 1
    # characterization 留痕不产 decisions（advisory）；晋升照常。
    accepted = await chapter_repo.latest_accepted(pid, 1)
    assert accepted is not None


@pytest.mark.asyncio
async def test_characterization_surfaces_in_review_history() -> None:
    """列 characterization 经 GET .../reviews 透传到历史项（前端面板 seed 用）。"""
    review_repo = FakeReviewRepo()
    pid = uuid.uuid4()
    await review_repo.record(
        pid, 1, conflicts=[], characterization={"issues": [{"character": "沈砚"}]}
    )
    client, _, _, _, _ = _make_client(review_repo=review_repo)
    async with client:
        resp = await client.get(f"/projects/{pid}/chapters/1/reviews")
    assert resp.status_code == 200
    row = resp.json()["reviews"][0]
    assert row["characterization"]["issues"][0]["character"] == "沈砚"


@pytest.mark.asyncio
async def test_accept_with_no_review_succeeds() -> None:
    """无审稿留痕（零冲突）→ gate 直通，仍晋升 + 提炼 digest。"""
    from ww_api.services.digest_extraction import ChapterDigestFacts

    digest_gw = FakeReviewGateway(parsed=ChapterDigestFacts(summary="s"))
    client, chapter_repo, _, digest_repo, session = _make_client(digest_gateway=digest_gw)
    pid = uuid.uuid4()
    async with client:
        resp = await client.post(
            f"/projects/{pid}/chapters/1/accept",
            json={"final_text": "终稿", "decisions": []},
        )
    assert resp.status_code == 200
    assert resp.json()["accepted_version"] == 1
    assert session.commits == 1
    assert len(digest_repo.rows) == 1


# ---- T2.4 冲突 gate 拦截 ----


@pytest.mark.asyncio
async def test_accept_blocks_when_conflict_unresolved() -> None:
    review_repo = FakeReviewRepo()
    pid = uuid.uuid4()
    await _seed_review(
        review_repo,
        pid,
        [
            {"type": "性格漂移", "where": "a", "refs": [], "suggestion": "b"},
            {"type": "设定违例", "where": "c", "refs": [], "suggestion": "d"},
        ],
    )
    digest_gw = FakeReviewGateway(parsed=ContinuityReview())
    client, chapter_repo, _, digest_repo, session = _make_client(
        review_repo=review_repo, digest_gateway=digest_gw
    )
    async with client:
        resp = await client.post(
            f"/projects/{pid}/chapters/1/accept",
            json={
                "final_text": "终稿",
                # 只裁决了 0 号冲突，1 号缺判 → 拦截。
                "decisions": [{"conflict_index": 0, "verdict": "ignore"}],
            },
        )
    assert resp.status_code == 409
    assert resp.json()["error"]["code"] == ErrorCode.CONFLICT_UNRESOLVED
    assert resp.json()["error"]["details"]["missing_conflict_indices"] == [1]
    # 不写库：未晋升、未提炼 digest、未提交、未调 digest 网关。
    assert await chapter_repo.latest_accepted(pid, 1) is None
    assert len(digest_repo.rows) == 0
    assert session.commits == 0
    assert len(digest_gw.requests) == 0


# ---- T2.4 事务回滚 ----


@pytest.mark.asyncio
async def test_accept_rolls_back_when_a_step_fails() -> None:
    """裁决留痕步骤抛错 → 整体回滚（不 commit）。"""
    review_repo = FakeReviewRepo(fail_set_decisions=True)
    pid = uuid.uuid4()
    await _seed_review(review_repo, pid, [])
    from ww_api.services.digest_extraction import ChapterDigestFacts

    digest_gw = FakeReviewGateway(parsed=ChapterDigestFacts(summary="s"))
    client, _, _, _, session = _make_client(review_repo=review_repo, digest_gateway=digest_gw)
    # set_decisions 在事务中途抛错；ASGITransport 默认上抛未处理异常。
    with pytest.raises(RuntimeError, match="rollback test"):
        async with client:
            await client.post(
                f"/projects/{pid}/chapters/1/accept",
                json={"final_text": "终稿", "decisions": []},
            )
    # 关键：事务未 commit（半态不落库，整体回滚）。
    assert session.commits == 0
