"""编排器测试替身——mock 网关（`stream` / `run`）+ 内存审稿留痕 repo。

绝对导入（无包目录下不能相对导入，见 gotchas 2026-06-18）：`from fakes_orchestrator import ...`。
"""

from __future__ import annotations

import uuid
from collections.abc import AsyncIterator
from typing import Any

from pydantic import BaseModel
from ww_llm_gateway.types import (
    Delta,
    LlmRequest,
    LlmResponse,
    ServedBy,
    Usage,
)


class FakeStreamGateway:
    """按预设 token 列表吐 `Delta`，并记录收到的 `LlmRequest`（断言请求构造）。"""

    def __init__(self, tokens: list[str]) -> None:
        self._tokens = tokens
        self.last_request: LlmRequest | None = None
        self.call_count = 0

    async def stream(self, req: LlmRequest) -> AsyncIterator[Delta]:
        self.last_request = req
        self.call_count += 1
        for tok in self._tokens:
            yield Delta(text=tok)


class RaisingGateway:
    """先吐几个 token，再抛指定异常——模拟流式中途失败（验 SSE error 归一）。"""

    def __init__(self, tokens: list[str], exc: Exception) -> None:
        self._tokens = tokens
        self._exc = exc

    async def stream(self, req: LlmRequest) -> AsyncIterator[Delta]:
        for tok in self._tokens:
            yield Delta(text=tok)
        raise self._exc


def _stub_usage() -> Usage:
    return Usage(
        provider="fake",
        model="fake-analyst",
        input_tokens=1,
        output_tokens=1,
        cost_minor=0,
        currency="USD",
    )


class FakeRunGateway:
    """按预设 `parsed` 实例回 `LlmResponse`，记录收到的请求（断言审稿请求构造）。"""

    def __init__(self, parsed: BaseModel) -> None:
        self._parsed = parsed
        self.last_request: LlmRequest | None = None
        self.call_count = 0

    async def run(self, req: LlmRequest) -> LlmResponse:
        self.last_request = req
        self.call_count += 1
        return LlmResponse(
            text=self._parsed.model_dump_json(),
            parsed=self._parsed,
            usage=_stub_usage(),
            served_by=ServedBy(provider="fake", model="fake-analyst"),
        )


class SchemaRoutingRunGateway:
    """按 `req.output_schema` 路由返对应 `parsed`——并行三审各拿自己 schema 的产物。

    `by_schema`：{schema 类型: 该 schema 的实例}。命中即回该 parsed；未命中抛 KeyError
    （测试要保证三审 schema 都登记）。统计每个 schema 的调用次数。
    """

    def __init__(self, by_schema: dict[type[BaseModel], BaseModel]) -> None:
        self._by_schema = by_schema
        self.calls: list[type[BaseModel] | None] = []

    async def run(self, req: LlmRequest) -> LlmResponse:
        schema = req.output_schema
        self.calls.append(schema)
        parsed = self._by_schema[schema] if schema is not None else None
        return LlmResponse(
            text=parsed.model_dump_json() if parsed is not None else "",
            parsed=parsed,
            usage=_stub_usage(),
            served_by=ServedBy(provider="fake", model="fake-analyst"),
        )


class FailingRunGateway:
    """`run` 抛指定异常——验「某审失败不阻塞其余」+ 未完成占位归一。"""

    def __init__(self, exc: Exception) -> None:
        self._exc = exc
        self.call_count = 0

    async def run(self, req: LlmRequest) -> LlmResponse:
        self.call_count += 1
        raise self._exc


class FakeReviewRepo:
    """内存审稿留痕 repo（实现 `ReviewRepo` Protocol 的 `record`）。"""

    def __init__(self) -> None:
        self.records: list[dict[str, Any]] = []

    async def record(
        self,
        project_id: uuid.UUID,
        chapter_no: int,
        *,
        chapter_version: int | None = None,
        conflicts: list[dict[str, Any]],
        foreshadow_sug: list[dict[str, Any]] | None = None,
        style: dict[str, Any] | None = None,
        pace: dict[str, Any] | None = None,
        characterization: dict[str, Any] | None = None,
        health_score: int | None = None,
    ) -> Any:
        entry = {
            "id": uuid.uuid4(),
            "project_id": project_id,
            "chapter_no": chapter_no,
            "chapter_version": chapter_version,
            "conflicts": list(conflicts),
            "foreshadow_sug": list(foreshadow_sug or []),
            "style": style,
            "pace": pace,
            "characterization": characterization,
            "health_score": health_score,
        }
        self.records.append(entry)
        return entry
