"""W16 grid-layout slice structural tests — synthetic generic fixtures only.

No scenario-specific tokens (rooms/props/colors/narrative literals) in test
methodology. The structural methodology grep guard scans script source for
fixture-specific literals but is used STRUCTURALLY, never to judge meaning.
"""
from __future__ import annotations

import json
import re
import sys
from pathlib import Path

import pytest


_REPO_ROOT = Path(__file__).resolve().parents[3]
_SCRIPTS_DIR = _REPO_ROOT / "backend" / "scripts"
if str(_SCRIPTS_DIR) not in sys.path:
    sys.path.insert(0, str(_SCRIPTS_DIR))


# ─────────────────────────────────────────────────────────────────────────────
# Synthetic generic fixture — mirrors W15 source_topology_brief.json shape but
# uses opaque ids (`FPx`, `Ux`, ...) so the test methodology stays free of
# scenario tokens. No bedroom/kitchen/curtain/etc. literals in this file.
# ─────────────────────────────────────────────────────────────────────────────

def _synthetic_topology_brief() -> dict:
    return {
        "target_fp_ids": ["FPx"],
        "source_topology_by_fp": {
            "FPx": {
                "fp_id": "FPx",
                "spatial_units": [
                    {"unit_id": "U1", "unit_label": "zone one",
                     "unit_kind": "living_zone", "is_enclosed_room": False,
                     "evidence_refs": [
                         {"source_ref": "scene:1", "quote": "q1", "field": "text"}
                     ]},
                    {"unit_id": "U2", "unit_label": "zone two",
                     "unit_kind": "kitchen_zone", "is_enclosed_room": False,
                     "evidence_refs": []},
                    {"unit_id": "U3", "unit_label": "room alpha",
                     "unit_kind": "private_room", "is_enclosed_room": True,
                     "evidence_refs": []},
                ],
                "relationships": [
                    {"from_unit": "U1", "to_unit": "U2",
                     "relation_kind": "open_connection"},
                    {"from_unit": "U1", "to_unit": "U3",
                     "relation_kind": "door_between"},
                ],
                "room_count_assessment": {
                    "standalone_enclosed_room_count": 1,
                    "rationale": "one enclosed",
                },
                "do_not_collapse_units": [],
                "conflicts_and_assumptions": [],
            },
        },
    }


def _synthetic_candidate() -> dict:
    return {
        "candidate_floor_plans": {
            "FPx": {
                "fp_id": "FPx", "group_id_pointer": "Gx",
                "candidate_diagram_t2i_prompt": "schematic alpha.",
                "candidate_key_elements": [],
                "candidate_numbered_elements": [
                    {"number": 1, "label": "alpha", "category": "area",
                     "position_hint": "p1", "unit_id_pointer": "U1"},
                    {"number": 2, "label": "beta", "category": "area",
                     "position_hint": "p2", "unit_id_pointer": "U2"},
                    {"number": 3, "label": "gamma", "category": "area",
                     "position_hint": "p3", "unit_id_pointer": "U3"},
                    {"number": 7, "label": "side opening",
                     "category": "opening",
                     "position_hint": "north wall",
                     "unit_id_pointer": "U3"},
                    {"number": 11, "label": "interior fixture",
                     "category": "furniture",
                     "position_hint": "perimeter",
                     "unit_id_pointer": "U3"},
                ],
                "candidate_camera_recommendations": [],
                "reconciliation_notes_vs_production": "",
            },
        },
    }


def _synthetic_llm_grid_layout() -> dict:
    # Baseline must satisfy every W16 + W16d invariant:
    # - U1 primary_zone 36 cells in [25, 40]
    # - U2 compact_nook_or_wall_run 4 cells (one-row 4x1)
    # - U3 secondary_zone 16 cells in [8, 18]
    # - total unique cells = 56 in [55, 75]
    # - U1 and U2 share a row edge (open_connection)
    # - U1 and U3 share a column edge with row overlap (door_between)
    return {
        "grid_layout_by_fp": {
            "FPx": {
                "grid": {"cols": 10, "rows": 10},
                "outer_outline": [{"x": 0, "y": 0, "w": 10, "h": 10}],
                "units": [
                    {"unit_id": "U1", "unit_kind": "living_zone",
                     "enclosure_mode": "open_zone_inside_plan",
                     "rects": [{"x": 0, "y": 0, "w": 6, "h": 6}],
                     "open_to": ["U2"], "door_to": ["U3"],
                     "scale_hint": "primary_zone",
                     "rationale_refs": ["scene:1"]},
                    {"unit_id": "U2", "unit_kind": "kitchen_zone",
                     "enclosure_mode": "open_zone_inside_plan",
                     "rects": [{"x": 0, "y": 6, "w": 4, "h": 1}],
                     "open_to": ["U1"], "door_to": [],
                     "scale_hint": "compact_nook_or_wall_run",
                     "rationale_refs": []},
                    {"unit_id": "U3", "unit_kind": "private_room",
                     "enclosure_mode": "full_wall_enclosed_room",
                     "rects": [{"x": 6, "y": 0, "w": 4, "h": 4}],
                     "open_to": [], "door_to": ["U1"],
                     "scale_hint": "secondary_zone",
                     "rationale_refs": []},
                ],
                "doorways": [
                    {"from_unit": "U1", "to_unit": "U3",
                     "edge_hint": "shared_edge"},
                ],
                "windows": [
                    {"unit_id": "U3", "edge_hint": "east"},
                ],
                "fixtures": [
                    {"number": 1, "unit_id": "U1", "cell": [2, 2],
                     "render_mode": "area_marker"},
                    {"number": 2, "unit_id": "U2", "cell": [1, 6],
                     "render_mode": "area_marker"},
                    {"number": 3, "unit_id": "U3", "cell": [8, 2],
                     "render_mode": "area_marker"},
                    {"number": 7, "unit_id": "U3", "cell": [6, 1],
                     "anchor_edge": "west", "render_mode": "opening_marker"},
                    {"number": 11, "unit_id": "U3", "cell": [8, 3],
                     "render_mode": "tiny_wall_icon"},
                ],
                "adjacency_notes": [],
                "layout_rationale": "compact arrangement, not a row.",
            },
        },
    }


