"""T5.5 Skill registry loader 单测（ARCH §5.6 加载段）。

`SkillRegistry` 从 `SkillRepo`（DB 行抽象）加载声明式 `AgentSpec`（builtin/custom/community），
提供 `get(name)` / `list_scope(scope)` / `names()`。加载时跑 `validate_declaration`——
越权声明（reads/writes 指向未知表）的 skill 直接拒绝（不入册），守 §5.6 表权限契约。

纯内存 fake `SkillRepo`，无 DB：注册行只是声明数据，registry 转成 frozen AgentSpec。
不变量 #2：行里只带 tier，registry 不解析具体 model（model 解析在网关）。
"""

from __future__ import annotations

import pytest
from ww_agents import AgentSpec
from ww_shared import AppError, ErrorCode
from ww_skills import SkillRecord, SkillRegistry


def _record(
    name: str,
    *,
    scope: str = "custom",
    reads: tuple[str, ...] = (),
    writes: tuple[str, ...] = (),
) -> SkillRecord:
    return SkillRecord(
        name=name,
        scope=scope,
        tier="writer",
        system_prompt=f"prompt for {name}",
        reads=reads,
        writes=writes,
        genre=None,
    )


class _FakeSkillRepo:
    def __init__(self, records: list[SkillRecord]) -> None:
        self._records = records

    async def list_all(self) -> list[SkillRecord]:
        return list(self._records)


@pytest.mark.asyncio
async def test_load_builds_specs_keyed_by_name() -> None:
    repo = _FakeSkillRepo(
        [
            _record(
                "worldgen",
                scope="builtin",
                reads=("world_entities",),
                writes=("world_entities",),
            ),
            _record("cpgen", scope="custom", reads=("characters",)),
        ]
    )

    registry = await SkillRegistry.load(repo)

    assert set(registry.names()) == {"worldgen", "cpgen"}
    spec = registry.get("worldgen")
    assert isinstance(spec, AgentSpec)
    assert spec.tier == "writer"
    assert spec.reads == ("world_entities",)
    assert spec.scope == "builtin"


@pytest.mark.asyncio
async def test_get_missing_skill_raises_not_found() -> None:
    registry = await SkillRegistry.load(_FakeSkillRepo([_record("a")]))

    with pytest.raises(AppError) as exc:
        registry.get("missing")

    assert exc.value.code is ErrorCode.NOT_FOUND


@pytest.mark.asyncio
async def test_list_scope_filters_by_scope() -> None:
    repo = _FakeSkillRepo(
        [
            _record("b1", scope="builtin"),
            _record("c1", scope="custom"),
            _record("c2", scope="custom"),
        ]
    )

    registry = await SkillRegistry.load(repo)

    assert {s.name for s in registry.list_scope("custom")} == {"c1", "c2"}
    assert {s.name for s in registry.list_scope("builtin")} == {"b1"}


@pytest.mark.asyncio
async def test_load_rejects_over_permission_skill() -> None:
    # 越权声明（reads 指向未知表）→ 加载即拒绝（守 §5.6），不静默入册。
    repo = _FakeSkillRepo([_record("evil", reads=("secret_table",))])

    with pytest.raises(AppError) as exc:
        await SkillRegistry.load(repo)

    assert exc.value.code is ErrorCode.VALIDATION


@pytest.mark.asyncio
async def test_load_rejects_skill_colliding_with_reserved_review_name() -> None:
    # 用户 skill 偷用四审保留名（continuity）→ 入库即拒（守卫前移，不变量 #3）。
    repo = _FakeSkillRepo([_record("continuity", reads=("chapter_digests",))])

    with pytest.raises(AppError) as exc:
        await SkillRegistry.load(repo)

    assert exc.value.code is ErrorCode.VALIDATION


@pytest.mark.asyncio
async def test_load_rejects_skill_colliding_with_builtin_name() -> None:
    # 用户 skill 冒用非四审的内置 name（brainstorm）→ 入库即拒。
    repo = _FakeSkillRepo([_record("brainstorm", reads=("projects",))])

    with pytest.raises(AppError) as exc:
        await SkillRegistry.load(repo)

    assert exc.value.code is ErrorCode.VALIDATION


@pytest.mark.asyncio
async def test_resolver_build_does_not_raise_on_reserved_collision() -> None:
    # 守卫在入库期（load），resolver.build 纯合并不校验——不得因同名抛错。
    from ww_skills import SpecResolver

    empty = await SkillRegistry.load(_FakeSkillRepo([]))

    resolver = SpecResolver.build(empty)  # 不抛

    assert resolver.get("continuity") is not None


def test_skill_record_reads_writes_are_immutable_tuples() -> None:
    # frozen 只防整字段重绑，不防 list 原地变异——reads/writes 必须是 tuple（无 append）。
    record = _record("immut", reads=("characters",), writes=("world_entities",))

    assert isinstance(record.reads, tuple)
    assert isinstance(record.writes, tuple)
    assert record.reads == ("characters",)
    assert record.writes == ("world_entities",)
    assert not hasattr(record.reads, "append")
