"""T4.2 第四审（style/drift）单测——并入 review 图 + collect style 列 + SSE style 事件（C4 扩）。

全部注入 mock 网关 / 内存 fake repo，无真 LLM、无真 Postgres（图用 MemorySaver）。
asyncio_mode=auto。验四审齐（continuity/foreshadow/pace/style）、collect 落 `style` 列、
SSE `section{name:"style"}` + `style{score,segments}`、无指纹优雅降级。
"""

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,
    Conflict,
    ContinuityReview,
    ForeshadowReview,
    ForeshadowSuggestion,
    PaceIssue,
    PaceReview,
    StyleDriftReview,
    StyleDriftSegment,
)
from ww_core.orchestrator import (
    CHARACTERIZATION,
    CONTINUITY,
    EVENT_CONFLICT,
    EVENT_DONE,
    EVENT_FORESHADOW,
    EVENT_PACE,
    EVENT_SECTION,
    EVENT_STYLE,
    FORESHADOW,
    PACE,
    REVIEW_INCOMPLETE,
    REVIEW_OK,
    REVIEW_SPECS,
    SECTION_DONE,
    STYLE,
    ChapterState,
    build_review_context,
    build_review_graph,
    collect_reviews,
    extract_style,
    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 = "他一拳轰碎山岳。"

_CONFLICT = Conflict(
    type="能力不符", where="第3段", refs=["人物卡:主角"], suggestion="改为震裂巨石"
)
_FORESHADOW = ForeshadowReview(
    planted=[ForeshadowSuggestion(code="FS-MARK", title="胸口胎记", where="第2段", note="皇族")]
)
_PACE = PaceReview(water=[PaceIssue(where="第4段", reason="注水")], hook=True, beat_map=[1, 3, 5])
_STYLE = StyleDriftReview(
    score=72,
    segments=[
        StyleDriftSegment(idx=3, score=40, label="机翻腔"),
        StyleDriftSegment(idx=7, score=55),
    ],
)
_CHARACTERIZATION = CharacterizationReview(
    issues=[
        CharacterizationIssue(
            character="沈砚",
            aspect="动机一致性",
            where="第4段",
            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 _four_reviews() -> dict[str, Any]:
    return {
        CONTINUITY: {"status": REVIEW_OK, "result": {"conflicts": [_CONFLICT.model_dump()]}},
        FORESHADOW: {"status": REVIEW_OK, "result": _FORESHADOW.model_dump()},
        PACE: {"status": REVIEW_OK, "result": _PACE.model_dump()},
        STYLE: {"status": REVIEW_OK, "result": _STYLE.model_dump()},
    }


# ---- REVIEW_SPECS 含第四审 ----


def test_review_specs_include_five_audits() -> None:
    # 四审 + 灵感③ advisory 人物塑造第五审（characterization 末位追加）。
    names = [s.name for s in REVIEW_SPECS]
    assert names == ["continuity", "foreshadow", "pace", "style", "characterization"]


# ---- collect：style 列映射 ----


def test_extract_style_returns_whole_dict() -> None:
    style = extract_style(_four_reviews())

    assert style is not None
    assert style["score"] == 72
    assert len(style["segments"]) == 2
    assert style["segments"][0]["label"] == "机翻腔"


def test_extract_style_none_when_incomplete() -> None:
    reviews = {STYLE: {"status": REVIEW_INCOMPLETE, "result": None}}
    assert extract_style(reviews) is None


def test_extract_style_none_when_absent() -> None:
    assert extract_style({}) is None


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

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

    assert len(repo.records) == 1  # 一次 record 落齐四审
    rec = repo.records[0]
    assert rec["conflicts"][0]["type"] == "能力不符"
    assert len(rec["foreshadow_sug"]) == 1
    assert rec["pace"]["hook"] is True
    assert rec["style"]["score"] == 72  # style→style 列
    assert rec["style"]["segments"][0]["label"] == "机翻腔"


# ---- 四审并行图跑通（mock 网关按 schema 路由产四 parsed）----


async def test_five_review_graph_runs_all_audits_end_to_end() -> None:
    gateway = SchemaRoutingRunGateway(
        {
            ContinuityReview: ContinuityReview(conflicts=[_CONFLICT]),
            ForeshadowReview: _FORESHADOW,
            PaceReview: _PACE,
            StyleDriftReview: _STYLE,
            CharacterizationReview: _CHARACTERIZATION,
        }
    )
    repo = FakeReviewRepo()
    graph = build_review_graph(gateway, repo, checkpointer=MemorySaver())
    config: RunnableConfig = {"configurable": {"thread_id": "rev-style-1"}}

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

    assert final["reviews"][CONTINUITY]["status"] == REVIEW_OK
    assert final["reviews"][FORESHADOW]["status"] == REVIEW_OK
    assert final["reviews"][PACE]["status"] == REVIEW_OK
    assert final["reviews"][STYLE]["status"] == REVIEW_OK
    assert final["reviews"][CHARACTERIZATION]["status"] == REVIEW_OK
    assert len(gateway.calls) == 5  # 五审各调一次网关（含 advisory 人物塑造）
    rec = repo.records[0]
    assert rec["conflicts"] and rec["foreshadow_sug"] and rec["pace"]
    assert rec["style"]["score"] == 72
    assert rec["characterization"]["issues"][0]["character"] == "沈砚"


async def test_four_review_graph_isolates_failing_style_audit() -> None:
    # style schema 缺失 → SchemaRoutingRunGateway 抛 KeyError → run_review 隔离为 incomplete
    gateway = SchemaRoutingRunGateway(
        {
            ContinuityReview: ContinuityReview(conflicts=[_CONFLICT]),
            ForeshadowReview: _FORESHADOW,
            PaceReview: _PACE,
        }
    )
    repo = FakeReviewRepo()
    graph = build_review_graph(gateway, repo, checkpointer=MemorySaver())
    config: RunnableConfig = {"configurable": {"thread_id": "rev-style-2"}}

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

    assert final["reviews"][STYLE]["status"] == REVIEW_INCOMPLETE
    assert final["reviews"][CONTINUITY]["status"] == REVIEW_OK
    rec = repo.records[0]
    assert rec["conflicts"]  # 其余三审照常落库
    assert rec["style"] is None  # style 失败列留空，不阻塞其余


# ---- SSE：normalize_review surface 四审（含 style 事件）----


async def test_normalize_review_surfaces_style_event() -> None:
    events = [e async for e in normalize_review(_four_reviews())]
    kinds = [e.event for e in events]

    # 字典序遍历：continuity < foreshadow < pace < style
    assert kinds == [
        EVENT_SECTION,
        EVENT_CONFLICT,
        EVENT_SECTION,
        EVENT_FORESHADOW,
        EVENT_SECTION,
        EVENT_PACE,
        EVENT_SECTION,
        EVENT_STYLE,
        EVENT_DONE,
    ]
    style_evt = next(e for e in events if e.event == EVENT_STYLE)
    assert style_evt.data["score"] == 72
    segments = style_evt.data["segments"]
    assert segments == [
        {"idx": 3, "score": 40, "label": "机翻腔"},
        {"idx": 7, "score": 55, "label": None},
    ]
    style_section = next(
        e for e in events if e.event == EVENT_SECTION and e.data.get("name") == STYLE
    )
    assert style_section.data["status"] == SECTION_DONE
    assert events[-1].data == {"length": 4}  # 四审项


# ---- 无指纹优雅降级：style 审返回 score=100/空段（默认 schema）落库且 SSE 不报错 ----


async def test_style_graceful_degrade_when_no_fingerprint() -> None:
    # 材料无指纹 → 第四审产默认 StyleDriftReview(score=100, segments=[])（system_prompt 指示）
    degraded = StyleDriftReview()  # score=100, segments=[]
    gateway = SchemaRoutingRunGateway(
        {
            ContinuityReview: ContinuityReview(conflicts=[]),
            ForeshadowReview: ForeshadowReview(),
            PaceReview: PaceReview(),
            StyleDriftReview: degraded,
        }
    )
    repo = FakeReviewRepo()
    graph = build_review_graph(gateway, repo, checkpointer=MemorySaver())
    config: RunnableConfig = {"configurable": {"thread_id": "rev-style-degrade"}}

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

    assert final["reviews"][STYLE]["status"] == REVIEW_OK
    rec = repo.records[0]
    assert rec["style"] == {"score": 100, "segments": []}  # 降级也落库（非 None）

    events = [e async for e in normalize_review(final["reviews"])]
    style_evt = next(e for e in events if e.event == EVENT_STYLE)
    assert style_evt.data["score"] == 100
    assert style_evt.data["segments"] == []
