"""T5.1 生成节点单测：worldbuilder + character-gen + 入库前 continuity 校验缝。

注入 mock 网关（产 WorldGenResult / CharacterGenResult / ContinuityReview parsed），
无真 LLM、无真 Postgres。生成节点是独立生成（不在写章/审稿流水线），裸函数；
只产结构化产物，**不写库**（持久化属 T5.2 入库端点，不变量 #3）。asyncio_mode=auto。
"""

from __future__ import annotations

import uuid

import pytest
from fakes_orchestrator import FailingRunGateway, FakeRunGateway, SchemaRoutingRunGateway
from ww_agents import (
    CharacterCard,
    CharacterGenResult,
    CharacterRelation,
    Conflict,
    ContinuityReview,
    WorldEntityCard,
    WorldGenResult,
    character_gen_spec,
    continuity_spec,
    worldbuilder_spec,
)
from ww_core.orchestrator import (
    build_character_gen_context,
    build_precheck_context,
    precheck_generated_cards,
    run_character_gen,
    run_worldbuilder,
)

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

_WORLD = WorldGenResult(
    entities=[
        WorldEntityCard(type="力量体系", name="九转炼气", rules=["逐境突破不可越级"]),
    ]
)

_CARDS = CharacterGenResult(
    cards=[
        CharacterCard(
            name="苏黎",
            role="对手",
            traits=["表面冷漠"],
            backstory="出身敌对势力",
            arc="复仇 → 救赎",
            speech_tics=["惯用反问"],
            tags=["亦正亦邪"],
            relations=[CharacterRelation(name="主角", kind="宿敌")],
        )
    ]
)


# ---- run_worldbuilder：返回结构化世界观，只读不写库 ----


async def test_run_worldbuilder_returns_structured_entities() -> None:
    gateway = FakeRunGateway(_WORLD)

    result = await run_worldbuilder(
        worldbuilder_spec,
        brief="设计一个修真世界的力量体系",
        project_context="题材：修真；立意：逆袭",
        gateway=gateway,
        user_id=USER,
        project_id=PROJECT,
    )

    assert isinstance(result, WorldGenResult)
    assert result.entities[0].name == "九转炼气"
    assert result.entities[0].rules == ["逐境突破不可越级"]
    assert gateway.call_count == 1


async def test_run_worldbuilder_forwards_writer_tier_request() -> None:
    gateway = FakeRunGateway(_WORLD)

    await run_worldbuilder(
        worldbuilder_spec,
        brief="设计力量体系",
        project_context="题材：修真",
        gateway=gateway,
        user_id=USER,
        project_id=PROJECT,
    )

    req = gateway.last_request
    assert req is not None
    assert req.tier == "writer"  # 不变量②：spec 档位透传
    assert req.stream is False  # 生成用 run() 非 stream()
    assert req.output_schema is WorldGenResult
    assert req.system[0].cache is True  # 不变量#9
    assert "设计力量体系" in req.input
    assert "题材：修真" in req.input


async def test_run_worldbuilder_raises_when_gateway_fails() -> None:
    # 独立生成：网关失败直接上抛（端点/T5.2 处理），不静默吞。
    gateway = FailingRunGateway(RuntimeError("provider down"))

    with pytest.raises(RuntimeError):
        await run_worldbuilder(
            worldbuilder_spec,
            brief="x",
            project_context="y",
            gateway=gateway,
            user_id=USER,
            project_id=PROJECT,
        )


async def test_run_worldbuilder_raises_when_parsed_missing() -> None:
    class _NoParseGateway:
        async def run(self, req: object) -> object:
            from ww_llm_gateway.types import LlmResponse, ServedBy, Usage

            return LlmResponse(
                text="{}",
                parsed=None,
                usage=Usage(
                    provider="fake",
                    model="fake-writer",
                    input_tokens=1,
                    output_tokens=1,
                    cost_minor=0,
                    currency="USD",
                ),
                served_by=ServedBy(provider="fake", model="fake-writer"),
            )

    with pytest.raises(ValueError, match="parsed"):
        await run_worldbuilder(
            worldbuilder_spec,
            brief="x",
            project_context="y",
            gateway=_NoParseGateway(),  # type: ignore[arg-type]
            user_id=USER,
            project_id=PROJECT,
        )


