"""W13 structural tests — generic synthetic fixtures only.

No scenario-specific tokens (rooms/props/states/colors/narrative literals) in
test methodology. The production checkpoint loader is not exercised here;
fp_context is injected directly as a synthetic dict.
"""
from __future__ import annotations

import json
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))


def _synthetic_candidates() -> dict:
    return {
        "FP1": {
            "fp_id": "FP1",
            "group_id_pointer": "G1",
            "candidate_diagram_t2i_prompt": "schematic",
            "candidate_key_elements": [],
            "candidate_numbered_elements": [
                {"number": 1, "label": "alpha", "category": "area",
                 "position_hint": "p1", "zone_id_pointer": "Z1"},
                {"number": 2, "label": "beta", "category": "opening",
                 "position_hint": "p2", "zone_id_pointer": "Z1"},
                {"number": 3, "label": "gamma", "category": "furniture",
                 "position_hint": "p3", "zone_id_pointer": "Z1"},
                {"number": 4, "label": "delta", "category": "plot_device",
                 "position_hint": "p4", "zone_id_pointer": "Z1"},
            ],
            "candidate_camera_recommendations": [
                {"bg_id": "BG1", "sub_location": "Z1",
                 "camera_position": "near #1", "camera_height": "1.6m",
                 "lens_hint": "35mm", "framing_notes": ""},
                {"bg_id": "BG2", "sub_location": "Z1",
                 "camera_position": "near #2", "camera_height": "1.6m",
                 "lens_hint": "35mm", "framing_notes": ""},
            ],
            "reconciliation_notes_vs_production": "",
        },
    }


def _synthetic_per_bg() -> dict:
    return {
        "BG1": {
            "bg_id": "BG1", "group_id": "G1", "fp_id": "FP1",
            "applies_to_shots": ["S1_Shot1"],
            "floor_plan_ref_role": "layout_only",
            "use_numbered_elements": [1, 2],
            "ignore_numbered_elements": [4],
            "camera_axis_used": "ax1",
            "camera_axis_source": "w11_axis",
            "visible_zone_scope": ["Z1"],
            "prior_bg_ref_role": "none",
            "render_prompt_appendix": "appendix1",
            "final_prompt_assembly_preview": "base text 1",
        },
        "BG2": {
            "bg_id": "BG2", "group_id": "G1", "fp_id": "FP1",
            "applies_to_shots": ["S1_Shot2"],
            "floor_plan_ref_role": "layout_only",
            "use_numbered_elements": [3],
            "ignore_numbered_elements": [4],
            "camera_axis_used": "ax_local",
            "camera_axis_source": "candidate_camera_recommendation",
            "visible_zone_scope": ["Z1"],
            "prior_bg_ref_role": "weak_continuity",
            "render_prompt_appendix": "appendix2",
            "final_prompt_assembly_preview": "base text 2",
        },
        "BG3": {
            "bg_id": "BG3", "group_id": "G2", "fp_id": "",
            "applies_to_shots": ["S2_Shot1"],
            "floor_plan_ref_role": "not_used",
            "use_numbered_elements": [],
            "ignore_numbered_elements": [],
            "camera_axis_used": "ax_exterior",
            "camera_axis_source": "w11_axis",
            "visible_zone_scope": ["zoneX"],
            "prior_bg_ref_role": "none",
            "render_prompt_appendix": "appendix3",
            "final_prompt_assembly_preview": "base text 3",
        },
    }


def _synthetic_fp_context(*, png_exists: bool = True) -> dict:
    return {
        "fp_prompt_path": "/fake", "fp_render_path": "/fake",
        "fp_prompt_status": "ok", "fp_render_status": "ok",
        "floor_plans": {
            "FP1": {
                "fp_id": "FP1", "group_id": "G1", "depends_on_fp": [],
                "key_elements": [], "numbered_elements": [],
                "camera_recommendations": [], "t2i_prompt": "",
                "applied_shots": [],
                "png_path": "/fake/FP1.png" if png_exists else "",
                "png_exists": png_exists,
            },
        },
    }


