"""AC-2 `SqlAiMessageRepo` 单测（真 pg——seq/定序是 SQL 语义，fake 无法验证）。

docs/design/ai-chat-history-plan.md §3.1 / §6。验证 append-only 侧表的两操作：
- `append`：一批多条 bubble → 批内 `seq` = 0..n-1、同 `thread_id`、meta round-trip、
  项目级 `chapter_no=None` 持久化；只 flush（提交交调用方）。
- `list_for_chapter`：`(created_at DESC, seq DESC)` newest-first + 分页；`chapter_no`
  非 None → 本章 ∪ 项目级(NULL)；None → 仅项目级；可选 `kind` 过滤。

关键 gotcha（本测据此设计）：Postgres `now()` 是事务起始恒定时刻——同一事务里两批共享
`created_at`。故要拿到「跨批 created_at 不同」必须**每批各自 commit**（模拟每次 POST 独立
事务），批内再靠 `seq` 定序。无 pg → skip。
"""

from __future__ import annotations

import uuid
from collections.abc import AsyncIterator
from datetime import UTC, datetime

import pytest
from pydantic import ValidationError
from sqlalchemy import delete, select
from sqlalchemy.ext.asyncio import AsyncSession
from ww_core.domain.ai_message_repo import (
    AiMessageRepo,
    AiMessageView,
    AiTurnRow,
    SqlAiMessageRepo,
)
from ww_db import get_sessionmaker
from ww_db.models import AiMessage, Project, User


@pytest.fixture
async def seeded() -> AsyncIterator[tuple[AsyncSession, uuid.UUID]]:
    """真 pg session + 一个 seed 好的 project（owner user）。无 pg → skip；结束清理。"""
    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")

    owner_id = uuid.uuid4()
    project_id = uuid.uuid4()
    async with maker() as setup:
        setup.add(User(id=owner_id, email=f"ac2-{owner_id}@local", display_name="ac2"))
        await setup.flush()  # user 先落库，满足 project.owner_id FK。
        setup.add(Project(id=project_id, owner_id=owner_id, title="AC-2 测试作品"))
        await setup.commit()

    session = maker()
    try:
        yield session, project_id
    finally:
        await session.close()
        async with maker() as cleanup:
            await cleanup.execute(delete(AiMessage).where(AiMessage.project_id == project_id))
            await cleanup.execute(delete(Project).where(Project.id == project_id))
            await cleanup.execute(delete(User).where(User.id == owner_id))
            await cleanup.commit()
        await maker.kw["bind"].dispose()
        get_sessionmaker.cache_clear()


async def test_append_assigns_seq_0_based_and_shares_thread_id(
    seeded: tuple[AsyncSession, uuid.UUID],
) -> None:
    # Arrange
    session, project_id = seeded
    repo: AiMessageRepo = SqlAiMessageRepo(session)
    thread_id = uuid.uuid4()

    # Act：一次交换的 3 条 bubble 作为一批 append。
    views = await repo.append(
        project_id,
        thread_id=thread_id,
        chapter_no=5,
        kind="refine",
        tool_key=None,
        rows=[
            AiTurnRow(role="author", content="原选段"),
            AiTurnRow(role="ai", content="重写段", meta={"version_no": 1, "segment": "原选段"}),
            AiTurnRow(role="author", content="再改一版"),
        ],
    )

    # Assert：批内 seq = 0..n-1；同 thread_id；字段/meta round-trip；有 id/created_at。
    assert [v.seq for v in views] == [0, 1, 2]
    assert {v.thread_id for v in views} == {thread_id}
    assert [v.role for v in views] == ["author", "ai", "author"]
    assert [v.content for v in views] == ["原选段", "重写段", "再改一版"]
    assert views[1].meta == {"version_no": 1, "segment": "原选段"}
    assert all(v.chapter_no == 5 and v.kind == "refine" for v in views)
    assert all(isinstance(v.id, uuid.UUID) and v.created_at is not None for v in views)


async def test_append_project_level_chapter_no_none_persists(
    seeded: tuple[AsyncSession, uuid.UUID],
) -> None:
    # Arrange
    session, project_id = seeded
    repo = SqlAiMessageRepo(session)

    # Act：工具箱项目级生成（chapter_no=None）。
    views = await repo.append(
        project_id,
        thread_id=uuid.uuid4(),
        chapter_no=None,
        kind="generator",
        tool_key="expand",
        rows=[AiTurnRow(role="ai", content="扩写预览", meta={"tool_key": "expand"})],
    )

    # Assert
    assert views[0].chapter_no is None
    assert views[0].tool_key == "expand"


async def test_list_newest_first_created_at_desc_then_seq_desc(
    seeded: tuple[AsyncSession, uuid.UUID],
) -> None:
    # Arrange：两批各自 commit（模拟两次 POST 独立事务）→ created_at 不同。
    session, project_id = seeded
    repo = SqlAiMessageRepo(session)
    await repo.append(
        project_id,
        thread_id=uuid.uuid4(),
        chapter_no=1,
        kind="rewrite",
        tool_key=None,
        rows=[AiTurnRow(role="author", content="A0"), AiTurnRow(role="ai", content="A1")],
    )
    await session.commit()
    await repo.append(
        project_id,
        thread_id=uuid.uuid4(),
        chapter_no=1,
        kind="rewrite",
        tool_key=None,
        rows=[AiTurnRow(role="author", content="B0"), AiTurnRow(role="ai", content="B1")],
    )
    await session.commit()

    # Act
    rows = await repo.list_for_chapter(project_id, 1, limit=50, offset=0)

    # Assert：新批（B）整体先于旧批（A）；批内 seq DESC。
    assert [r.content for r in rows] == ["B1", "B0", "A1", "A0"]


