"""W21B-w5 STEP4 (A wiring): apply_space_partition_plan canonical normalizer.

The bg_space_partition step's space_partition_plan is the LLM SOT for cross-zone
reuse. ``apply_space_partition_plan`` is the FINAL authority over the canonical
render surface — when a usable plan is present it OVERRIDES every downstream-read
field from the partition SOT and demotes the wave4 geometry route / mirror / DAG
output to ``geometry_*_diagnostic`` (Codex A2 + 5-condition lock). An unusable /
uncovered plan is a fail-closed NO-OP. These tests pin all 5 conditions, with the
two geometry↔partition disagreement directions as the core cases.
"""
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_PARTITION,
    apply_space_partition_plan,
)


def _node(bg_id, node_index, *, render_action=RENDER_ACTION_NEW, reuse_target="",
          ref_parents=None, needs_new=None, group=None, shareability="exclusive"):
    if needs_new is None:
        needs_new = render_action == RENDER_ACTION_NEW
    parents = list(ref_parents or [])
    return {
        "bg_id": bg_id,
        "node_index": node_index,
        "render_action": render_action,
        "reuse_target_bg_id": reuse_target,
        "needs_new_plate": needs_new,
        "ref_tree_parents": parents,
        "ref_role_per_parent": {p: "style" for p in parents},
        "render_order_index": node_index,
        "max_refs": MAX_REFS_PER_BG,
        "plate_group_id": group or bg_id,
        "plate_anchor_bg_id": group or bg_id,
        "plate_shareability": shareability,
    }


def _plan(render_actions, ref_parents, node_assignments, plate_groups):
    return {
        "render_actions": render_actions,
        "ref_tree_parents": ref_parents,
        "node_assignments": node_assignments,
        "plate_groups": plate_groups,
    }


# ───────────────────────── fail-closed (no-op) ─────────────────────────


def test_none_plan_is_noop_legacy_unchanged():
    nodes = [_node("A", 0), _node("B", 1, render_action=RENDER_ACTION_REUSE,
                                  reuse_target="A", ref_parents=[])]
    out, summary = apply_space_partition_plan(nodes, None)
    assert summary["space_partition_applied"] is False
    assert summary["space_partition_fallback_reason"] == "partition_plan_missing"
    # legacy canonical untouched
    assert out[1]["render_action"] == RENDER_ACTION_REUSE
    assert out[1]["reuse_target_bg_id"] == "A"
    for n in out:
        assert n["render_action_source"] == RENDER_ACTION_SOURCE_GEOMETRY
        assert n["partition_fallback_reason"] == "partition_plan_missing"
        assert "geometry_render_action_diagnostic" not in n  # no diag on no-op


def test_plan_without_render_actions_is_noop():
    nodes = [_node("A", 0)]
    out, summary = apply_space_partition_plan(nodes, {"render_actions": {}})
    assert summary["space_partition_applied"] is False
    assert summary["space_partition_fallback_reason"] == "partition_plan_no_render_actions"


def test_plan_missing_node_assignments_is_noop():
    # render_actions covers the bg but node_assignments is absent → fail-closed,
    # NOT a silent gid=bg default (Codex review hardening).
    nodes = [_node("A", 0)]
    plan = {
        "render_actions": {"A": {"render_action": RENDER_ACTION_NEW,
                                 "reuse_target_bg_id": ""}},
        "plate_groups": [{"plate_group_id": "grp::A", "anchor_bg_id": "A",
                          "member_bg_ids": []}],
    }
    _, summary = apply_space_partition_plan(nodes, plan)
    assert summary["space_partition_applied"] is False
    assert summary["space_partition_fallback_reason"] == (
        "partition_plan_no_node_assignments")


def test_plan_missing_group_anchor_is_noop():
    # node_assignments references a group with no plate_groups anchor entry →
    # fail-closed rather than anchor=bg default.
    nodes = [_node("A", 0)]
    plan = {
        "render_actions": {"A": {"render_action": RENDER_ACTION_NEW,
                                 "reuse_target_bg_id": ""}},
        "node_assignments": {"A": "grp::A"},
        "ref_tree_parents": {"A": []},
        "plate_groups": [],  # no anchor for grp::A
    }
    _, summary = apply_space_partition_plan(nodes, plan)
    assert summary["space_partition_applied"] is False
    assert summary["space_partition_fallback_reason"].startswith(
        "partition_plan_missing_group_anchor:")