def test_w13_payload_coverage_and_unlisted_set_difference():
    """Every per_bg bg_id appears in payload; unlisted = candidate - use - ignore."""
    from experiment_background_render_payload_slice import (
        _build_per_bg_payloads,
        _build_w13_compatibility_report,
    )
    per_bg = _synthetic_per_bg()
    cands = _synthetic_candidates()
    fpctx = _synthetic_fp_context(png_exists=True)
    payloads = _build_per_bg_payloads(
        per_bg=per_bg, candidates=cands, fp_context=fpctx,
    )
    assert set(payloads.keys()) == {"BG1", "BG2", "BG3"}

    # BG1: candidate {1,2,3,4} - use {1,2} - ignore {4} = unlisted {3}
    unlisted_bg1 = [s.split(" ", 1)[0] for s in payloads["BG1"]["unlisted_candidate_elements_with_labels"]]
    assert unlisted_bg1 == ["#3"], unlisted_bg1
    # BG2: candidate {1,2,3,4} - use {3} - ignore {4} = unlisted {1,2}
    unlisted_bg2 = [s.split(" ", 1)[0] for s in payloads["BG2"]["unlisted_candidate_elements_with_labels"]]
    assert unlisted_bg2 == ["#1", "#2"], unlisted_bg2
    # BG3 (not_used): unlisted is empty (no fp)
    assert payloads["BG3"]["unlisted_candidate_elements_with_labels"] == []

    report = _build_w13_compatibility_report(
        per_bg=per_bg, payloads=payloads,
        candidates=cands, fp_context=fpctx,
        production_diff_empty=True, db_write_count=0,
        image_import_seen=False, missing_inputs=[],
        prev_run_id="fakeW12", stage_status="generated",
    )
    assert report["invariants"]["all_bg_payloads_covered"]["pass"] is True

    # Drop a bg from per_bg, regenerate payloads → coverage invariant fails.
    per_bg_short = json.loads(json.dumps(per_bg))
    per_bg_short.pop("BG3")
    payloads_short = _build_per_bg_payloads(
        per_bg=per_bg_short, candidates=cands, fp_context=fpctx,
    )
    # And we feed the FULL per_bg to the report so the set differs.
    report_bad = _build_w13_compatibility_report(
        per_bg=per_bg, payloads=payloads_short,
        candidates=cands, fp_context=fpctx,
        production_diff_empty=True, db_write_count=0,
        image_import_seen=False, missing_inputs=[],
        prev_run_id="fakeW12", stage_status="generated",
    )
    assert report_bad["invariants"]["all_bg_payloads_covered"]["pass"] is False