# ─────────────────────────────────────────────────────────────────────────────


def test_w16_grid_bounds_and_unit_id_validation(tmp_path):
    """grid is 10x10; every rect lies inside bounds; every unit_id referenced
    in the LLM output must already exist in the W15 topology spatial_units."""
    from experiment_floor_plan_grid_layout_slice import (
        _build_w16_compatibility_report,
    )
    layout = _synthetic_llm_grid_layout()
    topo = _synthetic_topology_brief()
    cand = _synthetic_candidate()
    rep = _build_w16_compatibility_report(
        grid_layout=layout, topology_brief=topo, candidate=cand,
        target_fp_ids={"FPx"},
        production_diff_empty=True, db_write_count=0,
        image_import_seen=False, image_api_call_count=0,
        svg_emitted=True, model_used="gpt-5.5",
        stage_status="generated", missing_inputs=[],
        prev_run_id="fakeW15e",
    )
    inv = rep["invariants"]
    assert inv["grid_is_10x10_and_all_rects_in_bounds"]["pass"] is True
    assert inv["unit_ids_exist_in_w15_topology"]["pass"] is True

    # Mutate one rect out of bounds → fails.
    bad_layout = json.loads(json.dumps(layout))
    bad_layout["grid_layout_by_fp"]["FPx"]["units"][0]["rects"][0]["w"] = 99
    rep_bad = _build_w16_compatibility_report(
        grid_layout=bad_layout, topology_brief=topo, candidate=cand,
        target_fp_ids={"FPx"},
        production_diff_empty=True, db_write_count=0,
        image_import_seen=False, image_api_call_count=0,
        svg_emitted=True, model_used="gpt-5.5",
        stage_status="generated", missing_inputs=[],
        prev_run_id="fakeW15e",
    )
    assert rep_bad["invariants"]["grid_is_10x10_and_all_rects_in_bounds"]["pass"] is False

    # Mutate a unit_id to one absent from topology → fails.
    bad_unit = json.loads(json.dumps(layout))
    bad_unit["grid_layout_by_fp"]["FPx"]["units"][0]["unit_id"] = "U_UNKNOWN"
    rep_unit = _build_w16_compatibility_report(
        grid_layout=bad_unit, topology_brief=topo, candidate=cand,
        target_fp_ids={"FPx"},
        production_diff_empty=True, db_write_count=0,
        image_import_seen=False, image_api_call_count=0,
        svg_emitted=True, model_used="gpt-5.5",
        stage_status="generated", missing_inputs=[],
        prev_run_id="fakeW15e",
    )
    assert rep_unit["invariants"]["unit_ids_exist_in_w15_topology"]["pass"] is False


def test_w16_fixture_number_and_unit_validation(tmp_path):
    """Every fixture.number must exist in the W15 candidate numbered_elements,
    and fixture.unit_id must agree with the unit_id_pointer for that number."""
    from experiment_floor_plan_grid_layout_slice import (
        _build_w16_compatibility_report,
    )
    layout = _synthetic_llm_grid_layout()
    topo = _synthetic_topology_brief()
    cand = _synthetic_candidate()

    rep = _build_w16_compatibility_report(
        grid_layout=layout, topology_brief=topo, candidate=cand,
        target_fp_ids={"FPx"},
        production_diff_empty=True, db_write_count=0,
        image_import_seen=False, image_api_call_count=0,
        svg_emitted=True, model_used="gpt-5.5",
        stage_status="generated", missing_inputs=[],
        prev_run_id="fakeW15e",
    )
    assert rep["invariants"]["fixtures_reference_candidate_numbers_and_units"]["pass"] is True

    # Unknown fixture number → fails.
    bad_num = json.loads(json.dumps(layout))
    bad_num["grid_layout_by_fp"]["FPx"]["fixtures"].append(
        {"number": 999, "unit_id": "U1", "cell": [1, 1],
         "render_mode": "floor_marker"}
    )
    rep_num = _build_w16_compatibility_report(
        grid_layout=bad_num, topology_brief=topo, candidate=cand,
        target_fp_ids={"FPx"},
        production_diff_empty=True, db_write_count=0,
        image_import_seen=False, image_api_call_count=0,
        svg_emitted=True, model_used="gpt-5.5",
        stage_status="generated", missing_inputs=[],
        prev_run_id="fakeW15e",
    )
    assert rep_num["invariants"]["fixtures_reference_candidate_numbers_and_units"]["pass"] is False

    # Wrong unit binding for a known number → fails.
    bad_bind = json.loads(json.dumps(layout))
    # Move fixture #7 (candidate unit_id_pointer=U3) onto U1 → mismatch.
    for fx in bad_bind["grid_layout_by_fp"]["FPx"]["fixtures"]:
        if fx["number"] == 7:
            fx["unit_id"] = "U1"
    rep_bind = _build_w16_compatibility_report(
        grid_layout=bad_bind, topology_brief=topo, candidate=cand,
        target_fp_ids={"FPx"},
        production_diff_empty=True, db_write_count=0,
        image_import_seen=False, image_api_call_count=0,
        svg_emitted=True, model_used="gpt-5.5",
        stage_status="generated", missing_inputs=[],
        prev_run_id="fakeW15e",
    )
    assert rep_bind["invariants"]["fixtures_reference_candidate_numbers_and_units"]["pass"] is False