# ---- build_character_gen_context：群像防雷同（注入已有 + 已生成）----


def test_build_character_gen_context_injects_brief_count_role() -> None:
    ctx = build_character_gen_context(
        brief="给我一个亦正亦邪的女二",
        count=3,
        role="CP",
        world_context="力量体系：九转炼气（逐境突破）",
        existing_chars=[],
        generated_so_far=[],
    )

    assert "给我一个亦正亦邪的女二" in ctx
    assert "3" in ctx
    assert "CP" in ctx
    assert "九转炼气" in ctx


def test_build_character_gen_context_injects_existing_and_generated() -> None:
    # M5-a 防雷同：已有角色 + 本批已生成卡都进上下文，要求差异化
    existing = [CharacterCard(name="李白", role="主角", backstory="b", arc="a")]
    generated = [CharacterCard(name="苏黎", role="对手", backstory="b2", arc="a2")]

    ctx = build_character_gen_context(
        brief="再来一个配角",
        count=1,
        role=None,
        world_context="",
        existing_chars=existing,
        generated_so_far=generated,
    )

    assert "李白" in ctx  # 已有角色
    assert "苏黎" in ctx  # 本批已生成卡


def test_build_character_gen_context_injects_role_mix() -> None:
    # ⑦ 群像按定位配比：role_mix 各定位 + 数量都进上下文，指示模型按配比分配
    ctx = build_character_gen_context(
        brief="给我一组群像",
        count=6,
        role=None,
        world_context="",
        existing_chars=[],
        generated_so_far=[],
        role_mix={"主角": 1, "对手": 2, "配角": 3},
    )

    assert "配比" in ctx  # 配比块标识
    assert "主角" in ctx and "对手" in ctx and "配角" in ctx
    assert "1" in ctx and "2" in ctx and "3" in ctx


def test_build_character_gen_context_role_mix_overrides_single_role() -> None:
    # role_mix 在场时用配比块，不再输出单一「角色定位：role」行
    ctx = build_character_gen_context(
        brief="群像",
        count=3,
        role="主角",  # 应被 role_mix 覆盖
        world_context="",
        existing_chars=[],
        generated_so_far=[],
        role_mix={"对手": 3},
    )

    assert "角色定位：主角" not in ctx
    assert "对手" in ctx


def test_build_character_gen_context_handles_empty_anti_dup() -> None:
    # 首次单生成：无已有/已生成时仍产出可用上下文（不崩）
    ctx = build_character_gen_context(
        brief="主角",
        count=1,
        role="主角",
        world_context="",
        existing_chars=[],
        generated_so_far=[],
    )
    assert "主角" in ctx


# ---- run_character_gen：返回结构化群像，注入防雷同上下文 ----


async def test_run_character_gen_returns_structured_cards() -> None:
    gateway = FakeRunGateway(_CARDS)

    result = await run_character_gen(
        character_gen_spec,
        brief="亦正亦邪的女二",
        count=1,
        role="对手",
        world_context="力量体系：九转炼气",
        existing_chars=[],
        generated_so_far=[],
        gateway=gateway,
        user_id=USER,
        project_id=PROJECT,
    )

    assert isinstance(result, CharacterGenResult)
    assert result.cards[0].name == "苏黎"
    assert gateway.call_count == 1


async def test_run_character_gen_injects_anti_dup_into_request() -> None:
    existing = [CharacterCard(name="李白", role="主角", backstory="b", arc="a")]
    gateway = FakeRunGateway(_CARDS)

    await run_character_gen(
        character_gen_spec,
        brief="配角",
        count=2,
        role=None,
        world_context="九转炼气",
        existing_chars=existing,
        generated_so_far=[],
        gateway=gateway,
        user_id=USER,
        project_id=PROJECT,
    )

    req = gateway.last_request
    assert req is not None
    assert req.tier == "writer"
    assert req.output_schema is CharacterGenResult
    assert "李白" in req.input  # 已有角色注入（防雷同）
    assert "九转炼气" in req.input  # 世界观约束注入


