from __future__ import annotations

from dataclasses import dataclass
from pathlib import Path

import fastapi

from recoil.api.schemas.board import (
    BeatRefModel,
    BoardDetail,
    BoardModel,
    BoardProvenance,
    StoryGateSummary,
)


def _ref_model() -> BeatRefModel:
    return BeatRefModel(
        project="tartarus",
        episode=1,
        strategy="continuity",
        ordinal=1,
        scene_id="BATCH_001",
        selector="EP001_CONT_001",
    )


def test_board_model_defaults_and_versioning_contract_round_trips() -> None:
    m = BoardModel(
        ref=_ref_model(),
        provenance=None,
        board=None,
        board_artifact=None,
        reason="no_board_pointer",
    )

    assert m.newer_unpointed_versions == 0
    assert m.candidates == []
    assert m.versions == []
    assert m.detail_status is None
    assert m.schema_version == 1
    assert "schema_version" in m.model_dump()
    assert "schemaVersion" in m.model_dump(by_alias=True)

    assert "schemaVersion" not in m.model_dump(by_alias=True)["ref"]
    assert "schema_version" not in m.model_dump()["ref"]
    assert not hasattr(BeatRefModel, "model_fields") or "schema_version" not in BeatRefModel.model_fields

    m2 = BoardModel(
        ref=_ref_model(),
        provenance=BoardProvenance(
            status="proposed",
            approved_by=None,
            source_sha256="x",
            version=4,
            updated_at=None,
            gate=None,
        ),
        board=None,
        board_artifact="prep/ep_001/storyboards/EP001_CONT_001_v04.png",
    )
    assert "schemaVersion" not in m2.model_dump(by_alias=True)["provenance"]
    assert "schema_version" not in BoardProvenance.model_fields

    round_tripped = BoardModel.model_validate(m.model_dump())
    assert round_tripped == m
    alias_round_tripped = BoardModel.model_validate(m.model_dump(by_alias=True))
    assert alias_round_tripped == m


def test_schema_module_is_transport_free() -> None:
    import recoil.api.schemas.board as m

    assert not hasattr(m, "app")
    assert not hasattr(m, "router")
    assert not any(isinstance(value, (fastapi.FastAPI, fastapi.APIRouter)) for value in vars(m).values())


def test_typed_gate_and_board_detail_drop_unreviewed_keys() -> None:
    prov = BoardProvenance(
        status="proposed",
        approved_by=None,
        source_sha256="x",
        version=4,
        updated_at=None,
        gate=StoryGateSummary.from_gate(
            {
                "mode": "shadow",
                "route": "ok",
                "confidence": 0.92,
                "verdict_path": "p.json",
                "EXTRA_LEAK": "should_not_appear",
            }
        ),
    )

    assert isinstance(prov.gate, StoryGateSummary)
    assert prov.gate.mode == "shadow"
    assert prov.gate.confidence == 0.92
    assert "EXTRA_LEAK" not in prov.model_dump()["gate"]
    assert set(prov.model_dump()["gate"].keys()) == {"mode", "route", "confidence", "verdict_path"}

    det = BoardDetail.from_sidecar({"status": "candidate", "approved_by": None, "RAW_SIDECAR_KEY": "leak"})
    assert isinstance(det, BoardDetail)
    assert det.status == "candidate"
    assert set(det.model_dump().keys()) == {"status", "approved_by"}
    assert "RAW_SIDECAR_KEY" not in det.model_dump()

    assert StoryGateSummary.from_gate(None) is None
    assert BoardDetail.from_sidecar(None) is None


def test_fully_populated_wire_shape_locks_typed_subobjects() -> None:
    m = BoardModel(
        ref=_ref_model(),
        board_artifact="prep/ep_001/storyboards/EP001_CONT_001_v04.png",
        board=BoardDetail.from_sidecar(
            {"status": "candidate", "approved_by": None, "RAW_SIDECAR_KEY": "leak"}
        ),
        provenance=BoardProvenance(
            status="proposed",
            approved_by=None,
            source_sha256="x",
            version=4,
            updated_at=None,
            gate=StoryGateSummary.from_gate(
                {
                    "mode": "shadow",
                    "route": "ok",
                    "confidence": 0.92,
                    "verdict_path": "p.json",
                    "EXTRA_LEAK": "should_not_appear",
                }
            ),
        ),
        candidates=["EP001_CONT_001_v04.png", "EP001_CONT_001_v05.png"],
        versions=[4, 5],
        newer_unpointed_versions=1,
    )

    assert set(m.model_dump()["board"].keys()) == {"status", "approved_by"}
    assert set(m.model_dump()["provenance"]["gate"].keys()) == {
        "mode",
        "route",
        "confidence",
        "verdict_path",
    }


def test_beat_ref_model_from_ref_drops_internal_scene_path() -> None:
    @dataclass(frozen=True)
    class Ref:
        project: str
        episode: int
        strategy: str
        ordinal: int
        scene_id: str
        selector: str
        scene_path: Path

    model = BeatRefModel.from_ref(
        Ref(
            project="tartarus",
            episode=1,
            strategy="continuity",
            ordinal=1,
            scene_id="BATCH_001",
            selector="EP001_CONT_001",
            scene_path=Path("ep_001_BATCH_001.json"),
        )
    )

    assert model.selector == "EP001_CONT_001"
    assert "scene_path" not in model.model_dump()