def test_w16_adjacency_checker_positive_and_negative(tmp_path):
    """For every declared open_to / door_to edge, the two unit's rect sets
    must either touch (share an edge segment) OR be within 1 grid cell of
    each other (allows door-jamb tolerance). Non-touching, non-adjacent
    declared adjacency must fail."""
    from experiment_floor_plan_grid_layout_slice import (
        _build_w16_compatibility_report,
    )
    layout = _synthetic_llm_grid_layout()
    topo = _synthetic_topology_brief()
    cand = _synthetic_candidate()

    rep = _build_w16_compatibility_report(
        grid_layout=layout, topology_brief=topo, candidate=cand,
        target_fp_ids={"FPx"},
        production_diff_empty=True, db_write_count=0,
        image_import_seen=False, image_api_call_count=0,
        svg_emitted=True, model_used="gpt-5.5",
        stage_status="generated", missing_inputs=[],
        prev_run_id="fakeW15e",
    )
    assert rep["invariants"][
        "declared_open_or_door_adjacency_has_touching_or_nearby_grid_relation"
    ]["pass"] is True

    # Move U3 far away from U1 so they no longer touch.
    bad_adj = json.loads(json.dumps(layout))
    for u in bad_adj["grid_layout_by_fp"]["FPx"]["units"]:
        if u["unit_id"] == "U3":
            u["rects"] = [{"x": 0, "y": 8, "w": 2, "h": 2}]
    rep_adj = _build_w16_compatibility_report(
        grid_layout=bad_adj, topology_brief=topo, candidate=cand,
        target_fp_ids={"FPx"},
        production_diff_empty=True, db_write_count=0,
        image_import_seen=False, image_api_call_count=0,
        svg_emitted=True, model_used="gpt-5.5",
        stage_status="generated", missing_inputs=[],
        prev_run_id="fakeW15e",
    )
    # U1 sits at y=0..6 / x=0..6; U3 sits at y=8..10 / x=0..2. There is a
    # one-row gap (y=7) between them — not touching, not within tolerance.
    # Move U3 even further to be safely > 1 cell away.
    for u in bad_adj["grid_layout_by_fp"]["FPx"]["units"]:
        if u["unit_id"] == "U3":
            u["rects"] = [{"x": 0, "y": 9, "w": 2, "h": 1}]
    rep_adj = _build_w16_compatibility_report(
        grid_layout=bad_adj, topology_brief=topo, candidate=cand,
        target_fp_ids={"FPx"},
        production_diff_empty=True, db_write_count=0,
        image_import_seen=False, image_api_call_count=0,
        svg_emitted=True, model_used="gpt-5.5",
        stage_status="generated", missing_inputs=[],
        prev_run_id="fakeW15e",
    )
    assert rep_adj["invariants"][
        "declared_open_or_door_adjacency_has_touching_or_nearby_grid_relation"
    ]["pass"] is False


def test_w16_svg_render_emits_labels(tmp_path):
    """SVG render must place each unit as a labelled rect and emit a numbered
    circle marker per fixture, deterministically (no image API call). The
    SVG file must be written under <run_dir>/svg/<fp_id>.svg, must be
    XML well-formed, and marker numbers must appear as plain text (not as
    `>N<` glyphs which break XML parsing)."""
    import xml.etree.ElementTree as ET

    from experiment_floor_plan_grid_layout_slice import (
        _render_w16_svg,
    )
    layout = _synthetic_llm_grid_layout()
    run_dir = tmp_path
    out_path = _render_w16_svg(
        fp_id="FPx",
        fp_layout=layout["grid_layout_by_fp"]["FPx"],
        run_dir=run_dir,
    )
    assert out_path.exists()
    assert out_path == run_dir / "svg" / "FPx.svg"
    svg_text = out_path.read_text()
    assert svg_text.startswith("<?xml") or svg_text.startswith("<svg")
    # Every unit_id appears as a <text> label.
    for uid in ("U1", "U2", "U3"):
        assert uid in svg_text, f"missing unit label {uid}"
    # Every fixture number must be rendered as a `data-fixture-number`
    # attribute (machine-readable). The `>>N<<` raw-bracket anti-pattern
    # that broke XML parsing in W16a must NOT reappear.
    for n in (1, 2, 3, 7, 11):
        assert f'data-fixture-number="{n}"' in svg_text, (
            f"missing data-fixture-number={n}"
        )
        assert f">>{n}<<" not in svg_text, (
            f"W16a `>>N<<` raw-bracket anti-pattern reappeared for {n}"
        )
    # Enclosed unit U3 uses stronger stroke than open units.
    assert 'data-enclosure="full_wall_enclosed_room"' in svg_text
    # The SVG must be XML well-formed AND every fixture number must
    # appear as the plain integer body of a <text> element (no raw `<`
    # or `>` mid-text).
    tree = ET.parse(out_path)
    ns = "{http://www.w3.org/2000/svg}"
    text_numbers: set = set()
    for t in tree.getroot().iter(f"{ns}text"):
        body = (t.text or "").strip()
        if body.isdigit():
            text_numbers.add(int(body))
    for n in (1, 2, 3, 7, 11):
        assert n in text_numbers, (
            f"fixture marker {n} not present as plain-integer <text> body"
        )


