"""M1 端到端：立项 → 写一章草稿(mock 网关) → 自动保存（真实 DB，零 token）。

证明 M1 闭环：`POST /projects` → `GET /projects/{id}` → `POST .../draft`(SSE) →
`PUT .../draft`(自动保存)，且 DB 为真源（projects/chapters/usage_ledger 落库）。

确定性 & 零成本：网关用**真实 `Gateway`** 但注入一个吐固定 `Delta` 的假适配器
（不联网、不花 token），其 `ledger` 是**真实 `SqlAlchemyLedgerSink`** —— 闭环含用量
记账也被走通。无 DB 时跳过（对齐 `tests/test_jobs_integration.py`）。

坑（见 memory/gotchas）：
- `ASGITransport` 不跑 lifespan → 用 `asgi-lifespan` 的 `LifespanManager` 触发
  `seed_stub_user`，否则 `projects.owner_id`/`usage_ledger.owner_id` FK 报错。
- `get_sessionmaker` 缓存的 async engine 绑定首个事件循环；每个 DB 测试清缓存重建、
  结束 dispose。
- 网关 `SqlAlchemyLedgerSink` 只 `flush()` 不 `commit()`（写库事务由编排层控制，
  见不变量）；draft 端点在 SSE 流耗尽后对**请求 session** `commit()`，usage_ledger
  方落库。本测试用请求 session 做 ledger，故无需手动提交——即对该提交的回归校验。
"""

from __future__ import annotations

from collections.abc import AsyncIterator
from typing import Annotated

import httpx
import pytest
from asgi_lifespan import LifespanManager
from fastapi import Depends
from sqlalchemy import delete, func, select
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
from ww_db import get_session, get_sessionmaker
from ww_db.models import Chapter, Project, UsageLedger
from ww_llm_gateway import (
    Gateway,
    SqlAlchemyLedgerSink,
    resolve_route,
)
from ww_llm_gateway.adapters.base import (
    Capabilities,
    ProviderResult,
    ProviderUsage,
    StreamChunk,
)
from ww_llm_gateway.types import LlmRequest

# writer 档位的全局默认 provider（config.tier_defaults["writer"] = "deepseek:..."）。
_WRITER_PROVIDER = "deepseek"
# 确定性流式 token（不联网、不花 token）。
_TOKENS = ["第", "一", "章", "：", "开端。"]
# 假适配器在末尾块回报的 token 数（喂记账，证明用量闭环走通）。
_FAKE_INPUT_TOKENS = 7
_FAKE_OUTPUT_TOKENS = 5


class _FakeStreamingAdapter:
    """实现 `ProviderAdapter` Protocol：吐固定 `StreamChunk`，绝不联网。

    末尾块带 `ProviderUsage` → 网关据此记账（cost 经 pricing 表，未知 model→0）。
    """

    provider = _WRITER_PROVIDER

    def capabilities(self) -> Capabilities:
        return Capabilities()

    async def complete(self, req: LlmRequest, model: str) -> ProviderResult:
        # M1 草稿走 stream；complete 不参与本 E2E，仍给确定性实现。
        return ProviderResult(
            text="".join(_TOKENS),
            usage=ProviderUsage(input_tokens=_FAKE_INPUT_TOKENS, output_tokens=_FAKE_OUTPUT_TOKENS),
        )

    async def stream(self, req: LlmRequest, model: str) -> AsyncIterator[StreamChunk]:
        for token in _TOKENS:
            yield StreamChunk(text=token)
        # 末尾用量块（text=""）：网关收尾时落 1 条 usage_ledger。
        yield StreamChunk(
            usage=ProviderUsage(input_tokens=_FAKE_INPUT_TOKENS, output_tokens=_FAKE_OUTPUT_TOKENS)
        )


@pytest.fixture
async def e2e_sm() -> AsyncIterator[async_sessionmaker[AsyncSession]]:
    """真实 DB session 工厂；无 pg 时跳过（每测试清缓存重建 engine、结束 dispose）。"""
    get_sessionmaker.cache_clear()
    maker = get_sessionmaker()
    try:
        async with maker() as probe:
            await probe.execute(select(1))
    except Exception:
        pytest.skip("postgres not reachable")
    yield maker
    await maker.kw["bind"].dispose()
    get_sessionmaker.cache_clear()


def _parse_sse(raw: str) -> list[tuple[str, str]]:
    """把 text/event-stream 原文解析为 `(event, data)` 帧列表。"""
    frames: list[tuple[str, str]] = []
    event: str | None = None
    data: str | None = None
    for line in raw.splitlines():
        if line.startswith("event:"):
            event = line[len("event:") :].strip()
        elif line.startswith("data:"):
            data = line[len("data:") :].strip()
        elif line == "":
            if event is not None and data is not None:
                frames.append((event, data))
            event, data = None, None
    if event is not None and data is not None:
        frames.append((event, data))
    return frames