def test_w13_api_method_preview_and_floor_plan_ref_resolution():
    """layout_only + fp_id → images.edit; not_used → images.generate. edit
    preview requires fp_ref path + exists; missing causes invariant fail.
    layout_only + empty use list emits a non-fatal payload warning."""
    from experiment_background_render_payload_slice import (
        _build_per_bg_payloads,
        _build_w13_compatibility_report,
    )
    per_bg = _synthetic_per_bg()
    cands = _synthetic_candidates()
    fpctx_ok = _synthetic_fp_context(png_exists=True)
    payloads = _build_per_bg_payloads(
        per_bg=per_bg, candidates=cands, fp_context=fpctx_ok,
    )
    assert payloads["BG1"]["api_method_preview"] == "images.edit"
    assert payloads["BG1"]["floor_plan_ref_path"] == "/fake/FP1.png"
    assert payloads["BG1"]["floor_plan_ref_exists"] is True
    assert payloads["BG2"]["api_method_preview"] == "images.edit"
    assert payloads["BG3"]["api_method_preview"] == "images.generate"
    assert payloads["BG3"]["floor_plan_ref_path"] == ""
    assert payloads["BG3"]["floor_plan_ref_exists"] is False
    # final_prompt for not_used bg must NOT carry the floor-plan reference block.
    assert "Floor-plan reference" not in payloads["BG3"]["final_prompt_text_for_image_preview"]
    assert "Floor-plan reference" in payloads["BG1"]["final_prompt_text_for_image_preview"]
    assert "Do not draw or anchor on these numbered elements" in payloads["BG1"]["final_prompt_text_for_image_preview"]

    report_ok = _build_w13_compatibility_report(
        per_bg=per_bg, payloads=payloads,
        candidates=cands, fp_context=fpctx_ok,
        production_diff_empty=True, db_write_count=0,
        image_import_seen=False, missing_inputs=[],
        prev_run_id="fakeW12", stage_status="generated",
    )
    assert report_ok["invariants"]["labels_and_floor_plan_refs_resolved"]["pass"] is True

    # png_exists=False → floor_plan_ref_exists False → invariant fail and
    # payload warning emitted.
    fpctx_missing = _synthetic_fp_context(png_exists=False)
    payloads_missing = _build_per_bg_payloads(
        per_bg=per_bg, candidates=cands, fp_context=fpctx_missing,
    )
    assert payloads_missing["BG1"]["floor_plan_ref_exists"] is False
    assert any(
        "edit_preview_" in w
        for w in payloads_missing["BG1"]["warnings_advisories"]
    )
    report_missing = _build_w13_compatibility_report(
        per_bg=per_bg, payloads=payloads_missing,
        candidates=cands, fp_context=fpctx_missing,
        production_diff_empty=True, db_write_count=0,
        image_import_seen=False, missing_inputs=[],
        prev_run_id="fakeW12", stage_status="generated",
    )
    assert report_missing["invariants"]["labels_and_floor_plan_refs_resolved"]["pass"] is False

    # layout_only with empty use list → non-fatal warning, no fail.
    per_bg_empty_use = json.loads(json.dumps(per_bg))
    per_bg_empty_use["BG1"]["use_numbered_elements"] = []
    payloads_empty = _build_per_bg_payloads(
        per_bg=per_bg_empty_use, candidates=cands, fp_context=fpctx_ok,
    )
    assert "layout_only_but_use_empty" in payloads_empty["BG1"]["warnings_advisories"]

    # Unresolved use number (not in candidate set) → labels invariant fail.
    per_bg_bad_use = json.loads(json.dumps(per_bg))
    per_bg_bad_use["BG1"]["use_numbered_elements"] = [1, 99]
    payloads_bad_use = _build_per_bg_payloads(
        per_bg=per_bg_bad_use, candidates=cands, fp_context=fpctx_ok,
    )
    report_bad_use = _build_w13_compatibility_report(
        per_bg=per_bg_bad_use, payloads=payloads_bad_use,
        candidates=cands, fp_context=fpctx_ok,
        production_diff_empty=True, db_write_count=0,
        image_import_seen=False, missing_inputs=[],
        prev_run_id="fakeW12", stage_status="generated",
    )
    assert report_bad_use["invariants"]["labels_and_floor_plan_refs_resolved"]["pass"] is False


def test_w13_model_and_no_image_api_guards():
    """image_model is always gpt-image-2; image_import_seen flips the no-call
    invariant; production_diff and db_write guards are surfaced."""
    from experiment_background_render_payload_slice import (
        _build_per_bg_payloads,
        _build_w13_compatibility_report,
    )
    per_bg = _synthetic_per_bg()
    cands = _synthetic_candidates()
    fpctx = _synthetic_fp_context(png_exists=True)
    payloads = _build_per_bg_payloads(
        per_bg=per_bg, candidates=cands, fp_context=fpctx,
    )
    for p in payloads.values():
        assert p["image_model"] == "gpt-image-2"
    report = _build_w13_compatibility_report(
        per_bg=per_bg, payloads=payloads,
        candidates=cands, fp_context=fpctx,
        production_diff_empty=True, db_write_count=0,
        image_import_seen=False, missing_inputs=[],
        prev_run_id="fakeW12", stage_status="generated",
    )
    assert report["invariants"]["model_is_gpt_image_2"]["pass"] is True
    assert report["invariants"]["no_image_api_call_and_zero_count"]["pass"] is True
    assert report["invariants"]["production_diff_zero_and_db_write_zero"]["pass"] is True
    assert report["invariants"]["w12_inputs_present"]["pass"] is True

    # Mutate one payload's image_model → model_is_gpt_image_2 fails.
    payloads_bad = json.loads(json.dumps(payloads))
    payloads_bad["BG1"]["image_model"] = "some-other-model"
    report_bad_model = _build_w13_compatibility_report(
        per_bg=per_bg, payloads=payloads_bad,
        candidates=cands, fp_context=fpctx,
        production_diff_empty=True, db_write_count=0,
        image_import_seen=False, missing_inputs=[],
        prev_run_id="fakeW12", stage_status="generated",
    )
    assert report_bad_model["invariants"]["model_is_gpt_image_2"]["pass"] is False

    # image_import_seen → no_image_api_call_and_zero_count fails.
    report_import_seen = _build_w13_compatibility_report(
        per_bg=per_bg, payloads=payloads,
        candidates=cands, fp_context=fpctx,
        production_diff_empty=True, db_write_count=0,
        image_import_seen=True, missing_inputs=[],
        prev_run_id="fakeW12", stage_status="generated",
    )
    assert report_import_seen["invariants"]["no_image_api_call_and_zero_count"]["pass"] is False

    # production_diff non-empty OR db_write_count > 0 → guard fails.
    report_prod_dirty = _build_w13_compatibility_report(
        per_bg=per_bg, payloads=payloads,
        candidates=cands, fp_context=fpctx,
        production_diff_empty=False, db_write_count=0,
        image_import_seen=False, missing_inputs=[],
        prev_run_id="fakeW12", stage_status="generated",
    )
    assert report_prod_dirty["invariants"]["production_diff_zero_and_db_write_zero"]["pass"] is False
    report_db_dirty = _build_w13_compatibility_report(
        per_bg=per_bg, payloads=payloads,
        candidates=cands, fp_context=fpctx,
        production_diff_empty=True, db_write_count=1,
        image_import_seen=False, missing_inputs=[],
        prev_run_id="fakeW12", stage_status="generated",
    )
    assert report_db_dirty["invariants"]["production_diff_zero_and_db_write_zero"]["pass"] is False

    # missing W12 inputs → w12_inputs_present fails.
    report_missing_inputs = _build_w13_compatibility_report(
        per_bg=per_bg, payloads=payloads,
        candidates=cands, fp_context=fpctx,
        production_diff_empty=True, db_write_count=0,
        image_import_seen=False, missing_inputs=["per_bg_render_reference_instruction.json"],
        prev_run_id="fakeW12", stage_status="generated",
    )
    assert report_missing_inputs["invariants"]["w12_inputs_present"]["pass"] is False


