"""T3.4 大纲节点单测——build_outline_request + run_outline（C6 扩 / ARCH §5.4 §6.2）。

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

from __future__ import annotations

import uuid

import pytest
from fakes_orchestrator import FailingRunGateway, FakeRunGateway
from ww_agents import (
    ForeshadowWindow,
    OutlineChapter,
    OutlineResult,
    outliner_spec,
)
from ww_core.orchestrator import build_outline_request, run_outline

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

CONTEXT = "## 立意\n复仇成长\n## 已开伏笔\nFS-BIRTH: 身世之谜（待第20-30章回收）"

_OUTLINE = OutlineResult(
    chapters=[
        OutlineChapter(
            no=1,
            beats=["主角登场", "埋身世伏笔"],
            foreshadow_windows=[
                ForeshadowWindow(
                    code="FS-BIRTH",
                    plant_chapter=1,
                    expected_close_from=20,
                    expected_close_to=30,
                )
            ],
        ),
        OutlineChapter(no=2, beats=["遭遇对手"], foreshadow_windows=[]),
    ]
)


# ---- build_outline_request：tier 不变量、缓存断点、output_schema、run 非 stream ----


def test_build_outline_request_uses_spec_tier_and_cached_prompt() -> None:
    req = build_outline_request(outliner_spec, context=CONTEXT, user_id=USER, project_id=PROJECT)

    assert req.tier == "analyst"  # 不变量②：spec 档位透传，不传 model
    assert req.stream is False  # 排大纲用 run() 非 stream()
    assert len(req.system) == 1
    assert req.system[0].text == outliner_spec.system_prompt
    assert req.system[0].cache is True  # 不变量#9
    assert req.input == CONTEXT
    assert req.output_schema is OutlineResult
    assert req.scope.user_id == USER
    assert req.scope.project_id == PROJECT


# ---- run_outline：返回结构化大纲含 foreshadow_windows，只读不写库 ----


async def test_run_outline_returns_structured_outline() -> None:
    gateway = FakeRunGateway(_OUTLINE)

    result = await run_outline(
        outliner_spec, context=CONTEXT, gateway=gateway, user_id=USER, project_id=PROJECT
    )

    assert isinstance(result, OutlineResult)
    assert len(result.chapters) == 2
    assert result.chapters[0].no == 1
    window = result.chapters[0].foreshadow_windows[0]
    assert window.code == "FS-BIRTH"
    assert window.expected_close_to == 30
    assert gateway.call_count == 1


async def test_run_outline_forwards_constructed_request() -> None:
    gateway = FakeRunGateway(_OUTLINE)

    await run_outline(
        outliner_spec, context=CONTEXT, gateway=gateway, user_id=USER, project_id=PROJECT
    )

    req = gateway.last_request
    assert req is not None
    assert req.tier == "analyst"
    assert req.output_schema is OutlineResult
    assert req.input == CONTEXT


async def test_run_outline_raises_when_gateway_fails() -> None:
    # 大纲是独立生成（非流水线并行审）；网关失败直接上抛，端点/T3.5 处理。
    gateway = FailingRunGateway(RuntimeError("provider down"))

    with pytest.raises(RuntimeError):
        await run_outline(
            outliner_spec,
            context=CONTEXT,
            gateway=gateway,
            user_id=USER,
            project_id=PROJECT,
        )


async def test_run_outline_raises_when_parsed_missing() -> None:
    # 带 output_schema 时 parsed 必非 None（C1）；若网关违约返回 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 run_outline(
            outliner_spec,
            context=CONTEXT,
            gateway=_NoParseGateway(),  # type: ignore[arg-type]
            user_id=USER,
            project_id=PROJECT,
        )
