"""T3.1 伏笔账本写侧 repo 单测（ARCH §6.2）。

纯内存 fake，无 DB：断言 register 唯一、list_by_status 过滤、record_progress append
不覆盖、transition 走状态机校验、scan_overdue 命中集合、按 project_id 隔离、frozen
View 不可变。`(project_id, code)` 唯一性是 **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.foreshadow_repo import ForeshadowLedgerRepo, ForeshadowLedgerView
from ww_core.domain.foreshadow_state import (
    CLOSED,
    OVERDUE,
    PARTIAL,
    InvalidTransition,
)

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


@dataclass
class _Row:
    project_id: uuid.UUID
    code: str
    title: str
    status: str
    planted_at: int | None
    content: str | None
    expected_close_from: int | None
    expected_close_to: int | None
    importance: str | None
    links: list[Any]
    progress: list[Any]


def _view(row: _Row) -> ForeshadowLedgerView:
    return ForeshadowLedgerView(
        code=row.code,
        title=row.title,
        status=row.status,
        planted_at=row.planted_at,
        content=row.content,
        expected_close_from=row.expected_close_from,
        expected_close_to=row.expected_close_to,
        importance=row.importance,
        links=list(row.links),
        progress=list(row.progress),
    )


@dataclass
class FakeForeshadowLedgerRepo:
    """内存替身——镜像 SqlForeshadowLedgerRepo 语义（按 (project_id, code) 唯一）。"""

    rows: list[_Row] = field(default_factory=list)

    def _find(self, project_id: uuid.UUID, code: str) -> _Row | None:
        return next((r for r in self.rows if r.project_id == project_id and r.code == code), None)

    async def register(
        self,
        project_id: uuid.UUID,
        *,
        code: str,
        title: str,
        planted_at: int | None = None,
        content: str | None = None,
        expected_close_from: int | None = None,
        expected_close_to: int | None = None,
        importance: str | None = None,
    ) -> ForeshadowLedgerView:
        if self._find(project_id, code) is not None:
            raise ValueError(f"duplicate foreshadow code: {code}")
        row = _Row(
            project_id=project_id,
            code=code,
            title=title,
            status="OPEN",
            planted_at=planted_at,
            content=content,
            expected_close_from=expected_close_from,
            expected_close_to=expected_close_to,
            importance=importance,
            links=[],
            progress=[],
        )
        self.rows.append(row)
        return _view(row)

    async def get(self, project_id: uuid.UUID, code: str) -> ForeshadowLedgerView | None:
        row = self._find(project_id, code)
        return _view(row) if row is not None else None

    async def list_by_status(
        self, project_id: uuid.UUID, status: str | None = None
    ) -> list[ForeshadowLedgerView]:
        return [
            _view(r)
            for r in self.rows
            if r.project_id == project_id and (status is None or r.status == status)
        ]

    async def transition(
        self, project_id: uuid.UUID, code: str, *, to_status: str
    ) -> ForeshadowLedgerView:
        from ww_core.domain.foreshadow_state import ForeshadowStatus
        from ww_core.domain.foreshadow_state import transition as apply_transition

        row = self._find(project_id, code)
        if row is None:
            raise LookupError(f"foreshadow not found: {code}")
        row.status = apply_transition(ForeshadowStatus(row.status), ForeshadowStatus(to_status))
        return _view(row)

    async def record_progress(
        self, project_id: uuid.UUID, code: str, *, entry: dict[str, Any]
    ) -> ForeshadowLedgerView:
        row = self._find(project_id, code)
        if row is None:
            raise LookupError(f"foreshadow not found: {code}")
        row.progress = [*row.progress, dict(entry)]
        return _view(row)

    async def scan_overdue(
        self, project_id: uuid.UUID, *, current_chapter: int
    ) -> list[ForeshadowLedgerView]:
        from ww_core.domain.foreshadow_state import ForeshadowStatus, apply_overdue_scan

        changed: list[ForeshadowLedgerView] = []
        for r in self.rows:
            if r.project_id != project_id:
                continue
            new = apply_overdue_scan(
                ForeshadowStatus(r.status),
                current_chapter=current_chapter,
                expected_close_to=r.expected_close_to,
            )
            if new != r.status:
                r.status = new
                changed.append(_view(r))
        return changed


def _repo() -> FakeForeshadowLedgerRepo:
    return FakeForeshadowLedgerRepo()


# ---- register ----


async def test_register_starts_open() -> None:
    repo: ForeshadowLedgerRepo = _repo()
    view = await repo.register(
        PROJECT, code="F1", title="神秘玉佩", planted_at=3, expected_close_to=20
    )
    assert isinstance(view, ForeshadowLedgerView)
    assert view.code == "F1"
    assert view.status == "OPEN"
    assert view.planted_at == 3
    assert view.expected_close_to == 20
    assert view.progress == []


async def test_register_duplicate_code_rejected() -> None:
    repo: ForeshadowLedgerRepo = _repo()
    await repo.register(PROJECT, code="F1", title="a")
    with pytest.raises(ValueError):
        await repo.register(PROJECT, code="F1", title="b")


async def test_register_same_code_other_project_ok() -> None:
    repo: ForeshadowLedgerRepo = _repo()
    await repo.register(PROJECT, code="F1", title="a")
    other = await repo.register(OTHER, code="F1", title="b")
    assert other.code == "F1"


# ---- get / list_by_status ----


async def test_get_returns_none_when_absent() -> None:
    repo: ForeshadowLedgerRepo = _repo()
    assert await repo.get(PROJECT, "nope") is None


async def test_list_by_status_filters() -> None:
    repo: ForeshadowLedgerRepo = _repo()
    await repo.register(PROJECT, code="A", title="a")
    b = await repo.register(PROJECT, code="B", title="b")
    assert b.status == "OPEN"
    await repo.transition(PROJECT, "B", to_status=PARTIAL)

    opens = await repo.list_by_status(PROJECT, "OPEN")
    assert {v.code for v in opens} == {"A"}
    partials = await repo.list_by_status(PROJECT, PARTIAL)
    assert {v.code for v in partials} == {"B"}


async def test_list_by_status_none_returns_all() -> None:
    repo: ForeshadowLedgerRepo = _repo()
    await repo.register(PROJECT, code="A", title="a")
    await repo.register(PROJECT, code="B", title="b")
    assert len(await repo.list_by_status(PROJECT)) == 2


async def test_list_by_status_isolated_by_project() -> None:
    repo: ForeshadowLedgerRepo = _repo()
    await repo.register(PROJECT, code="A", title="a")
    await repo.register(OTHER, code="B", title="b")
    rows = await repo.list_by_status(PROJECT)
    assert len(rows) == 1 and rows[0].code == "A"


# ---- transition (走状态机) ----


async def test_transition_applies_state_machine() -> None:
    repo: ForeshadowLedgerRepo = _repo()
    await repo.register(PROJECT, code="A", title="a")
    v = await repo.transition(PROJECT, "A", to_status=PARTIAL)
    assert v.status == "PARTIAL"
    v2 = await repo.transition(PROJECT, "A", to_status=CLOSED)
    assert v2.status == "CLOSED"


async def test_transition_rejects_illegal() -> None:
    repo: ForeshadowLedgerRepo = _repo()
    await repo.register(PROJECT, code="A", title="a")
    await repo.transition(PROJECT, "A", to_status=CLOSED)
    with pytest.raises(InvalidTransition):
        await repo.transition(PROJECT, "A", to_status=PARTIAL)


# ---- record_progress (append-only) ----


async def test_record_progress_appends_not_overwrites() -> None:
    repo: ForeshadowLedgerRepo = _repo()
    await repo.register(PROJECT, code="A", title="a")
    await repo.record_progress(PROJECT, "A", entry={"chapter": 5, "note": "first hint"})
    v = await repo.record_progress(PROJECT, "A", entry={"chapter": 9, "note": "second"})
    assert len(v.progress) == 2
    assert v.progress[0]["chapter"] == 5
    assert v.progress[1]["chapter"] == 9


# ---- scan_overdue ----


async def test_scan_overdue_marks_due_rows() -> None:
    repo: ForeshadowLedgerRepo = _repo()
    await repo.register(PROJECT, code="DUE", title="d", expected_close_to=10)
    await repo.register(PROJECT, code="NOTYET", title="n", expected_close_to=30)
    await repo.register(PROJECT, code="NOWIN", title="w")  # 无窗口
    changed = await repo.scan_overdue(PROJECT, current_chapter=15)
    assert {v.code for v in changed} == {"DUE"}
    assert all(v.status == "OVERDUE" for v in changed)
    # NOTYET / NOWIN 保持 OPEN
    assert (await repo.get(PROJECT, "NOTYET")).status == "OPEN"  # type: ignore[union-attr]


async def test_scan_overdue_skips_closed() -> None:
    repo: ForeshadowLedgerRepo = _repo()
    await repo.register(PROJECT, code="C", title="c", expected_close_to=10)
    await repo.transition(PROJECT, "C", to_status=CLOSED)
    changed = await repo.scan_overdue(PROJECT, current_chapter=99)
    assert changed == []
    assert (await repo.get(PROJECT, "C")).status == "CLOSED"  # type: ignore[union-attr]


async def test_scan_overdue_isolated_by_project() -> None:
    repo: ForeshadowLedgerRepo = _repo()
    await repo.register(OTHER, code="X", title="x", expected_close_to=5)
    changed = await repo.scan_overdue(PROJECT, current_chapter=99)
    assert changed == []  # OTHER 的行不受 PROJECT 扫描影响


# ---- frozen View ----


async def test_view_is_frozen() -> None:
    repo: ForeshadowLedgerRepo = _repo()
    v = await repo.register(PROJECT, code="A", title="a")
    with pytest.raises(ValidationError):
        v.status = OVERDUE