async def test_m1_closed_loop_project_to_draft_to_autosave(
    e2e_sm: async_sessionmaker[AsyncSession],
) -> None:
    import json

    from ww_api.main import create_app
    from ww_api.services.project_deps import get_writer_gateway

    # 注入：真实 Gateway + 假适配器 + 真实 ledger。ledger 用**请求 session**
    # （`Depends(get_session)` 被 FastAPI 按请求缓存，与 draft 端点同一实例）——
    # 故由端点流末的 `session.commit()` 落库，不在测试里手动提交。这正是对端点
    # 提交修复的回归校验：若端点不提交，下面 usage_ledger 断言会失败。
    def _override_gateway(
        session: Annotated[AsyncSession, Depends(get_session)],
    ) -> Gateway:
        return Gateway(
            adapters={_WRITER_PROVIDER: _FakeStreamingAdapter()},
            ledger=SqlAlchemyLedgerSink(session),
            resolver=resolve_route,
        )

    app = create_app()
    app.dependency_overrides[get_writer_gateway] = _override_gateway

    transport = httpx.ASGITransport(app=app)
    # LifespanManager 触发 lifespan → seed_stub_user（owner_id FK 依赖它）。
    async with LifespanManager(app):
        async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
            # 1) 立项 → 201。唯一标题避免跨次干扰。
            title = "M1 E2E 闭环验证作品"
            create_resp = await client.post(
                "/projects",
                json={
                    "title": title,
                    "genre": "玄幻",
                    "logline": "少年逆袭",
                    "selling_points": ["爽点密集"],
                },
            )
            assert create_resp.status_code == 201
            created = create_resp.json()
            project_id = created["id"]
            assert created["title"] == title

            # 2) GET 详情 → 200，与立项一致。
            get_resp = await client.get(f"/projects/{project_id}")
            assert get_resp.status_code == 200
            fetched = get_resp.json()
            assert fetched["id"] == project_id
            assert fetched["title"] == title
            assert fetched["genre"] == "玄幻"
            assert fetched["selling_points"] == ["爽点密集"]

            # 3) 流式写章草稿 → 消费 SSE：≥1 token 帧 + 终结 done 帧，重组文本。
            draft_resp = await client.post(f"/projects/{project_id}/chapters/1/draft")
            assert draft_resp.status_code == 200
            assert draft_resp.headers["content-type"].startswith("text/event-stream")
            frames = _parse_sse(draft_resp.text)
            token_frames = [d for (ev, d) in frames if ev == "token"]
            done_frames = [d for (ev, d) in frames if ev == "done"]
            error_frames = [d for (ev, d) in frames if ev == "error"]
            assert len(token_frames) >= 1
            assert len(done_frames) == 1
            assert error_frames == []
            streamed_text = "".join(json.loads(d)["text"] for d in token_frames)
            assert streamed_text == "".join(_TOKENS)
            # done 帧带累计长度。
            assert json.loads(done_frames[0])["length"] == len(streamed_text)
            # 不手动提交：draft 端点流末 `session.commit()` 已把 usage_ledger 落库。

            # 4) 自动保存草稿 ← 重组文本 → 200 DraftResponse{status:'draft', version:1}。
            save_resp = await client.put(
                f"/projects/{project_id}/chapters/1/draft",
                json={"text": streamed_text},
            )
            assert save_resp.status_code == 200
            saved = save_resp.json()
            assert saved["project_id"] == project_id
            assert saved["chapter_no"] == 1
            assert saved["status"] == "draft"
            assert saved["version"] == 1
            assert saved["length"] == len(streamed_text)

            # PUT 再次 → 幂等：不新增章节版本（覆盖同一行）。
            save_resp2 = await client.put(
                f"/projects/{project_id}/chapters/1/draft",
                json={"text": streamed_text},
            )
            assert save_resp2.status_code == 200
            assert save_resp2.json()["version"] == 1

    # 5) DB 断言（经 e2e session 查询）——DB 是唯一真源。
    project_uuid = created["id"]
    async with e2e_sm() as verify:
        # projects 行存在且字段一致。
        project_row = (
            await verify.execute(select(Project).where(Project.id == project_uuid))
        ).scalar_one()
        assert project_row.title == title

        # chapters 草稿行：status='draft', version=1, content=已保存文本。
        chapter_rows = (
            (await verify.execute(select(Chapter).where(Chapter.project_id == project_uuid)))
            .scalars()
            .all()
        )
        assert len(chapter_rows) == 1  # 幂等：两次 PUT 仍一行
        chapter = chapter_rows[0]
        assert chapter.chapter_no == 1
        assert chapter.status == "draft"
        assert chapter.version == 1
        assert chapter.content == streamed_text

        # usage_ledger：草稿流至少写 1 条（用量记账闭环走通）。
        ledger_count = (
            await verify.execute(
                select(func.count())
                .select_from(UsageLedger)
                .where(UsageLedger.project_id == project_uuid)
            )
        ).scalar_one()
        assert ledger_count >= 1
        ledger_row = (
            (
                await verify.execute(
                    select(UsageLedger).where(UsageLedger.project_id == project_uuid)
                )
            )
            .scalars()
            .first()
        )
        assert ledger_row is not None
        assert ledger_row.provider == _WRITER_PROVIDER
        assert ledger_row.input_tokens == _FAKE_INPUT_TOKENS
        assert ledger_row.output_tokens == _FAKE_OUTPUT_TOKENS

    # 清理：usage_ledger 的 project FK 无级联 → 先删它，再删 project（chapters 经 FK 级联）。
    async with e2e_sm() as cleanup:
        await cleanup.execute(delete(UsageLedger).where(UsageLedger.project_id == project_uuid))
        await cleanup.execute(delete(Project).where(Project.id == project_uuid))
        await cleanup.commit()
