"""W21B Phase 2 (2026-06-08): dwelling zone-map → zone-1-plate application.

The ``dwelling_zone_map`` step groups same-dwelling bgs (across camera angles)
the edge-judge could not. Phase 2 turns that grouping into the render surface:
EXACTLY ONE plate per zone (the anchor renders new; every same-zone non-anchor
bg ALIASES the anchor plate via ``reuse_existing_plate``). The non-anchor is a
reference / alias, NOT a derive — so it carries NO ``ref_tree_parents`` (the
derive signal). User hard rule: one physical space = one background plate;
per-shot angle / character variety is Phase 3 (scene_image i2i referencing that
one plate).

These tests pin the pure helpers (no DB / LLM / image): the usability gate,
anchor selection, and the full application — plus the byte-identical guard that
the legacy ``apply_space_partition_plan`` path is untouched when no zone plan
applies.
"""
from __future__ import annotations

from app.modules.pipeline.shot_aware_bg_render_plan import (
    MAX_REFS_PER_BG,
    RENDER_ACTION_NEW,
    RENDER_ACTION_REUSE,
    RENDER_ACTION_SOURCE_GEOMETRY,
    RENDER_ACTION_SOURCE_ZONE_MAP,
    _zone_plan_is_usable,
    apply_dwelling_zone_map_plan,
    select_zone_anchor,
)


def _node(bg_id, node_index, *, render_action=RENDER_ACTION_NEW, reuse_target="",
          is_anchor=False):
    return {
        "bg_id": bg_id,
        "node_index": node_index,
        "is_dwelling_identity_anchor": is_anchor,
        "render_action": render_action,
        "reuse_target_bg_id": reuse_target,
        "needs_new_plate": render_action == RENDER_ACTION_NEW,
        "ref_tree_parents": [],
        "ref_role_per_parent": {},
        "render_order_index": node_index,
        "max_refs": MAX_REFS_PER_BG,
        "plate_group_id": bg_id,
        "plate_anchor_bg_id": bg_id,
        "plate_shareability": "exclusive",
    }


def _zone_map(bg_zone, *, synthetic=False, confidences=None):
    """Minimal zone_map contract slice — only the fields the helpers read."""
    confidences = confidences or {}
    return {
        "fp_id": "fp_x",
        "synthetic": synthetic,
        "bg_zone_assignments": {
            bg: {
                "zone_id": zid,
                "shot_ids": [],
                "grid_focus": None if zid is None else {"x": 1, "y": 1},
                "confidence": confidences.get(bg),
                "assignment_state": "fallback" if zid is None else "ok",
            }
            for bg, zid in bg_zone.items()
        },
    }


# ───────────────────────── usability gate ─────────────────────────


def test_none_zone_plan_not_usable():
    ok, reason = _zone_plan_is_usable(None, frozenset({"A", "B"}))
    assert ok is False
    assert reason == "zone_plan_missing"


def test_synthetic_zone_plan_not_usable():
    # The synthetic fixture asserts NOTHING — it must never drive the surface.
    zp = _zone_map({"A": "Z01", "B": "Z02"}, synthetic=True)
    ok, reason = _zone_plan_is_usable(zp, frozenset({"A", "B"}))
    assert ok is False
    assert reason == "zone_plan_synthetic"


def test_single_zone_not_usable():
    # All node bgs in one zone → nothing to consolidate; legacy path (byte-id).
    zp = _zone_map({"A": "Z01", "B": "Z01"})
    ok, reason = _zone_plan_is_usable(zp, frozenset({"A", "B"}))
    assert ok is False
    assert reason == "zone_plan_single_zone"


def test_uncovered_bg_not_usable():
    # A node bg with no zone assignment → fail-closed (no half-consume).
    zp = _zone_map({"A": "Z01", "B": "Z02"})
    ok, reason = _zone_plan_is_usable(zp, frozenset({"A", "B", "C"}))
    assert ok is False
    assert reason.startswith("zone_plan_uncovered_bgs:")
    assert "C" in reason


def test_null_zone_id_not_usable():
    # A covered bg whose zone_id is None (VLM fallback) → fail-closed.
    zp = _zone_map({"A": "Z01", "B": None})
    ok, reason = _zone_plan_is_usable(zp, frozenset({"A", "B"}))
    assert ok is False
    assert reason.startswith("zone_plan_null_zone")
    assert "B" in reason