def test_w16_svg_invariant_blocks_invalid_xml(tmp_path):
    """The svg_render_emitted invariant must fail when the SVG file exists
    but is not XML well-formed."""
    from experiment_floor_plan_grid_layout_slice import (
        _build_w16_compatibility_report,
        _render_w16_svg,
    )
    layout = _synthetic_llm_grid_layout()
    topo = _synthetic_topology_brief()
    cand = _synthetic_candidate()

    out_path = _render_w16_svg(
        fp_id="FPx",
        fp_layout=layout["grid_layout_by_fp"]["FPx"],
        run_dir=tmp_path,
    )
    # The well-formed run must pass.
    rep = _build_w16_compatibility_report(
        grid_layout=layout, topology_brief=topo, candidate=cand,
        target_fp_ids={"FPx"},
        production_diff_empty=True, db_write_count=0,
        image_import_seen=False, image_api_call_count=0,
        svg_emitted=True, svg_paths_by_fp={"FPx": out_path},
        model_used="gpt-5.5",
        stage_status="generated", missing_inputs=[],
        prev_run_id="fakeW15e",
    )
    assert rep["invariants"]["svg_is_well_formed_xml"]["pass"] is True

    # Intentionally corrupt the SVG to be XML-invalid.
    out_path.write_text("<svg><text>>1<</text></svg>")
    rep_bad = _build_w16_compatibility_report(
        grid_layout=layout, topology_brief=topo, candidate=cand,
        target_fp_ids={"FPx"},
        production_diff_empty=True, db_write_count=0,
        image_import_seen=False, image_api_call_count=0,
        svg_emitted=True, svg_paths_by_fp={"FPx": out_path},
        model_used="gpt-5.5",
        stage_status="generated", missing_inputs=[],
        prev_run_id="fakeW15e",
    )
    assert rep_bad["invariants"]["svg_is_well_formed_xml"]["pass"] is False


def test_w16_dry_run_no_image_api_and_production_guard(tmp_path):
    """dry-run with --generate off must skip the LLM call. The combined
    production guard surfaces production_diff / db_write / image_import /
    image_api_call_count as one invariant."""
    from experiment_floor_plan_grid_layout_slice import (
        _build_w16_compatibility_report,
    )
    layout = _synthetic_llm_grid_layout()
    topo = _synthetic_topology_brief()
    cand = _synthetic_candidate()

    rep_ok = _build_w16_compatibility_report(
        grid_layout=layout, topology_brief=topo, candidate=cand,
        target_fp_ids={"FPx"},
        production_diff_empty=True, db_write_count=0,
        image_import_seen=False, image_api_call_count=0,
        svg_emitted=True, model_used="gpt-5.5",
        stage_status="generated", missing_inputs=[],
        prev_run_id="fakeW15e",
    )
    assert rep_ok["invariants"][
        "production_diff_zero_db_write_zero_image_api_call_zero"
    ]["pass"] is True
    assert rep_ok["invariants"]["svg_render_emitted_and_no_image_api_call"]["pass"] is True

    # Any of: production_diff dirty / db_write / image import / image api → fails.
    rep_image = _build_w16_compatibility_report(
        grid_layout=layout, topology_brief=topo, candidate=cand,
        target_fp_ids={"FPx"},
        production_diff_empty=True, db_write_count=0,
        image_import_seen=False, image_api_call_count=1,
        svg_emitted=True, model_used="gpt-5.5",
        stage_status="generated", missing_inputs=[],
        prev_run_id="fakeW15e",
    )
    assert rep_image["invariants"][
        "production_diff_zero_db_write_zero_image_api_call_zero"
    ]["pass"] is False
    assert rep_image["invariants"]["svg_render_emitted_and_no_image_api_call"]["pass"] is False

    # target_fp_only invariant: ask for an fp not in {fp_l05_01} → fails. We
    # use the actual production CLI value to assert the wave-1 restriction.
    rep_target = _build_w16_compatibility_report(
        grid_layout=layout, topology_brief=topo, candidate=cand,
        target_fp_ids={"FPx", "FPy"},
        production_diff_empty=True, db_write_count=0,
        image_import_seen=False, image_api_call_count=0,
        svg_emitted=True, model_used="gpt-5.5",
        stage_status="generated", missing_inputs=[],
        prev_run_id="fakeW15e",
    )
    # The script's CLI restricts targets to {fp_l05_01}; the helper merely
    # records what was passed. Here we assert the surface field carries
    # >1 ids honestly (the script will reject upstream).
    assert sorted(
        rep_target["invariants"]["target_fp_only_fp_l05_01"]["detail"]["received_target_fp_ids"]
    ) == ["FPx", "FPy"]


