"""F3 提示词/模板库 端到端（真 pg；无网关——模板库不调 LLM）。

证明单用户本地模板库 CRUD 闭环：创建→列出→删除真 pg 回环 + 输入校验（title/body 空 → 422）。
owner_id 全程 stub（单用户原型）。无 pg → skip。

镜像现有 `tests/test_*_e2e.py` 范式：真 `get_sessionmaker`（无 pg skip）+ `LifespanManager` +
`ASGITransport`，DB 真源逐项断言。模板库不触达 LLM 网关，故无需 mock 适配器。
"""

from __future__ import annotations

import uuid
from collections.abc import AsyncIterator

import httpx
import pytest
from asgi_lifespan import LifespanManager
from sqlalchemy import delete, select
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
from ww_db import get_sessionmaker
from ww_db.models import PromptTemplate


@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()


async def _cleanup(sm: async_sessionmaker[AsyncSession], ids: list[uuid.UUID]) -> None:
    if not ids:
        return
    async with sm() as cleanup:
        await cleanup.execute(delete(PromptTemplate).where(PromptTemplate.id.in_(ids)))
        await cleanup.commit()


async def _template_count(sm: async_sessionmaker[AsyncSession], template_id: uuid.UUID) -> int:
    async with sm() as s:
        return len(
            (await s.execute(select(PromptTemplate).where(PromptTemplate.id == template_id)))
            .scalars()
            .all()
        )


async def test_templates_create_list_delete_roundtrip(
    e2e_sm: async_sessionmaker[AsyncSession],
) -> None:
    """用例 1：POST → GET → DELETE 真 pg 回环（行真落 pg；删除后表无行）。"""
    from ww_api.main import create_app

    app = create_app()
    transport = httpx.ASGITransport(app=app)
    created_id: uuid.UUID | None = None
    try:
        async with LifespanManager(app):
            async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
                # 1) POST → 201，模板真落 pg。
                create_resp = await client.post(
                    "/templates",
                    json={
                        "title": "玄幻开篇模板",
                        "body": "请按照黄金三章节奏，开篇即上钩……",
                        "category": "开篇",
                        "tool_key": "opening",
                    },
                )
                assert create_resp.status_code == 201, create_resp.text
                created = create_resp.json()
                assert created["title"] == "玄幻开篇模板"
                assert created["body"] == "请按照黄金三章节奏，开篇即上钩……"
                assert created["category"] == "开篇"
                assert created["tool_key"] == "opening"
                created_id = uuid.UUID(created["id"])

                # DB 真源：模板行真落 pg（owner stub）。
                assert await _template_count(e2e_sm, created_id) == 1

                # 2) GET → 列表含刚建模板。
                list_resp = await client.get("/templates")
                assert list_resp.status_code == 200
                listed = list_resp.json()
                assert created["id"] in [t["id"] for t in listed]
                mine = next(t for t in listed if t["id"] == created["id"])
                assert mine["title"] == "玄幻开篇模板"
                assert mine["body"] == "请按照黄金三章节奏，开篇即上钩……"

                # 3) DELETE → 204；DB 真源行被删。
                del_resp = await client.delete(f"/templates/{created['id']}")
                assert del_resp.status_code == 204
                assert await _template_count(e2e_sm, created_id) == 0

                # 删除后再列出不含该模板。
                after = (await client.get("/templates")).json()
                assert created["id"] not in [t["id"] for t in after]

                # 4) 删除不存在 → 404 NOT_FOUND envelope。
                missing = await client.delete(f"/templates/{uuid.uuid4()}")
                assert missing.status_code == 404
                assert missing.json()["error"]["code"] == "NOT_FOUND"
    finally:
        if created_id is not None:
            await _cleanup(e2e_sm, [created_id])


async def test_templates_create_blank_title_returns_422(
    e2e_sm: async_sessionmaker[AsyncSession],
) -> None:
    """用例 2：title 空 → 422（schema min_length=1）；DB 无行落库。"""
    from ww_api.main import create_app

    app = create_app()
    transport = httpx.ASGITransport(app=app)
    async with LifespanManager(app):
        async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
            before = len((await client.get("/templates")).json())

            resp = await client.post("/templates", json={"title": "", "body": "非空正文"})
            assert resp.status_code == 422, resp.text

            # body 空亦 422（守输入校验纪律）。
            resp_body = await client.post("/templates", json={"title": "有标题", "body": ""})
            assert resp_body.status_code == 422

            # 两次 422 均未落库（列表数量不变）。
            after = len((await client.get("/templates")).json())
            assert after == before