def test_uncovered_bg_is_fp_level_noop():
    # plan covers A but not B → no half-consume, whole fp falls back.
    nodes = [_node("A", 0), _node("B", 1)]
    plan = _plan(
        {"A": {"render_action": RENDER_ACTION_NEW, "reuse_target_bg_id": ""}},
        {"A": []}, {"A": "grp::A"},
        [{"plate_group_id": "grp::A", "anchor_bg_id": "A", "member_bg_ids": []}],
    )
    out, summary = apply_space_partition_plan(nodes, plan)
    assert summary["space_partition_applied"] is False
    assert summary["space_partition_fallback_reason"].startswith(
        "partition_plan_uncovered_bgs:")
    assert out[0]["render_action_source"] == RENDER_ACTION_SOURCE_GEOMETRY


# ────────── core: geometry ↔ partition disagreement, partition wins ──────────


def test_geometry_reuse_partition_breaks_to_new():
    # (a) geometry: B reuses A. partition: A,B are DIFFERENT spaces → B own plate.
    # This is the bathroom-vs-living break the whole wave fixes.
    nodes = [
        _node("A", 0),
        _node("B", 1, render_action=RENDER_ACTION_REUSE, reuse_target="A",
              ref_parents=[], group="A"),
    ]
    plan = _plan(
        {"A": {"render_action": RENDER_ACTION_NEW, "reuse_target_bg_id": ""},
         "B": {"render_action": RENDER_ACTION_NEW, "reuse_target_bg_id": ""}},
        {"A": [], "B": []},
        {"A": "grp::A", "B": "grp::B"},
        [{"plate_group_id": "grp::A", "anchor_bg_id": "A", "member_bg_ids": []},
         {"plate_group_id": "grp::B", "anchor_bg_id": "B", "member_bg_ids": []}],
    )
    out, summary = apply_space_partition_plan(nodes, plan)
    assert summary["space_partition_applied"] is True
    b = out[1]
    assert b["render_action"] == RENDER_ACTION_NEW  # partition won
    assert b["reuse_target_bg_id"] == ""
    assert b["needs_new_plate"] is True
    assert b["plate_group_id"] == "grp::B"
    assert b["render_action_source"] == RENDER_ACTION_SOURCE_PARTITION
    # geometry value preserved as diagnostic only
    assert b["geometry_render_action_diagnostic"] == RENDER_ACTION_REUSE
    assert b["geometry_reuse_target_bg_id_diagnostic"] == "A"
    # both are now solo fresh plates → 2 groups, 0 reuse
    assert summary["reuse_plate_count"] == 0
    assert summary["plate_group_count"] == 2
    assert summary["dag_node_count"] == 2


def test_same_space_different_angle_member_derives_not_reuse():
    # (b') same physical space, but shot_aware (camera-aware) decided NEW because the
    # framing differs (different camera_unit/look_at). partition must NOT promote
    # NEW→reuse; it keeps NEW and grants anchor lineage →
    # "same space, different angle = derive". This replaces the stale
    # test_geometry_new_partition_groups_to_reuse which pinned the false promote.
    nodes = [_node("A", 0), _node("B", 1, render_action=RENDER_ACTION_NEW)]
    plan = _plan(
        # partition plate-action WOULD promote B to reuse (same physical space) ...
        {"A": {"render_action": RENDER_ACTION_NEW, "reuse_target_bg_id": ""},
         "B": {"render_action": RENDER_ACTION_REUSE, "reuse_target_bg_id": "A"}},
        {"A": [], "B": ["A"]},
        {"A": "grp::A", "B": "grp::A"},
        [{"plate_group_id": "grp::A", "anchor_bg_id": "A", "member_bg_ids": ["B"]}],
    )
    out, summary = apply_space_partition_plan(nodes, plan)
    assert summary["space_partition_applied"] is True
    a, b = out[0], out[1]
    # ... but shot_aware render_action is the SOT: B stays a fresh plate (derive).
    assert b["render_action"] == RENDER_ACTION_NEW
    assert b["reuse_target_bg_id"] == ""
    assert b["needs_new_plate"] is True
    assert b["geometry_render_action_diagnostic"] == RENDER_ACTION_NEW
    # anchor lineage still applied by partition → derive, not independent
    assert b["ref_tree_parents"] == ["A"]
    assert b["ref_role_per_parent"] == {"A": "space_continuity"}
    assert b["plate_group_id"] == "grp::A"
    assert b["plate_anchor_bg_id"] == "A"
    assert a["plate_shareability"] == "shareable_partition"
    assert b["plate_shareability"] == "shareable_partition"
    # B renders its own plate now → it IS a fresh DAG node with an order index
    assert b["render_order_index"] is not None
    assert summary["reuse_plate_count"] == 0
    assert summary["plate_group_count"] == 1
    assert summary["dag_node_count"] == 2  # A and B both fresh


