"""W21B-wave-4 C3: shot_aware_bg_render_plan projection-card enrichment.

Pure-unit tests for ``enrich_nodes_with_projection_cards`` — the
deterministic post-routing pass that stamps the per-node projection-card
fields (anchor shot + card provenance + reuse inheritance) the plan vNext
owns (wiring brief §5/§8/§9). NO LLM / VLM / image / DB. Card consumption
is deterministic: the function reads the projection-card checkpoint index
that the step assembles and never re-derives card content.

Contract locked with Codex (2026-05-31 C3 direction consensus):
  - plan-level ``projection_card_state`` ∈
    {pass, needs_review, blocked, not_available, not_applicable}.
      * not_available = projection subsystem unavailable (card step OFF /
        checkpoint absent / card_index None).
      * not_applicable = this bg is not a projection target (no card for
        any of its shots).
  - reuse inheritance is FAIL-CLOSED: a reuse_existing_plate node copies
    the resolved projection fields of its render target (never a forced
    ``pass``); a missing target projection → not_available, never a false
    pass.
  - projection_card_id == projection_card_hash == the card envelope's
    content-addressed ``card_id`` in v0.
"""
from __future__ import annotations

from app.modules.pipeline.shot_aware_bg_render_plan import (
    enrich_nodes_with_projection_cards,
)


def _node(bg_id, node_index, *, render_action="render_new_plate", reuse_target=""):
    """Minimal post-routing node carrying exactly what enrichment reads."""
    return {
        "bg_id": bg_id,
        "node_index": node_index,
        "mode": "fp_seeded_anchor" if node_index == 0 else "reference_derived",
        "is_dwelling_identity_anchor": node_index == 0,
        "camera_decision": {"camera_unit": 1, "look_at_unit": 1},
        "render_action": render_action,
        "reuse_target_bg_id": reuse_target,
    }


def _readiness(**bg_to_shots):
    """per_bg_readiness: {bg_id: {applies_to_shots: [...]}}."""
    return {bg: {"applies_to_shots": list(shots)} for bg, shots in bg_to_shots.items()}


def _card(state, card_id, fallback=""):
    return {"card_state": state, "card_id": card_id, "fallback_reason": fallback}


def test_render_new_plate_with_passing_card_stamps_self_provenance():
    nodes = [_node("A", 0)]
    card_index = {"A": {"S1_Shot1": _card("pass", "cardA")}}
    out, summary = enrich_nodes_with_projection_cards(
        nodes=nodes,
        card_index=card_index,
        per_bg_readiness=_readiness(A=["S1_Shot1"]),
    )
    n = out[0]
    assert n["anchor_shot_id"] == "S1_Shot1"
    assert n["projection_card_id"] == "cardA"
    assert n["projection_card_hash"] == "cardA"
    assert n["projection_card_state"] == "pass"
    assert n["projection_card_fallback_reason"] == ""
    assert n["projection_card_source_bg_id"] == "A"
    assert n["projection_card_inherited"] is False
    # camera_decision / render_action untouched (additive enrichment).
    assert n["camera_decision"] == {"camera_unit": 1, "look_at_unit": 1}
    assert n["render_action"] == "render_new_plate"
    assert summary["projection_card_state_counts"]["pass"] == 1


def test_anchor_selection_prefers_best_card_state():
    """A bg with a blocked early shot + a passing later shot anchors on the
    passing shot (pass > needs_review > blocked), not on shot order."""
    nodes = [_node("A", 0)]
    card_index = {
        "A": {
            "S1_Shot1": _card("blocked", "card_blocked", "leak_detected"),
            "S1_Shot2": _card("pass", "card_pass"),
        }
    }
    out, _ = enrich_nodes_with_projection_cards(
        nodes=nodes,
        card_index=card_index,
        per_bg_readiness=_readiness(A=["S1_Shot1", "S1_Shot2"]),
    )
    n = out[0]
    assert n["anchor_shot_id"] == "S1_Shot2"
    assert n["projection_card_state"] == "pass"
    assert n["projection_card_id"] == "card_pass"


