"""REC-235 Phase 2 — wardrobe-continuity index query acceptance tests.

``readmodel.wardrobe_continuity_check`` is a pure cross-batch index query (part of the
``atom_read_model`` capability): given atom-version URNs, resolve each via the Phase 0
resolver, read ``facets["wardrobe"]`` (``{char_id: wardrobe_phase_id}``), and flag any
char that appears with MORE THAN ONE distinct wardrobe_phase_id across the RESOLVED atoms.

Failure model (explicit): an atom that resolves no-data (non-empty ``reason``) is recorded
in ``ContinuityReport.unresolved`` and EXCLUDED from the check — never silently treated as
"no wardrobe" (which would be a false-negative).

NON-VACUITY: the fixture builds the shot the way EpisodeRunner really does —
``beat_metadata["shot"] = dataclasses.asdict(CanonicalShot(... characters=[CharacterEntry(
char_id="JADE", wardrobe_phase_id=...)]))`` (the PROMOTED per-character field
``_facets_for`` reads), NOT a flat ``{"wardrobe": {...}}`` key. A flat-key fixture would
make ``_facets_for`` return ``None`` for wardrobe and the mismatch test would FAIL to flag
— so the explicit wardrobe-facet assertion below proves the query exercises the live shape.
"""

from __future__ import annotations

import dataclasses

from recoil.pipeline._lib.plan_loader import CanonicalShot, CharacterEntry
from recoil.pipeline.core.persistence import save_scene, scene_path
from recoil.pipeline.core.take import Beat, Scene
from recoil.workspace import readmodel as rm
from recoil.workspace.tests.test_readmodel_atom import _take
from recoil.workspace.tests.test_readmodel_board import _make_project


def _shot(shot_id: str, wardrobe_phase_id: str, char: str = "JADE") -> dict:
    """EpisodeRunner shot shape with a PARAMETERIZED promoted wardrobe phase (real shape)."""
    return dataclasses.asdict(
        CanonicalShot(
            shot_id=shot_id,
            scene_index=0,
            sequence_id="BATCH",
            pipeline="video",
            previs_model=None,
            video_model=None,
            location_id="loc_bridge",
            characters=[CharacterEntry(char_id=char, wardrobe_phase_id=wardrobe_phase_id)],
            shot_type="WIDE",
            duration_s=None,
            is_env_only=False,
            has_dialogue=False,
            aspect_ratio=None,
            raw={},
        )
    )


def _save_atom(project: str, scene_id: str, beat_id: str, wardrobe_phase_id: str) -> str:
    """Persist a one-beat/one-take scene (its OWN batch) and return its atom-version URN @t0."""
    beat = Beat(
        beat_id=beat_id,
        takes=[_take(beat_id, 0, None)],
        beat_metadata={"shot": _shot(beat_id, wardrobe_phase_id)},
    )
    save_scene(Scene(scene_id=scene_id, beats=[beat]), scene_path(project, 1, scene_id))
    return f"atom:EP001/beat/{beat_id}@t0"


# ── Mismatch flagged across batches ──────────────────────────────────────────────────────


def test_wardrobe_mismatch_flagged_across_batches(tmp_path, monkeypatch):
    project, *_ = _make_project(tmp_path, monkeypatch)
    # TWO atoms in DIFFERENT scenes/batches; same char JADE at DIFFERENT wardrobe phases.
    urn_a = _save_atom(project, "BATCH_002", "EP001_SH02", "jade_phase_1")
    urn_b = _save_atom(project, "BATCH_005", "EP001_SH05", "jade_phase_2")

    # NON-VACUITY guard: the real promoted shape yields a real wardrobe facet (not None) —
    # a flat-key fixture would make this None and silently defeat the mismatch check.
    assert rm.resolve_atom_version(urn_a, project).facets["wardrobe"] == {"JADE": "jade_phase_1"}

    report = rm.wardrobe_continuity_check([urn_a, urn_b], project)

    assert len(report.wardrobe_mismatches) == 1
    mismatch = report.wardrobe_mismatches[0]
    assert mismatch.char_id == "JADE"
    assert mismatch.phases_by_atom == {urn_a: "jade_phase_1", urn_b: "jade_phase_2"}
    assert report.unresolved == []
    assert report.checked == [urn_a, urn_b]


# ── Consistent wardrobe → no mismatch ────────────────────────────────────────────────────


def test_wardrobe_consistent_no_mismatch(tmp_path, monkeypatch):
    project, *_ = _make_project(tmp_path, monkeypatch)
    # The SAME two cross-batch atoms, JADE at the SAME phase in both → consistent.
    urn_a = _save_atom(project, "BATCH_002", "EP001_SH02", "jade_phase_1")
    urn_b = _save_atom(project, "BATCH_005", "EP001_SH05", "jade_phase_1")

    report = rm.wardrobe_continuity_check([urn_a, urn_b], project)

    assert report.wardrobe_mismatches == []
    assert report.unresolved == []


# ── Unresolved atom is recorded, never a silent false-negative ───────────────────────────


def test_unresolved_atom_excluded_no_false_negative(tmp_path, monkeypatch):
    project, *_ = _make_project(tmp_path, monkeypatch)
    # TWO resolvable atoms with CONFLICTING JADE wardrobe (a real mismatch) PLUS a no-data atom.
    urn_a = _save_atom(project, "BATCH_002", "EP001_SH02", "jade_phase_1")
    urn_b = _save_atom(project, "BATCH_003", "EP001_SH05", "jade_phase_2")
    # A bad-but-well-formed URN: a non-existent beat resolves no-data (a `reason`), NOT a raise.
    urn_bad = "atom:EP001/beat/EP001_SH99@t0"

    report = rm.wardrobe_continuity_check([urn_a, urn_b, urn_bad], project)

    # The no-data atom is VISIBLE in `unresolved` — never silently treated as "no wardrobe"
    # (a false-negative impl would drop it entirely and leave `unresolved` empty).
    assert urn_bad in report.unresolved
    # ...AND it must NOT mask the real JADE phase_1-vs-phase_2 mismatch among the resolved atoms.
    # (A record-then-short-circuit impl would record `unresolved` then return [] mismatches —
    # this assertion kills that counterexample.)
    assert len(report.wardrobe_mismatches) == 1
    assert report.wardrobe_mismatches[0].char_id == "JADE"
    assert report.checked == [urn_a, urn_b, urn_bad]