def test_same_space_same_framing_shot_aware_reuse_preserved():
    # shot_aware (camera-aware) already corroborated the SAME framing → reuse. The
    # partition plate-action happens to say NEW, but shot_aware render_action is the
    # SOT, so reuse is preserved (partition never demotes a same-group reuse).
    nodes = [_node("A", 0),
             _node("B", 1, render_action=RENDER_ACTION_REUSE, reuse_target="A")]
    plan = _plan(
        {"A": {"render_action": RENDER_ACTION_NEW, "reuse_target_bg_id": ""},
         "B": {"render_action": RENDER_ACTION_NEW, "reuse_target_bg_id": ""}},
        {"A": [], "B": ["A"]},
        {"A": "grp::A", "B": "grp::A"},
        [{"plate_group_id": "grp::A", "anchor_bg_id": "A", "member_bg_ids": ["B"]}],
    )
    out, _ = apply_space_partition_plan(nodes, plan)
    b = out[1]
    assert b["render_action"] == RENDER_ACTION_REUSE
    assert b["reuse_target_bg_id"] == "A"
    assert b["needs_new_plate"] is False


def test_cross_group_shot_aware_reuse_vetoed_to_new():
    # shot_aware reuse target lands in a DIFFERENT partition group (cross-zone false
    # reuse, e.g. bathroom→living): partition vetoes it back to a fresh own plate.
    # This is the cross-zone protection bg_space_partition was introduced for.
    nodes = [_node("A", 0),
             _node("B", 1, render_action=RENDER_ACTION_REUSE, reuse_target="A")]
    plan = _plan(
        {"A": {"render_action": RENDER_ACTION_NEW, "reuse_target_bg_id": ""},
         "B": {"render_action": RENDER_ACTION_NEW, "reuse_target_bg_id": ""}},
        {"A": [], "B": []},
        {"A": "grp::A", "B": "grp::B"},
        [{"plate_group_id": "grp::A", "anchor_bg_id": "A", "member_bg_ids": []},
         {"plate_group_id": "grp::B", "anchor_bg_id": "B", "member_bg_ids": []}],
    )
    out, _ = apply_space_partition_plan(nodes, plan)
    b = out[1]
    assert b["render_action"] == RENDER_ACTION_NEW
    assert b["reuse_target_bg_id"] == ""
    assert b["needs_new_plate"] is True
    # the shot_aware reuse intent is preserved as a diagnostic
    assert b["geometry_render_action_diagnostic"] == RENDER_ACTION_REUSE


# ──────────────── full-surface override + diagnostics ────────────────


def test_full_canonical_surface_overridden_and_diagnostics_kept():
    nodes = [_node("A", 0), _node("B", 1)]
    plan = _plan(
        {"A": {"render_action": RENDER_ACTION_NEW, "reuse_target_bg_id": ""},
         "B": {"render_action": RENDER_ACTION_REUSE, "reuse_target_bg_id": "A"}},
        {"A": [], "B": ["A"]},
        {"A": "grp::A", "B": "grp::A"},
        [{"plate_group_id": "grp::A", "anchor_bg_id": "A", "member_bg_ids": ["B"]}],
    )
    out, _ = apply_space_partition_plan(nodes, plan)
    for n in out:
        # every canonical field has a geometry_*_diagnostic twin
        for f in ("render_action", "reuse_target_bg_id", "needs_new_plate",
                  "ref_tree_parents", "ref_role_per_parent", "render_order_index",
                  "max_refs", "plate_group_id", "plate_anchor_bg_id",
                  "plate_shareability"):
            assert f"geometry_{f}_diagnostic" in n
        assert n["max_refs"] == MAX_REFS_PER_BG
        assert n["render_action_source"] == RENDER_ACTION_SOURCE_PARTITION


def test_ref_tree_capped_at_max_refs():
    # partition hands 3 parents; normalizer caps at 2.
    nodes = [_node("A", 0), _node("B", 1), _node("C", 2), _node("D", 3)]
    plan = _plan(
        {bg: {"render_action": RENDER_ACTION_NEW, "reuse_target_bg_id": ""}
         for bg in ("A", "B", "C", "D")},
        {"A": [], "B": [], "C": [], "D": ["A", "B", "C"]},
        {"A": "grp::A", "B": "grp::B", "C": "grp::C", "D": "grp::D"},
        [{"plate_group_id": f"grp::{bg}", "anchor_bg_id": bg, "member_bg_ids": []}
         for bg in ("A", "B", "C", "D")],
    )
    out, _ = apply_space_partition_plan(nodes, plan)
    d = out[3]
    assert len(d["ref_tree_parents"]) == MAX_REFS_PER_BG
    assert d["ref_tree_parents"] == ["A", "B"]
    assert all(r == "space_continuity" for r in d["ref_role_per_parent"].values())