def test_w16_gpt_5_5_routing_fail_closed(monkeypatch, tmp_path):
    """The LLM caller must route to GPT-5.5 explicitly. When OPENAI_API_KEY
    is absent OR the litellm completion raises, the caller must NOT fall
    back to gemini — it must raise and the run must record validation_failed
    upstream. monkeypatched fake caller verifies this routing surface."""
    import experiment_floor_plan_grid_layout_slice as mod
    monkeypatch.delenv("OPENAI_API_KEY", raising=False)
    with pytest.raises(RuntimeError) as excinfo:
        mod._generate_w16_via_llm({"foo": "bar"}, model="gpt-5.5")
    msg = str(excinfo.value)
    assert "OPENAI_API_KEY" in msg

    # When the model is not gpt-5.5, the routing must refuse (no fallback).
    monkeypatch.setenv("OPENAI_API_KEY", "sk-test-fake-key")
    with pytest.raises(RuntimeError) as excinfo2:
        mod._generate_w16_via_llm({"foo": "bar"}, model="gemini-3.5-flash")
    assert "gpt-5.5" in str(excinfo2.value).lower() or "model" in str(excinfo2.value).lower()


def test_w16_llm_call_no_auto_retry_by_default(monkeypatch):
    """`_generate_w16_via_llm` must NOT auto-retry by default. The previous
    `retry_once=True` default could attempt 2 paid calls on a transient
    failure even though the W16 spec was "GPT-5.5 generate 1 time only,
    fail-closed". A faked litellm that always raises must produce exactly
    one attempt before raising."""
    import experiment_floor_plan_grid_layout_slice as mod

    monkeypatch.setenv("OPENAI_API_KEY", "sk-test-fake-key")

    attempts = {"count": 0}

    class _FakeLitellm:
        @staticmethod
        def completion(*_, **__):
            attempts["count"] += 1
            raise RuntimeError("simulated transient failure")

    # Replace the real litellm import inside the helper. The helper does
    # `import litellm` lazily; we monkeypatch sys.modules so the lazy
    # import resolves to the fake.
    monkeypatch.setitem(sys.modules, "litellm", _FakeLitellm)

    with pytest.raises(RuntimeError):
        mod._generate_w16_via_llm({"foo": "bar"}, model="gpt-5.5")
    assert attempts["count"] == 1, (
        f"expected exactly 1 attempt by default, got {attempts['count']}"
    )

    # If the caller explicitly opts into retry, two attempts are allowed.
    attempts["count"] = 0
    with pytest.raises(RuntimeError):
        mod._generate_w16_via_llm(
            {"foo": "bar"}, model="gpt-5.5", retry_once=True,
        )
    assert attempts["count"] == 2


def test_w16_door_adjacency_requires_axis_overlap(tmp_path):
    """door_between tolerance is meant to absorb a one-cell door-jamb
    gap. It must NOT pass a diagonal pair that has no shared axis overlap
    (a true neither-touching-nor-axis-aligned case)."""
    from experiment_floor_plan_grid_layout_slice import (
        _rects_share_edge,
        _rects_within_tolerance,
    )
    # Diagonal 1-cell gap: A at (0,0)–(2,2), B at (3,3)–(5,5). No shared
    # edge, no axis overlap (col_gap=1, row_gap=1).
    a = [{"x": 0, "y": 0, "w": 2, "h": 2}]
    b = [{"x": 3, "y": 3, "w": 2, "h": 2}]
    assert _rects_share_edge(a, b) is False
    assert _rects_within_tolerance(a, b, tol=1) is False, (
        "diagonal 1-cell gap must NOT count as door-adjacent"
    )

    # Axis-aligned 1-cell gap (door-jamb tolerance OK).
    a2 = [{"x": 0, "y": 0, "w": 2, "h": 2}]
    b2 = [{"x": 3, "y": 0, "w": 2, "h": 2}]
    assert _rects_share_edge(a2, b2) is False
    assert _rects_within_tolerance(a2, b2, tol=1) is True, (
        "axis-aligned 1-cell gap with row overlap must count as adjacent"
    )

    # Shared edge (door directly on the wall) still passes.
    a3 = [{"x": 0, "y": 0, "w": 2, "h": 2}]
    b3 = [{"x": 2, "y": 0, "w": 2, "h": 2}]
    assert _rects_share_edge(a3, b3) is True
    assert _rects_within_tolerance(a3, b3, tol=1) is True


def test_w16_model_invariant_records_gpt_5_5_when_generated(tmp_path):
    """When the run was --generate, the compat report must record that
    `model_used == "gpt-5.5"`. dry-run records None (skip)."""
    from experiment_floor_plan_grid_layout_slice import (
        _build_w16_compatibility_report,
    )
    layout = _synthetic_llm_grid_layout()
    topo = _synthetic_topology_brief()
    cand = _synthetic_candidate()

    # Generated → model invariant active.
    rep = _build_w16_compatibility_report(
        grid_layout=layout, topology_brief=topo, candidate=cand,
        target_fp_ids={"FPx"},
        production_diff_empty=True, db_write_count=0,
        image_import_seen=False, image_api_call_count=0,
        svg_emitted=True, model_used="gpt-5.5",
        stage_status="generated", missing_inputs=[],
        prev_run_id="fakeW15e",
    )
    assert rep["invariants"]["model_is_gpt_5_5_when_generated"]["pass"] is True

    rep_bad = _build_w16_compatibility_report(
        grid_layout=layout, topology_brief=topo, candidate=cand,
        target_fp_ids={"FPx"},
        production_diff_empty=True, db_write_count=0,
        image_import_seen=False, image_api_call_count=0,
        svg_emitted=True, model_used="gemini-3.5-flash",
        stage_status="generated", missing_inputs=[],
        prev_run_id="fakeW15e",
    )
    assert rep_bad["invariants"]["model_is_gpt_5_5_when_generated"]["pass"] is False

    # Dry-run → model invariant auto-PASS (skip).
    rep_dry = _build_w16_compatibility_report(
        grid_layout=layout, topology_brief=topo, candidate=cand,
        target_fp_ids={"FPx"},
        production_diff_empty=True, db_write_count=0,
        image_import_seen=False, image_api_call_count=0,
        svg_emitted=True, model_used=None,
        stage_status="dry_run", missing_inputs=[],
        prev_run_id="fakeW15e",
    )
    assert rep_dry["invariants"]["model_is_gpt_5_5_when_generated"]["pass"] is True


