"""灵感③ 人物塑造 advisory 审单测——并入 review 图 + collect characterization 列 + SSE 事件。

全部注入 mock 网关 / 内存 fake repo，无真 LLM、无真 Postgres（图用 MemorySaver）。
asyncio_mode=auto。验第五审（advisory）落 `characterization` 列、SSE `section` + `characterization`
事件、空 issues 优雅降级、失败隔离不阻塞其余审。
"""

from __future__ import annotations

import uuid
from typing import Any

from fakes_orchestrator import FakeReviewRepo, SchemaRoutingRunGateway
from langchain_core.runnables import RunnableConfig
from langgraph.checkpoint.memory import MemorySaver
from ww_agents import (
    CharacterizationIssue,
    CharacterizationReview,
    ContinuityReview,
    ForeshadowReview,
    PaceReview,
    StyleDriftReview,
)
from ww_core.orchestrator import (
    CHARACTERIZATION,
    CONTINUITY,
    EVENT_CHARACTERIZATION,
    EVENT_DONE,
    EVENT_SECTION,
    REVIEW_INCOMPLETE,
    REVIEW_OK,
    SECTION_DONE,
    SECTION_INCOMPLETE,
    ChapterState,
    build_review_context,
    build_review_graph,
    collect_reviews,
    extract_characterization,
    normalize_review,
)

PROJECT = uuid.UUID("00000000-0000-0000-0000-000000000001")
USER = uuid.UUID("00000000-0000-0000-0000-0000000000aa")

STABLE = "## 世界观硬规则\n灵气可凝丹\n## 人物卡\n沈砚 动机：护妹至死"
VOLATILE = "## 近况\n主角已破境"
DRAFT = "他忽然转身就走，什么也没说。"

_CHAR = CharacterizationReview(
    issues=[
        CharacterizationIssue(
            character="沈砚",
            aspect="动机一致性",
            where="第1段",
            quote="他忽然转身就走，什么也没说。",
            diagnosis="与既定护妹动机断裂",
            suggestion="退缩扣回护妹动机",
            confidence=0.82,
        )
    ]
)


def _state(reviews: dict[str, Any] | None = None) -> ChapterState:
    state: ChapterState = {
        "project_id": PROJECT,
        "chapter_no": 7,
        "user_id": USER,
        "stable_core": STABLE,
        "volatile": VOLATILE,
        "draft": DRAFT,
        "review_context": build_review_context(draft=DRAFT, stable_core=STABLE, volatile=VOLATILE),
    }
    if reviews is not None:
        state["reviews"] = reviews
    return state


def _char_reviews() -> dict[str, Any]:
    return {CHARACTERIZATION: {"status": REVIEW_OK, "result": _CHAR.model_dump()}}


# ---- collect：characterization 列映射（纯函数）----


def test_extract_characterization_returns_whole_dict() -> None:
    char = extract_characterization(_char_reviews())

    assert char is not None
    assert len(char["issues"]) == 1
    assert char["issues"][0]["character"] == "沈砚"
    assert char["issues"][0]["confidence"] == 0.82


def test_extract_characterization_none_when_incomplete() -> None:
    reviews = {CHARACTERIZATION: {"status": REVIEW_INCOMPLETE, "result": None}}
    assert extract_characterization(reviews) is None


def test_extract_characterization_none_when_absent() -> None:
    assert extract_characterization({}) is None


async def test_collect_records_characterization_to_its_column() -> None:
    repo = FakeReviewRepo()

    await collect_reviews(_state(_char_reviews()), review_repo=repo)

    assert len(repo.records) == 1
    rec = repo.records[0]
    assert rec["characterization"]["issues"][0]["aspect"] == "动机一致性"
    # advisory：不进 conflicts（不阻断验收）
    assert rec["conflicts"] == []


# ---- 图：characterization 作为默认第五审并行跑通 ----