async def test_run_character_gen_injects_role_mix_into_request() -> None:
    # ⑦ role_mix 穿参：run → build_character_gen_context → 请求 input 含各定位
    gateway = FakeRunGateway(_CARDS)

    await run_character_gen(
        character_gen_spec,
        brief="群像",
        count=5,
        role=None,
        world_context="",
        existing_chars=[],
        generated_so_far=[],
        gateway=gateway,
        user_id=USER,
        project_id=PROJECT,
        role_mix={"主角": 2, "配角": 3},
    )

    req = gateway.last_request
    assert req is not None
    assert "配角" in req.input  # 配比定位注入


async def test_run_character_gen_raises_when_gateway_fails() -> None:
    gateway = FailingRunGateway(RuntimeError("provider down"))

    with pytest.raises(RuntimeError):
        await run_character_gen(
            character_gen_spec,
            brief="x",
            count=1,
            role=None,
            world_context="",
            existing_chars=[],
            generated_so_far=[],
            gateway=gateway,
            user_id=USER,
            project_id=PROJECT,
        )


# ---- precheck_generated_cards：入库前 continuity 校验缝（ARCH §6.5）----


def test_build_precheck_context_includes_cards_and_truth() -> None:
    ctx = build_precheck_context(
        cards=_CARDS.cards,
        world_context="力量体系：九转炼气（逐境突破不可越级）",
        characters_context="主角：李白",
    )

    assert "苏黎" in ctx  # 待校验角色卡
    assert "九转炼气" in ctx  # 世界观真相源
    assert "李白" in ctx  # 已有角色真相源


async def test_precheck_returns_conflicts_when_card_violates_world() -> None:
    # 编排器入库前追加 continuity 校验：角色能力越级 → 检出设定违例冲突
    review = ContinuityReview(
        conflicts=[
            Conflict(
                type="设定违例",
                where="苏黎背景：天生跨境界",
                refs=["九转炼气：逐境突破不可越级"],
                suggestion="改为逐境修炼",
            )
        ]
    )
    gateway = FakeRunGateway(review)

    conflicts = await precheck_generated_cards(
        continuity_spec,
        cards=_CARDS.cards,
        world_context="力量体系：九转炼气（逐境突破不可越级）",
        characters_context="",
        gateway=gateway,
        user_id=USER,
        project_id=PROJECT,
    )

    assert len(conflicts) == 1
    assert conflicts[0].type == "设定违例"
    assert gateway.call_count == 1
    # 校验用 continuity 档位（analyst），且只读
    req = gateway.last_request
    assert req is not None
    assert req.tier == "analyst"
    assert req.output_schema is ContinuityReview


async def test_precheck_returns_empty_when_no_conflict() -> None:
    gateway = FakeRunGateway(ContinuityReview(conflicts=[]))

    conflicts = await precheck_generated_cards(
        continuity_spec,
        cards=_CARDS.cards,
        world_context="力量体系：九转炼气",
        characters_context="",
        gateway=gateway,
        user_id=USER,
        project_id=PROJECT,
    )

    assert conflicts == []


async def test_precheck_uses_continuity_schema_routing() -> None:
    # 校验缝复用 continuity_spec → 网关按 output_schema 路由产 ContinuityReview
    gateway = SchemaRoutingRunGateway({ContinuityReview: ContinuityReview(conflicts=[])})

    conflicts = await precheck_generated_cards(
        continuity_spec,
        cards=_CARDS.cards,
        world_context="w",
        characters_context="c",
        gateway=gateway,
        user_id=USER,
        project_id=PROJECT,
    )

    assert conflicts == []
    assert gateway.calls == [ContinuityReview]


async def test_precheck_raises_when_parsed_missing() -> None:
    class _NoParseGateway:
        async def run(self, req: object) -> object:
            from ww_llm_gateway.types import LlmResponse, ServedBy, Usage

            return LlmResponse(
                text="{}",
                parsed=None,
                usage=Usage(
                    provider="fake",
                    model="fake-analyst",
                    input_tokens=1,
                    output_tokens=1,
                    cost_minor=0,
                    currency="USD",
                ),
                served_by=ServedBy(provider="fake", model="fake-analyst"),
            )

    with pytest.raises(ValueError, match="parsed"):
        await precheck_generated_cards(
            continuity_spec,
            cards=_CARDS.cards,
            world_context="w",
            characters_context="c",
            gateway=_NoParseGateway(),  # type: ignore[arg-type]
            user_id=USER,
            project_id=PROJECT,
        )