# ─────────────────────────────────────────────────────────────────────────────
# W16d schema-level budget invariants
# ─────────────────────────────────────────────────────────────────────────────


def _w16d_build_report(*, layout, topo, cand, target_fp_ids={"FPx"},
                       model_used="gpt-5.5", stage_status="generated",
                       svg_paths_by_fp=None):
    from experiment_floor_plan_grid_layout_slice import (
        _build_w16_compatibility_report,
    )
    return _build_w16_compatibility_report(
        grid_layout=layout, topology_brief=topo, candidate=cand,
        target_fp_ids=target_fp_ids,
        production_diff_empty=True, db_write_count=0,
        image_import_seen=False, image_api_call_count=0,
        svg_emitted=svg_paths_by_fp is not None,
        svg_paths_by_fp=svg_paths_by_fp,
        model_used=model_used,
        stage_status=stage_status, missing_inputs=[],
        prev_run_id="fakeW15e",
    )


def test_w16d_scale_hint_cell_budget_enforced():
    """compact_nook_or_wall_run max 4 cells + one-row/one-col or short L only;
    compact_service_cell + interior_threshold ≤4 cells; secondary_zone private
    rooms in 8-18; primary_zone in 25-40."""
    layout = _synthetic_llm_grid_layout()
    topo = _synthetic_topology_brief()
    cand = _synthetic_candidate()

    # Baseline synthetic passes.
    rep = _w16d_build_report(layout=layout, topo=topo, cand=cand)
    assert rep["invariants"]["scale_hint_cell_budget"]["pass"] is True

    # Inflate U2 (compact_nook_or_wall_run) into a 3x3 room (9 cells) → fail.
    bad = json.loads(json.dumps(layout))
    for u in bad["grid_layout_by_fp"]["FPx"]["units"]:
        if u["unit_id"] == "U2":
            u["rects"] = [{"x": 0, "y": 6, "w": 3, "h": 3}]
            u["scale_hint"] = "compact_nook_or_wall_run"
    rep_bad = _w16d_build_report(layout=bad, topo=topo, cand=cand)
    assert rep_bad["invariants"]["scale_hint_cell_budget"]["pass"] is False

    # secondary_zone private room with 20 cells → fail.
    bad2 = json.loads(json.dumps(layout))
    for u in bad2["grid_layout_by_fp"]["FPx"]["units"]:
        if u["unit_id"] == "U3":
            u["rects"] = [{"x": 6, "y": 0, "w": 4, "h": 5}]
            u["scale_hint"] = "secondary_zone"
    rep_bad2 = _w16d_build_report(layout=bad2, topo=topo, cand=cand)
    assert rep_bad2["invariants"]["scale_hint_cell_budget"]["pass"] is False


def test_w16d_total_occupied_cells_in_bound():
    """Unique occupied cells must be within 55-75/100. Baseline synthetic
    is sized to 56 (just inside the lower bound). Verify against oversized
    (100 cells used) and undersized (12 cells) layouts — both fail."""
    layout = _synthetic_llm_grid_layout()
    topo = _synthetic_topology_brief()
    cand = _synthetic_candidate()

    rep_ok = _w16d_build_report(layout=layout, topo=topo, cand=cand)
    assert rep_ok["invariants"]["total_occupied_cells_in_bound"]["pass"] is True

    # Inflate every unit so total > 75 → fail.
    big = json.loads(json.dumps(layout))
    big["grid_layout_by_fp"]["FPx"]["units"] = [
        {"unit_id": "U1", "unit_kind": "living_zone",
         "enclosure_mode": "open_zone_inside_plan",
         "rects": [{"x": 0, "y": 0, "w": 10, "h": 4}],
         "open_to": ["U2"], "door_to": ["U3"],
         "scale_hint": "primary_zone", "rationale_refs": []},
        {"unit_id": "U2", "unit_kind": "kitchen_zone",
         "enclosure_mode": "open_zone_inside_plan",
         "rects": [{"x": 0, "y": 4, "w": 10, "h": 1}],
         "open_to": ["U1"], "door_to": [],
         "scale_hint": "compact_nook_or_wall_run", "rationale_refs": []},
        {"unit_id": "U3", "unit_kind": "private_room",
         "enclosure_mode": "full_wall_enclosed_room",
         "rects": [{"x": 0, "y": 5, "w": 10, "h": 5}],
         "open_to": [], "door_to": ["U1"],
         "scale_hint": "secondary_zone", "rationale_refs": []},
    ]
    rep_big = _w16d_build_report(layout=big, topo=topo, cand=cand)
    assert rep_big["invariants"]["total_occupied_cells_in_bound"]["pass"] is False

    # Tiny micro-layout under the lower bound → fail.
    small = json.loads(json.dumps(layout))
    for u in small["grid_layout_by_fp"]["FPx"]["units"]:
        if u["unit_id"] == "U1":
            u["rects"] = [{"x": 0, "y": 0, "w": 3, "h": 3}]
        elif u["unit_id"] == "U2":
            u["rects"] = [{"x": 3, "y": 0, "w": 1, "h": 1}]
        elif u["unit_id"] == "U3":
            u["rects"] = [{"x": 4, "y": 0, "w": 2, "h": 1}]
    rep_small = _w16d_build_report(layout=small, topo=topo, cand=cand)
    assert rep_small["invariants"]["total_occupied_cells_in_bound"]["pass"] is False

    # Overlap-trick: declare two rects that overlap heavily; unique count
    # must dedup and so still fail when the unique count exits bounds.
    overlap = json.loads(json.dumps(layout))
    overlap["grid_layout_by_fp"]["FPx"]["units"][0]["rects"] = [
        {"x": 0, "y": 0, "w": 10, "h": 10},
        {"x": 0, "y": 0, "w": 10, "h": 10},   # duplicate – must not double-count
    ]
    rep_overlap = _w16d_build_report(layout=overlap, topo=topo, cand=cand)
    # 100 unique cells from U1 alone → outside [55, 75] → fail (proves
    # unique-cell counting, not naive area sum).
    assert rep_overlap["invariants"]["total_occupied_cells_in_bound"]["pass"] is False
    detail = rep_overlap["invariants"]["total_occupied_cells_in_bound"]["detail"]
    assert detail.get("unique_cells") == 100