def test_anchor_selection_tie_break_is_applies_order():
    """Equal card_state → earliest shot in applies_to_shots wins."""
    nodes = [_node("A", 0)]
    card_index = {
        "A": {
            "S1_Shot1": _card("needs_review", "card1", "low_confidence"),
            "S1_Shot2": _card("needs_review", "card2", "low_confidence"),
        }
    }
    out, _ = enrich_nodes_with_projection_cards(
        nodes=nodes,
        card_index=card_index,
        per_bg_readiness=_readiness(A=["S1_Shot1", "S1_Shot2"]),
    )
    n = out[0]
    assert n["anchor_shot_id"] == "S1_Shot1"
    assert n["projection_card_state"] == "needs_review"
    assert n["projection_card_fallback_reason"] == "low_confidence"


def test_card_index_none_is_not_available_additive_only():
    """card_index None (subsystem OFF) → every node not_available with self
    provenance; existing node fields untouched (additive)."""
    nodes = [_node("A", 0), _node("B", 1)]
    out, summary = enrich_nodes_with_projection_cards(
        nodes=nodes,
        card_index=None,
        per_bg_readiness=_readiness(A=["S1_Shot1"], B=["S1_Shot2"]),
    )
    for n in out:
        assert n["projection_card_state"] == "not_available"
        assert n["anchor_shot_id"] == ""
        assert n["projection_card_id"] == ""
        assert n["projection_card_hash"] == ""
        assert n["projection_card_source_bg_id"] == n["bg_id"]
        assert n["projection_card_inherited"] is False
        assert n["camera_decision"] == {"camera_unit": 1, "look_at_unit": 1}
    assert summary["projection_card_state_counts"]["not_available"] == 2


def test_bg_with_no_card_target_is_not_applicable():
    """card_index present but this bg has no card for any of its shots →
    not_applicable (the bg is not a projection target, distinct from the
    subsystem being unavailable)."""
    nodes = [_node("A", 0)]
    card_index = {"OTHER": {"S9_Shot9": _card("pass", "x")}}
    out, _ = enrich_nodes_with_projection_cards(
        nodes=nodes,
        card_index=card_index,
        per_bg_readiness=_readiness(A=["S1_Shot1"]),
    )
    n = out[0]
    assert n["projection_card_state"] == "not_applicable"
    assert n["anchor_shot_id"] == ""
    assert n["projection_card_source_bg_id"] == "A"
    assert n["projection_card_inherited"] is False


def test_reuse_inherits_passing_target_provenance():
    """reuse_existing_plate child inherits the render target's resolved
    projection fields: state/id/hash/anchor from target, source=target,
    inherited=true. image-0 reuse keeps provenance pointing at the pixels'
    origin (§8 decision-lock)."""
    nodes = [
        _node("A", 0),
        _node("B", 1, render_action="reuse_existing_plate", reuse_target="A"),
    ]
    card_index = {"A": {"S1_Shot1": _card("pass", "cardA")}}
    out, _ = enrich_nodes_with_projection_cards(
        nodes=nodes,
        card_index=card_index,
        per_bg_readiness=_readiness(A=["S1_Shot1"], B=["S1_Shot2"]),
    )
    child = out[1]
    assert child["projection_card_inherited"] is True
    assert child["projection_card_source_bg_id"] == "A"
    assert child["projection_card_state"] == "pass"
    assert child["projection_card_id"] == "cardA"
    assert child["projection_card_hash"] == "cardA"
    assert child["anchor_shot_id"] == "S1_Shot1"  # target's anchor shot


def test_reuse_does_not_forge_pass_when_target_blocked():
    """FAIL-CLOSED: a reuse child of a BLOCKED target inherits blocked, not
    a forged pass (Codex C3 consensus #2)."""
    nodes = [
        _node("A", 0),
        _node("B", 1, render_action="reuse_existing_plate", reuse_target="A"),
    ]
    card_index = {"A": {"S1_Shot1": _card("blocked", "cardA", "contradiction")}}
    out, _ = enrich_nodes_with_projection_cards(
        nodes=nodes,
        card_index=card_index,
        per_bg_readiness=_readiness(A=["S1_Shot1"], B=["S1_Shot2"]),
    )
    child = out[1]
    assert child["projection_card_inherited"] is True
    assert child["projection_card_source_bg_id"] == "A"
    assert child["projection_card_state"] == "blocked"
    assert child["projection_card_fallback_reason"] == "contradiction"


