"""T3.5 大纲写侧 repo 单测（ARCH §5.4 / §7.2）。

纯内存 fake，无 DB：断言 upsert 幂等覆盖、按 project_id 隔离、frozen View 不可变、
`_to_view` 的 beats 包裹/解包 round-trip。`(project_id, chapter_no)` 唯一性是 **DB 级**
（models UniqueConstraint），纯 fake 不引 pg 断言——真实 DB 路径由 T3.7 E2E 覆盖。
"""

from __future__ import annotations

import uuid
from dataclasses import dataclass, field
from typing import Any

import pytest
from pydantic import ValidationError
from ww_core.domain.outline_write_repo import (
    OutlineWriteRepo,
    OutlineWriteView,
    _to_view,
)

PROJECT = uuid.UUID("00000000-0000-0000-0000-000000000001")
OTHER = uuid.UUID("00000000-0000-0000-0000-000000000002")


@dataclass
class _Row:
    """模拟 ORM Outline 行（beats 是 JSONB dict、foreshadow_windows 是 JSONB list）。"""

    project_id: uuid.UUID
    volume: int
    chapter_no: int
    beats: dict[str, Any] | None
    foreshadow_windows: list[Any] = field(default_factory=list)


@dataclass
class FakeOutlineWriteRepo:
    """实现 `OutlineWriteRepo` Protocol（内存；幂等覆盖同 (project_id, chapter_no)）。"""

    rows: dict[tuple[uuid.UUID, int], _Row] = field(default_factory=dict)

    async def upsert_chapter(
        self,
        project_id: uuid.UUID,
        *,
        volume: int,
        chapter_no: int,
        beats: list[str],
        foreshadow_windows: list[dict[str, Any]],
    ) -> OutlineWriteView:
        row = self.rows.get((project_id, chapter_no))
        beats_payload = {"beats": list(beats)}
        windows = [dict(w) for w in foreshadow_windows]
        if row is None:
            row = _Row(
                project_id=project_id,
                volume=volume,
                chapter_no=chapter_no,
                beats=beats_payload,
                foreshadow_windows=windows,
            )
        else:
            row.volume = volume
            row.beats = beats_payload
            row.foreshadow_windows = windows
        self.rows[(project_id, chapter_no)] = row
        return _to_view(row)  # type: ignore[arg-type]


def _repo() -> OutlineWriteRepo:
    return FakeOutlineWriteRepo()


@pytest.mark.asyncio
async def test_upsert_inserts_new_chapter() -> None:
    repo = _repo()
    view = await repo.upsert_chapter(
        PROJECT,
        volume=1,
        chapter_no=1,
        beats=["a", "b"],
        foreshadow_windows=[{"code": "F1", "expected_close_to": 10}],
    )
    assert view.chapter_no == 1
    assert view.volume == 1
    assert view.beats == ["a", "b"]
    assert view.foreshadow_windows[0]["code"] == "F1"


@pytest.mark.asyncio
async def test_upsert_overwrites_same_chapter() -> None:
    repo = _repo()
    await repo.upsert_chapter(PROJECT, volume=1, chapter_no=1, beats=["old"], foreshadow_windows=[])
    view = await repo.upsert_chapter(
        PROJECT, volume=2, chapter_no=1, beats=["new"], foreshadow_windows=[]
    )
    assert view.beats == ["new"]
    assert view.volume == 2


@pytest.mark.asyncio
async def test_upsert_isolated_by_project() -> None:
    repo = FakeOutlineWriteRepo()
    await repo.upsert_chapter(PROJECT, volume=1, chapter_no=1, beats=["p"], foreshadow_windows=[])
    await repo.upsert_chapter(OTHER, volume=1, chapter_no=1, beats=["o"], foreshadow_windows=[])
    assert len(repo.rows) == 2


def test_to_view_unpacks_wrapped_beats() -> None:
    row = _Row(project_id=PROJECT, volume=1, chapter_no=3, beats={"beats": ["x", "y"]})
    view = _to_view(row)  # type: ignore[arg-type]
    assert view.beats == ["x", "y"]


def test_to_view_handles_null_beats() -> None:
    row = _Row(project_id=PROJECT, volume=1, chapter_no=3, beats=None)
    view = _to_view(row)  # type: ignore[arg-type]
    assert view.beats == []


def test_write_view_is_frozen() -> None:
    view = OutlineWriteView(chapter_no=1, volume=1, beats=["a"])
    with pytest.raises(ValidationError):
        view.volume = 2