async def test_list_chapter_union_includes_project_level(
    seeded: tuple[AsyncSession, uuid.UUID],
) -> None:
    # Arrange：第 5 章 + 项目级(NULL) + 第 6 章 各一批。
    session, project_id = seeded
    repo = SqlAiMessageRepo(session)
    await _append(repo, session, project_id, chapter_no=5, kind="refine", content="ch5")
    await _append(repo, session, project_id, chapter_no=None, kind="generator", content="proj")
    await _append(repo, session, project_id, chapter_no=6, kind="refine", content="ch6")

    # Act：看第 5 章抽屉 = 本章 ∪ 项目级，绝不含第 6 章。
    rows = await repo.list_for_chapter(project_id, 5, limit=50, offset=0)

    # Assert
    contents = {r.content for r in rows}
    assert contents == {"ch5", "proj"}
    assert "ch6" not in contents


async def test_list_project_only_when_chapter_no_none(
    seeded: tuple[AsyncSession, uuid.UUID],
) -> None:
    # Arrange
    session, project_id = seeded
    repo = SqlAiMessageRepo(session)
    await _append(repo, session, project_id, chapter_no=5, kind="refine", content="ch5")
    await _append(repo, session, project_id, chapter_no=None, kind="generator", content="proj")

    # Act：工具箱页（chapter_no=None）只看项目级。
    rows = await repo.list_for_chapter(project_id, None, limit=50, offset=0)

    # Assert
    assert [r.content for r in rows] == ["proj"]


async def test_list_kind_filter(seeded: tuple[AsyncSession, uuid.UUID]) -> None:
    # Arrange
    session, project_id = seeded
    repo = SqlAiMessageRepo(session)
    await _append(repo, session, project_id, chapter_no=2, kind="refine", content="r")
    await _append(repo, session, project_id, chapter_no=2, kind="rewrite", content="w")

    # Act
    rows = await repo.list_for_chapter(project_id, 2, kind="refine", limit=50, offset=0)

    # Assert
    assert [r.kind for r in rows] == ["refine"]
    assert [r.content for r in rows] == ["r"]


async def test_list_pagination_limit_offset(
    seeded: tuple[AsyncSession, uuid.UUID],
) -> None:
    # Arrange：三批各一条（newest-first = c,b,a）。
    session, project_id = seeded
    repo = SqlAiMessageRepo(session)
    await _append(repo, session, project_id, chapter_no=3, kind="continue", content="a")
    await _append(repo, session, project_id, chapter_no=3, kind="continue", content="b")
    await _append(repo, session, project_id, chapter_no=3, kind="continue", content="c")

    # Act + Assert：limit=1 取最新；offset=1 取第二新。
    first = await repo.list_for_chapter(project_id, 3, limit=1, offset=0)
    second = await repo.list_for_chapter(project_id, 3, limit=1, offset=1)
    assert [r.content for r in first] == ["c"]
    assert [r.content for r in second] == ["b"]


async def test_append_only_flushes_not_commits(
    seeded: tuple[AsyncSession, uuid.UUID],
) -> None:
    # Arrange：append 只 flush；另一独立 session 在本 session commit 前看不到行。
    session, project_id = seeded
    repo = SqlAiMessageRepo(session)
    thread_id = uuid.uuid4()
    await repo.append(
        project_id,
        thread_id=thread_id,
        chapter_no=9,
        kind="refine",
        tool_key=None,
        rows=[AiTurnRow(role="ai", content="uncommitted")],
    )

    # Act + Assert：未提交 → 别的连接查不到。
    maker = get_sessionmaker()
    async with maker() as other:
        before = (
            (await other.execute(select(AiMessage).where(AiMessage.thread_id == thread_id)))
            .scalars()
            .all()
        )
    assert before == []

    # 提交后可见。
    await session.commit()
    async with maker() as other:
        after = (
            (await other.execute(select(AiMessage).where(AiMessage.thread_id == thread_id)))
            .scalars()
            .all()
        )
    assert len(after) == 1


def test_view_is_frozen() -> None:
    view = AiMessageView(
        id=uuid.uuid4(),
        project_id=uuid.uuid4(),
        chapter_no=None,
        thread_id=uuid.uuid4(),
        seq=0,
        kind="refine",
        tool_key=None,
        role="ai",
        content="x",
        meta={},
        created_at=datetime.now(UTC),
    )
    with pytest.raises(ValidationError):
        view.content = "y"


async def _append(
    repo: SqlAiMessageRepo,
    session: AsyncSession,
    project_id: uuid.UUID,
    *,
    chapter_no: int | None,
    kind: str,
    content: str,
) -> None:
    """一条 ai bubble 一批 + commit（各批独立事务 → created_at 递增）。"""
    await repo.append(
        project_id,
        thread_id=uuid.uuid4(),
        chapter_no=chapter_no,
        kind=kind,
        tool_key=None,
        rows=[AiTurnRow(role="ai", content=content)],
    )
    await session.commit()