def test_w16d_private_room_primary_zone_disallowed():
    """Unless the source topology brief annotates the unit as
    `allow_primary_zone=true`, any spatial_unit with kind=private_room
    that the LLM tagged scale_hint=primary_zone must fail."""
    layout = _synthetic_llm_grid_layout()
    topo = _synthetic_topology_brief()
    cand = _synthetic_candidate()

    # Baseline synthetic uses secondary_zone for U3 (private_room) — pass.
    rep_ok = _w16d_build_report(layout=layout, topo=topo, cand=cand)
    assert rep_ok["invariants"]["private_room_primary_zone_disallowed"]["pass"] is True

    # Promote U3 to primary_zone → fail.
    bad = json.loads(json.dumps(layout))
    for u in bad["grid_layout_by_fp"]["FPx"]["units"]:
        if u["unit_id"] == "U3":
            u["scale_hint"] = "primary_zone"
    rep_bad = _w16d_build_report(layout=bad, topo=topo, cand=cand)
    assert rep_bad["invariants"]["private_room_primary_zone_disallowed"]["pass"] is False


def test_w16d_unit_rects_do_not_overlap():
    """Positive-area overlap between any two rects (intra or inter unit)
    must fail. Shared edges (touching) are OK."""
    layout = _synthetic_llm_grid_layout()
    topo = _synthetic_topology_brief()
    cand = _synthetic_candidate()

    rep_ok = _w16d_build_report(layout=layout, topo=topo, cand=cand)
    assert rep_ok["invariants"]["unit_rects_do_not_overlap"]["pass"] is True

    # Force U1 and U3 to overlap by a 2x2 area.
    bad = json.loads(json.dumps(layout))
    for u in bad["grid_layout_by_fp"]["FPx"]["units"]:
        if u["unit_id"] == "U3":
            u["rects"] = [{"x": 4, "y": 4, "w": 4, "h": 4}]
    rep_bad = _w16d_build_report(layout=bad, topo=topo, cand=cand)
    assert rep_bad["invariants"]["unit_rects_do_not_overlap"]["pass"] is False


