"""AI 立项方案生成端点测试（灵感⑤；内存替身，无 DB/无网络）。

覆盖 `POST /skills/project-plan/generate`（不带 project 前缀）：
- 无既有 project 也能生成（向导阶段 project 未建）→ 200 结构化预览 + 落 ledger。
- 种子门控：缺 genre / logline → 422，不触达网关、不 commit。
- 无凭据 → 503。
- 向导草稿序列化进网关 context（种子 + 已填字段透传）。
LLM 一律 mock，绝不联网。
"""

from __future__ import annotations

import os
from typing import Any

import httpx
import pytest
from cryptography.fernet import Fernet
from fakes_projects import FakeSession
from ww_agents import ProjectPlanResult
from ww_llm_gateway.types import LlmRequest, LlmResponse, ServedBy, Usage
from ww_shared import AppError, ErrorCode


class _CaptureGateway:
    """记录最近一次 req.input（验证向导草稿进了 context），返固定 ProjectPlanResult。"""

    def __init__(self, parsed: ProjectPlanResult) -> None:
        self._parsed = parsed
        self.last_input: str | None = None
        self.calls = 0

    async def run(self, req: LlmRequest) -> LlmResponse:
        self.calls += 1
        self.last_input = req.input if isinstance(req.input, str) else str(req.input)
        return LlmResponse(
            text=self._parsed.model_dump_json(),
            parsed=self._parsed,
            usage=Usage(
                provider="fake",
                model="fake",
                input_tokens=1,
                output_tokens=1,
                cost_minor=0,
                currency="USD",
            ),
            served_by=ServedBy(provider="fake", model="fake"),
        )


def _plan() -> ProjectPlanResult:
    return ProjectPlanResult(
        title_candidates=["逐光而行", "剑试九霄"],
        setting="上古仙门世界",
        narrative_structure="黄金三章立钩 → 升级",
        story_core="废柴逆袭封神的代价",
        ending_design="归隐收束（正剧）",
        tone="热血",
    )


def _make_app(
    *,
    gateway: Any,
    session: FakeSession | None = None,
    no_creds: bool = False,
) -> tuple[Any, FakeSession]:
    os.environ.setdefault("CREDENTIAL_ENC_KEY", Fernet.generate_key().decode())
    from ww_api.main import create_app
    from ww_api.services.project_deps import get_project_plan_gateway
    from ww_db import get_session

    session = session or FakeSession()

    async def _ok() -> Any:
        return gateway

    async def _no_creds() -> Any:
        raise AppError(ErrorCode.LLM_UNAVAILABLE, "未配置凭据", {"provider": "deepseek"})

    app = create_app()
    app.dependency_overrides[get_project_plan_gateway] = _no_creds if no_creds else _ok
    app.dependency_overrides[get_session] = lambda: session
    return app, session


def _client(app: Any) -> httpx.AsyncClient:
    transport = httpx.ASGITransport(app=app, raise_app_exceptions=False)
    return httpx.AsyncClient(transport=transport, base_url="http://test")


@pytest.mark.asyncio
async def test_generate_without_existing_project() -> None:
    # 向导阶段 project 未建：无 project 前缀端点直接生成，返结构化方案 + 落 ledger。
    gateway = _CaptureGateway(_plan())
    app, session = _make_app(gateway=gateway)

    async with _client(app) as client:
        resp = await client.post(
            "/skills/project-plan/generate",
            json={"genre": "仙侠", "logline": "废柴少年觉醒禁忌血脉，逆势封神。"},
        )

    assert resp.status_code == 200
    body = resp.json()
    assert body["title_candidates"] == ["逐光而行", "剑试九霄"]
    assert body["story_core"] == "废柴逆袭封神的代价"
    assert body["ending_design"].endswith("（正剧）")
    assert body["tone"] == "热血"
    assert session.commits == 1  # 预览不写业务表，但落 ledger


@pytest.mark.asyncio
async def test_generate_serializes_wizard_draft_into_context() -> None:
    # 种子 + 已填字段序列化进网关 context（有则贴合）。
    gateway = _CaptureGateway(_plan())
    app, _ = _make_app(gateway=gateway)

    async with _client(app) as client:
        resp = await client.post(
            "/skills/project-plan/generate",
            json={
                "genre": "都市",
                "logline": "外卖骑手觉醒读心术。",
                "title": "听风者",
                "tone": "轻松",
                "selling_points": ["逆袭", "系统流"],
                "brief": "偏悬疑向",
            },
        )

    assert resp.status_code == 200
    assert gateway.last_input is not None
    assert "题材：都市" in gateway.last_input
    assert "一句话故事：外卖骑手觉醒读心术。" in gateway.last_input
    assert "暂定书名：听风者" in gateway.last_input
    assert "核心卖点：逆袭、系统流" in gateway.last_input
    assert "偏悬疑向" in gateway.last_input  # brief 进 context


@pytest.mark.asyncio
async def test_requires_seed_missing_logline_422() -> None:
    # 种子门控：缺 logline → 422，不触达网关、不 commit。
    gateway = _CaptureGateway(_plan())
    app, session = _make_app(gateway=gateway)

    async with _client(app) as client:
        resp = await client.post("/skills/project-plan/generate", json={"genre": "仙侠"})

    assert resp.status_code == 422
    assert resp.json()["error"]["code"] == ErrorCode.VALIDATION
    assert gateway.calls == 0
    assert session.commits == 0


@pytest.mark.asyncio
async def test_requires_seed_missing_genre_422() -> None:
    gateway = _CaptureGateway(_plan())
    app, session = _make_app(gateway=gateway)

    async with _client(app) as client:
        resp = await client.post("/skills/project-plan/generate", json={"logline": "一句话故事。"})

    assert resp.status_code == 422
    assert gateway.calls == 0
    assert session.commits == 0


@pytest.mark.asyncio
async def test_requires_seed_blank_strings_422() -> None:
    # 纯空白种子也拒（strip 后为空）。
    gateway = _CaptureGateway(_plan())
    app, session = _make_app(gateway=gateway)

    async with _client(app) as client:
        resp = await client.post(
            "/skills/project-plan/generate", json={"genre": "   ", "logline": "  "}
        )

    assert resp.status_code == 422
    assert gateway.calls == 0
    assert session.commits == 0


@pytest.mark.asyncio
async def test_generate_no_credentials_503() -> None:
    app, session = _make_app(gateway=object(), no_creds=True)

    async with _client(app) as client:
        resp = await client.post(
            "/skills/project-plan/generate",
            json={"genre": "仙侠", "logline": "一句话故事。"},
        )

    assert resp.status_code == 503
    assert resp.json()["error"]["code"] == ErrorCode.LLM_UNAVAILABLE
    assert session.commits == 0