def test_reuse_inherits_needs_review_target():
    nodes = [
        _node("A", 0),
        _node("B", 1, render_action="reuse_existing_plate", reuse_target="A"),
    ]
    card_index = {"A": {"S1_Shot1": _card("needs_review", "cardA", "low_confidence")}}
    out, _ = enrich_nodes_with_projection_cards(
        nodes=nodes,
        card_index=card_index,
        per_bg_readiness=_readiness(A=["S1_Shot1"], B=["S1_Shot2"]),
    )
    child = out[1]
    assert child["projection_card_state"] == "needs_review"
    assert child["projection_card_inherited"] is True


def test_reuse_with_missing_target_projection_is_not_available_not_pass():
    """reuse target that exists but carries NO usable card (its own state is
    not_applicable) → child not_available + reuse_target_projection_missing,
    NEVER a silently-inherited not_applicable or a false pass (Codex C3
    review Required 2)."""
    nodes = [
        _node("A", 0),
        _node("B", 1, render_action="reuse_existing_plate", reuse_target="A"),
    ]
    # A is a projection target by readiness but has no card entry → A resolves
    # to not_applicable; B must fail-close to not_available, not inherit it.
    card_index = {"OTHER": {"S9_Shot9": _card("pass", "x")}}
    out, _ = enrich_nodes_with_projection_cards(
        nodes=nodes,
        card_index=card_index,
        per_bg_readiness=_readiness(A=["S1_Shot1"], B=["S1_Shot2"]),
    )
    child = out[1]
    assert child["projection_card_state"] == "not_available"
    assert child["projection_card_fallback_reason"] == "reuse_target_projection_missing"
    assert child["projection_card_inherited"] is True
    assert child["projection_card_source_bg_id"] == "A"


def test_pass_card_with_empty_id_fails_closed_to_blocked():
    """FAIL-CLOSED (Codex C3 review Required 1): a card reported pass but
    carrying no content-addressed id cannot be cross-checked downstream
    (C4 uses id == hash == card_id) → blocked + missing_projection_card_id,
    never a false pass."""
    nodes = [_node("A", 0)]
    card_index = {"A": {"S1_Shot1": _card("pass", "")}}
    out, _ = enrich_nodes_with_projection_cards(
        nodes=nodes,
        card_index=card_index,
        per_bg_readiness=_readiness(A=["S1_Shot1"]),
    )
    n = out[0]
    assert n["projection_card_state"] == "blocked"
    assert n["projection_card_fallback_reason"] == "missing_projection_card_id"
    assert n["projection_card_id"] == ""


def test_needs_review_card_with_empty_id_fails_closed_to_blocked():
    nodes = [_node("A", 0)]
    card_index = {"A": {"S1_Shot1": _card("needs_review", "", "low_confidence")}}
    out, _ = enrich_nodes_with_projection_cards(
        nodes=nodes,
        card_index=card_index,
        per_bg_readiness=_readiness(A=["S1_Shot1"]),
    )
    n = out[0]
    assert n["projection_card_state"] == "blocked"
    assert n["projection_card_fallback_reason"] == "missing_projection_card_id"


def test_unknown_card_state_fails_closed_with_invalid_fallback():
    """A card carrying an unrecognized / error state fails closed to blocked
    with a non-empty reason (never a silently-blank fallback)."""
    nodes = [_node("A", 0)]
    card_index = {"A": {"S1_Shot1": {"card_state": "weird", "card_id": "c1", "fallback_reason": ""}}}
    out, _ = enrich_nodes_with_projection_cards(
        nodes=nodes,
        card_index=card_index,
        per_bg_readiness=_readiness(A=["S1_Shot1"]),
    )
    n = out[0]
    assert n["projection_card_state"] == "blocked"
    assert n["projection_card_fallback_reason"] == "projection_card_invalid"


def test_reuse_target_absent_from_nodes_is_not_available():
    """reuse_target_bg_id points at a bg not present among the resolved
    nodes → child not_available + reuse_target_missing fallback."""
    nodes = [
        _node("B", 0, render_action="reuse_existing_plate", reuse_target="GHOST"),
    ]
    card_index = {"B": {"S1_Shot2": _card("pass", "cardB")}}
    out, _ = enrich_nodes_with_projection_cards(
        nodes=nodes,
        card_index=card_index,
        per_bg_readiness=_readiness(B=["S1_Shot2"]),
    )
    child = out[0]
    assert child["projection_card_state"] == "not_available"
    assert child["projection_card_fallback_reason"] == "reuse_target_missing"
    assert child["projection_card_inherited"] is True
    assert child["projection_card_source_bg_id"] == "GHOST"