def test_w13_html_first_screen_table_columns_and_order(tmp_path):
    """HTML first screen must show the per-bg payload table BEFORE invariants,
    with raw JSON collapsed inside <details>. Required columns must be present
    in order."""
    from experiment_background_render_payload_slice import (
        _build_per_bg_payloads,
        _build_w13_compatibility_report,
        _render_w13_html,
    )
    per_bg = _synthetic_per_bg()
    cands = _synthetic_candidates()
    fpctx = _synthetic_fp_context(png_exists=True)
    payloads = _build_per_bg_payloads(
        per_bg=per_bg, candidates=cands, fp_context=fpctx,
    )
    report = _build_w13_compatibility_report(
        per_bg=per_bg, payloads=payloads,
        candidates=cands, fp_context=fpctx,
        production_diff_empty=True, db_write_count=0,
        image_import_seen=False, missing_inputs=[],
        prev_run_id="fakeW12", stage_status="generated",
    )
    run_meta = {
        "run_id": "RID1", "stage": "w13_background_render_payload_slice",
        "run_status": "succeeded", "exit_code": 0,
        "derived_from": "fakeW12", "model_used": None,
        "image_generation_count": 0, "image_generation_backend": "gpt-image-2",
    }
    _render_w13_html(run_meta, payloads, report, tmp_path)
    html = (tmp_path / "index.html").read_text()

    # All bg ids must appear in the per-bg table (first screen).
    for bg in ("BG1", "BG2", "BG3"):
        assert bg in html

    # Required column headers in declared order.
    columns = [
        "bg_id", "fp_id", "api_method_preview", "fp ref exists",
        "use labels", "ignore labels", "unlisted labels",
        "prior role", "final prompt preview",
    ]
    positions = [html.find(c) for c in columns]
    assert all(p > 0 for p in positions), positions
    assert positions == sorted(positions), positions

    # Per-bg table comes BEFORE invariants section.
    per_bg_pos = html.find("Per-bg render payload preview")
    inv_pos = html.find("Invariants")
    assert per_bg_pos > 0 and inv_pos > per_bg_pos

    # Raw JSON must live inside collapsed <details>.
    raw_pos = html.find("raw run_meta")
    assert raw_pos > 0
    details_pos = html.rfind("<details>", 0, raw_pos)
    assert details_pos > 0 and details_pos < raw_pos

    # No image section, no actual image bytes references.
    assert "image_generation_count: <b>0</b>" in html or "image_generation_count\": 0" in html