def test_valid_multizone_usable():
    zp = _zone_map({"A": "Z01", "B": "Z02"})
    ok, reason = _zone_plan_is_usable(zp, frozenset({"A", "B"}))
    assert ok is True
    assert reason == ""


def test_usable_ignores_non_node_bgs():
    # zone_map may carry bgs that are not renderable nodes (no staged shot).
    # Usability is judged only over the node bgs actually present.
    zp = _zone_map({"A": "Z01", "B": "Z02", "C": "Z02"})
    ok, reason = _zone_plan_is_usable(zp, frozenset({"A", "B"}))
    assert ok is True
    assert reason == ""


# ───────────────────────── anchor selection ─────────────────────────


def _assign(confidences):
    return {bg: {"confidence": c} for bg, c in (confidences or {}).items()}


def test_select_anchor_prefers_clean_candidate():
    # The dossier's clean-background candidate wins over a lower node_index /
    # higher confidence non-clean bg — the anchor plate must be clean.
    members = [_node("A", 0), _node("B", 1)]
    diags: list = []
    anchor = select_zone_anchor(
        members=members, zone_id="Z01",
        clean_anchor_candidate_bg_ids=frozenset({"B"}),
        bg_zone_assignments=_assign({"A": 0.9, "B": 0.5}),
        diagnostics=diags,
    )
    assert anchor == "B"
    assert diags == []


def test_select_anchor_tiebreak_lower_node_index_within_clean():
    members = [_node("B", 1), _node("A", 0)]
    diags: list = []
    anchor = select_zone_anchor(
        members=members, zone_id="Z01",
        clean_anchor_candidate_bg_ids=frozenset({"A", "B"}),
        bg_zone_assignments=_assign({"A": 0.5, "B": 0.5}),
        diagnostics=diags,
    )
    assert anchor == "A"
    assert diags == []


def test_select_anchor_prefers_new_over_reuse():
    # A node already routed to a fresh plate (NEW) is a better anchor than one
    # routed to reuse another plate — even with a lower idx / higher conf.
    members = [
        _node("A", 0, render_action=RENDER_ACTION_REUSE, reuse_target="X"),
        _node("B", 1),
    ]
    diags: list = []
    anchor = select_zone_anchor(
        members=members, zone_id="Z01",
        clean_anchor_candidate_bg_ids=frozenset({"A", "B"}),
        bg_zone_assignments=_assign({"A": 0.9, "B": 0.5}),
        diagnostics=diags,
    )
    assert anchor == "B"


def test_select_anchor_confidence_breaks_ties():
    members = [_node("A", 5), _node("B", 5)]
    diags: list = []
    anchor = select_zone_anchor(
        members=members, zone_id="Z01",
        clean_anchor_candidate_bg_ids=frozenset({"A", "B"}),
        bg_zone_assignments=_assign({"A": 0.6, "B": 0.9}),
        diagnostics=diags,
    )
    assert anchor == "B"


def test_select_anchor_no_clean_candidate_falls_back_with_diagnostic():
    # No clean candidate in the zone: still pick a stable anchor (lower idx) but
    # surface the diagnostic to the visual gate (never hide it like synthetic).
    members = [_node("A", 0), _node("B", 1)]
    diags: list = []
    anchor = select_zone_anchor(
        members=members, zone_id="Z02",
        clean_anchor_candidate_bg_ids=frozenset(),
        bg_zone_assignments=_assign({"A": 0.5, "B": 0.5}),
        diagnostics=diags,
    )
    assert anchor == "A"
    assert diags == ["zone_anchor_no_clean_candidate:Z02"]


# ───────────────────── apply: one plate per zone ─────────────────────