def test_w16e_unit_kind_scale_hint_compatibility():
    """Each unit_kind has a strict whitelist of allowed scale_hint values
    (source topology can override via `allow_primary_zone=true` or
    `allow_scale_hints=[...]`). The default policy:
      living_zone → primary_zone only
      kitchen_zone → compact_nook_or_wall_run only
      service_room → compact_service_cell only
      entry_transition → interior_threshold or exterior_landing only
      private_room → secondary_zone only

    Baseline synthetic uses living_zone+primary_zone (pass), kitchen+nook
    (pass), private_room+secondary (pass). Each invalid pairing fails.
    """
    layout = _synthetic_llm_grid_layout()
    topo = _synthetic_topology_brief()
    cand = _synthetic_candidate()

    rep_ok = _w16d_build_report(layout=layout, topo=topo, cand=cand)
    assert rep_ok["invariants"]["unit_kind_scale_hint_compatibility"]["pass"] is True

    # service_room + secondary_zone (the actual W16d bathroom loophole).
    bad_service = json.loads(json.dumps(layout))
    bad_service["grid_layout_by_fp"]["FPx"]["units"].append({
        "unit_id": "U4", "unit_kind": "service_room",
        "enclosure_mode": "full_wall_enclosed_room",
        "rects": [{"x": 0, "y": 8, "w": 2, "h": 1}],
        "open_to": [], "door_to": [],
        "scale_hint": "secondary_zone", "rationale_refs": []
    })
    # Add the new unit to the topology so unit_ids_exist_in_w15_topology
    # does not blow up before we check compatibility.
    topo_bad = json.loads(json.dumps(topo))
    topo_bad["source_topology_by_fp"]["FPx"]["spatial_units"].append({
        "unit_id": "U4", "unit_label": "service zone",
        "unit_kind": "service_room", "is_enclosed_room": True,
        "evidence_refs": [],
    })
    rep_bad_service = _w16d_build_report(
        layout=bad_service, topo=topo_bad, cand=cand,
    )
    assert rep_bad_service["invariants"][
        "unit_kind_scale_hint_compatibility"
    ]["pass"] is False

    # kitchen_zone + primary_zone.
    bad_k = json.loads(json.dumps(layout))
    for u in bad_k["grid_layout_by_fp"]["FPx"]["units"]:
        if u["unit_id"] == "U2":
            u["scale_hint"] = "primary_zone"
    rep_bad_k = _w16d_build_report(layout=bad_k, topo=topo, cand=cand)
    assert rep_bad_k["invariants"][
        "unit_kind_scale_hint_compatibility"
    ]["pass"] is False

    # living_zone + compact_nook_or_wall_run.
    bad_l = json.loads(json.dumps(layout))
    for u in bad_l["grid_layout_by_fp"]["FPx"]["units"]:
        if u["unit_id"] == "U1":
            u["scale_hint"] = "compact_nook_or_wall_run"
    rep_bad_l = _w16d_build_report(layout=bad_l, topo=topo, cand=cand)
    assert rep_bad_l["invariants"][
        "unit_kind_scale_hint_compatibility"
    ]["pass"] is False

    # entry_transition + compact_nook_or_wall_run (not in entry whitelist).
    bad_e = json.loads(json.dumps(layout))
    bad_e["grid_layout_by_fp"]["FPx"]["units"].append({
        "unit_id": "U5", "unit_kind": "entry_transition",
        "enclosure_mode": "interior_threshold",
        "rects": [{"x": 2, "y": 8, "w": 1, "h": 1}],
        "open_to": [], "door_to": [],
        "scale_hint": "compact_nook_or_wall_run", "rationale_refs": []
    })
    topo_e = json.loads(json.dumps(topo))
    topo_e["source_topology_by_fp"]["FPx"]["spatial_units"].append({
        "unit_id": "U5", "unit_label": "entry",
        "unit_kind": "entry_transition", "is_enclosed_room": False,
        "evidence_refs": [],
    })
    rep_bad_e = _w16d_build_report(layout=bad_e, topo=topo_e, cand=cand)
    assert rep_bad_e["invariants"][
        "unit_kind_scale_hint_compatibility"
    ]["pass"] is False

    # private_room + primary_zone (already covered by
    # private_room_primary_zone_disallowed, but compatibility must also
    # fail unless `allow_primary_zone=true`).
    bad_p = json.loads(json.dumps(layout))
    for u in bad_p["grid_layout_by_fp"]["FPx"]["units"]:
        if u["unit_id"] == "U3":
            u["scale_hint"] = "primary_zone"
    rep_bad_p = _w16d_build_report(layout=bad_p, topo=topo, cand=cand)
    assert rep_bad_p["invariants"][
        "unit_kind_scale_hint_compatibility"
    ]["pass"] is False

    # Override: a private_room with allow_primary_zone=true must pass.
    topo_override = json.loads(json.dumps(topo))
    for su in topo_override["source_topology_by_fp"]["FPx"]["spatial_units"]:
        if su["unit_id"] == "U3":
            su["allow_primary_zone"] = True
    layout_ok = json.loads(json.dumps(layout))
    for u in layout_ok["grid_layout_by_fp"]["FPx"]["units"]:
        if u["unit_id"] == "U3":
            u["scale_hint"] = "primary_zone"
            u["rects"] = [{"x": 6, "y": 0, "w": 5, "h": 5}]   # within primary budget
            # ensure no overlap; widen if needed
    rep_override = _w16d_build_report(
        layout=layout_ok, topo=topo_override, cand=cand,
    )
    assert rep_override["invariants"][
        "unit_kind_scale_hint_compatibility"
    ]["pass"] is True


def test_w16d_fixture_cells_inside_declared_unit_rects():
    """Every fixture.cell must fall inside at least one rect of the
    fixture's bound unit_id. Cells outside the rect set must fail."""
    layout = _synthetic_llm_grid_layout()
    topo = _synthetic_topology_brief()
    cand = _synthetic_candidate()

    rep_ok = _w16d_build_report(layout=layout, topo=topo, cand=cand)
    assert rep_ok["invariants"]["fixture_cells_inside_declared_unit_rects"]["pass"] is True

    # Move fixture #1 outside its unit (U1) rect.
    bad = json.loads(json.dumps(layout))
    for fx in bad["grid_layout_by_fp"]["FPx"]["fixtures"]:
        if fx["number"] == 1:
            fx["cell"] = [9, 9]   # U1 rect is (0..6, 0..6); (9,9) is outside.
    rep_bad = _w16d_build_report(layout=bad, topo=topo, cand=cand)
    assert rep_bad["invariants"]["fixture_cells_inside_declared_unit_rects"]["pass"] is False


def test_w16_methodology_grep_no_scenario_specific_literals():
    """Source script must not embed scenario-specific tokens. Tokens are
    assembled per-char so this test file's own assertion text does not match
    the regex it builds."""
    script_path = _SCRIPTS_DIR / "experiment_floor_plan_grid_layout_slice.py"
    assert script_path.exists(), f"script missing: {script_path}"
    forbidden_tokens = [
        "b" + "edroom", "ki" + "tchen", "blood" + "stain", "cur" + "tain",
        "coo" + "ktop", "tele" + "vision", "cri" + "me", "vi" + "lla",
        "roo" + "ftop", "foot" + "print", "pol" + "ice", "de" + "ck",
        "wheel" + "house", "ba" + "throom",
    ]
    pat = re.compile(r"(?i)\b(" + "|".join(forbidden_tokens) + r")\b")
    m = pat.search(script_path.read_text())
    assert m is None, (
        f"{script_path.name}: scenario-specific token leaked → "
        f"{m.group(0) if m else ''}"
    )