async def test_characterization_runs_in_default_review_graph() -> None:
    gateway = SchemaRoutingRunGateway(
        {
            ContinuityReview: ContinuityReview(conflicts=[]),
            ForeshadowReview: ForeshadowReview(),
            PaceReview: PaceReview(),
            StyleDriftReview: StyleDriftReview(),
            CharacterizationReview: _CHAR,
        }
    )
    repo = FakeReviewRepo()
    graph = build_review_graph(gateway, repo, checkpointer=MemorySaver())
    config: RunnableConfig = {"configurable": {"thread_id": "char-1"}}

    final = await graph.ainvoke(_state(), config=config)

    assert final["reviews"][CHARACTERIZATION]["status"] == REVIEW_OK
    rec = repo.records[0]
    assert rec["characterization"]["issues"][0]["character"] == "沈砚"


async def test_characterization_failure_isolated_does_not_block_others() -> None:
    # characterization schema 缺失 → SchemaRoutingRunGateway 抛 KeyError → 隔离为 incomplete
    gateway = SchemaRoutingRunGateway(
        {
            ContinuityReview: ContinuityReview(conflicts=[]),
            ForeshadowReview: ForeshadowReview(),
            PaceReview: PaceReview(),
            StyleDriftReview: StyleDriftReview(),
        }
    )
    repo = FakeReviewRepo()
    graph = build_review_graph(gateway, repo, checkpointer=MemorySaver())
    config: RunnableConfig = {"configurable": {"thread_id": "char-2"}}

    final = await graph.ainvoke(_state(), config=config)

    assert final["reviews"][CHARACTERIZATION]["status"] == REVIEW_INCOMPLETE
    rec = repo.records[0]
    assert rec["characterization"] is None  # 失败列留空，不阻塞其余


# ---- SSE：normalize_review surface characterization 事件 ----


async def test_normalize_review_surfaces_characterization_event() -> None:
    events = [e async for e in normalize_review(_char_reviews())]

    # 单审：section(done) + characterization + done
    assert [e.event for e in events] == [EVENT_SECTION, EVENT_CHARACTERIZATION, EVENT_DONE]
    section = events[0]
    assert section.data == {"name": CHARACTERIZATION, "status": SECTION_DONE}
    char_evt = events[1]
    issues = char_evt.data["issues"]
    assert isinstance(issues, list)
    assert issues[0]["character"] == "沈砚"
    assert issues[0]["confidence"] == 0.82


async def test_characterization_sorts_first_in_multi_review_sse() -> None:
    # 字典序：characterization < continuity → advisory 事件排在最前，不打乱既有四审断言
    reviews = {
        CHARACTERIZATION: {"status": REVIEW_OK, "result": _CHAR.model_dump()},
        CONTINUITY: {"status": REVIEW_OK, "result": {"conflicts": []}},
    }
    events = [e async for e in normalize_review(reviews)]
    names = [e.data.get("name") for e in events if e.event == EVENT_SECTION]
    assert names == [CHARACTERIZATION, CONTINUITY]


async def test_normalize_review_marks_characterization_incomplete() -> None:
    reviews = {CHARACTERIZATION: {"status": REVIEW_INCOMPLETE, "result": None}}

    events = [e async for e in normalize_review(reviews)]

    assert [e.event for e in events] == [EVENT_SECTION, EVENT_DONE]
    assert events[0].data == {"name": CHARACTERIZATION, "status": SECTION_INCOMPLETE}


async def test_characterization_empty_issues_still_records() -> None:
    # 空 issues 降级仍是 ok 结果、照常落库（区别于 incomplete None）
    repo = FakeReviewRepo()
    reviews = {
        CHARACTERIZATION: {"status": REVIEW_OK, "result": CharacterizationReview().model_dump()}
    }

    await collect_reviews(_state(reviews), review_repo=repo)

    assert repo.records[0]["characterization"] == {"issues": []}