def test_zone_apply_one_plate_per_zone_rest_alias():
    # L05-shaped: 6 bgs, 2 zones (living Z01 = B01/B03/B04, bedroom Z02 =
    # B02/B05/B06). EXACTLY ONE plate renders per zone; the rest alias it.
    nodes = [
        _node("B01", 0, is_anchor=True),
        _node("B02", 1),
        _node("B03", 2),
        _node("B04", 3),
        _node("B05", 4),
        _node("B06", 5),
    ]
    zp = _zone_map(
        {"B01": "Z01", "B03": "Z01", "B04": "Z01",
         "B02": "Z02", "B05": "Z02", "B06": "Z02"},
        confidences={"B01": 0.9, "B03": 0.8, "B04": 0.8,
                     "B02": 0.9, "B05": 0.8, "B06": 0.8},
    )
    out, summary = apply_dwelling_zone_map_plan(
        nodes, zp, clean_anchor_candidate_bg_ids=frozenset({"B01", "B02"}))
    assert summary["space_partition_applied"] is True
    by = {n["bg_id"]: n for n in out}

    # exactly two fresh plates (one per zone) — the user's hard rule.
    assert sum(1 for n in out if n["needs_new_plate"]) == 2
    assert summary["dag_node_count"] == 2
    assert summary["plate_group_count"] == 2
    assert summary["reuse_plate_count"] == 4

    # zone anchors render new.
    for a in ("B01", "B02"):
        assert by[a]["render_action"] == RENDER_ACTION_NEW
        assert by[a]["needs_new_plate"] is True
        assert by[a]["reuse_target_bg_id"] == ""

    # non-anchors ALIAS their zone anchor — reuse, and critically NO
    # ref_tree_parents (that is a derive signal; this is a pure reference).
    for bg, anchor, zone in [("B03", "B01", "Z01"), ("B04", "B01", "Z01"),
                             ("B05", "B02", "Z02"), ("B06", "B02", "Z02")]:
        n = by[bg]
        assert n["render_action"] == RENDER_ACTION_REUSE
        assert n["reuse_target_bg_id"] == anchor
        assert n["needs_new_plate"] is False
        assert n["ref_tree_parents"] == []
        assert n["ref_role_per_parent"] == {}
        assert n["plate_group_id"] == zone
        assert n["plate_anchor_bg_id"] == anchor
        # Phase 3 contract: bg → its zone's single plate.
        assert n["zone_plate_bg_id"] == anchor
        assert n["zone_id"] == zone

    # every node stamped with the zone-map source + cleared fallback reason.
    for n in out:
        assert n["render_action_source"] == RENDER_ACTION_SOURCE_ZONE_MAP
        assert n["partition_fallback_reason"] == ""
        assert n["plate_shareability"] == "shareable_partition"


def test_zone_apply_single_member_zone_is_own_anchor():
    nodes = [_node("A", 0), _node("B", 1), _node("C", 2)]
    zp = _zone_map({"A": "Z01", "B": "Z02", "C": "Z02"})
    out, summary = apply_dwelling_zone_map_plan(
        nodes, zp, clean_anchor_candidate_bg_ids=frozenset({"A", "B"}))
    by = {n["bg_id"]: n for n in out}
    # A alone in Z01 → its own anchor plate (exclusive).
    assert by["A"]["render_action"] == RENDER_ACTION_NEW
    assert by["A"]["needs_new_plate"] is True
    assert by["A"]["plate_shareability"] == "exclusive"
    assert by["A"]["zone_plate_bg_id"] == "A"
    # B anchor, C alias in Z02.
    assert by["B"]["render_action"] == RENDER_ACTION_NEW
    assert by["C"]["render_action"] == RENDER_ACTION_REUSE
    assert by["C"]["reuse_target_bg_id"] == "B"
    assert by["B"]["plate_shareability"] == "shareable_partition"
    assert summary["dag_node_count"] == 2
    assert summary["plate_group_count"] == 2
    assert summary["reuse_plate_count"] == 1


def test_zone_apply_noop_when_no_plan():
    nodes = [_node("A", 0), _node("B", 1)]
    out, summary = apply_dwelling_zone_map_plan(
        nodes, None, clean_anchor_candidate_bg_ids=frozenset())
    assert summary["space_partition_applied"] is False
    assert summary["space_partition_fallback_reason"] == "zone_plan_missing"
    for n in out:
        assert n["render_action_source"] == RENDER_ACTION_SOURCE_GEOMETRY


def test_zone_apply_surfaces_no_clean_candidate_diagnostic():
    nodes = [_node("A", 0), _node("B", 1)]
    zp = _zone_map({"A": "Z01", "B": "Z02"})
    _, summary = apply_dwelling_zone_map_plan(
        nodes, zp, clean_anchor_candidate_bg_ids=frozenset())
    # no clean candidate in either zone → both surfaced, never hidden.
    assert sorted(summary["zone_anchor_diagnostics"]) == [
        "zone_anchor_no_clean_candidate:Z01",
        "zone_anchor_no_clean_candidate:Z02",
    ]
