"""W17C VLM readback slice — minimal safety tests only.

Codex specified: keep tests light, experiment stage. Three checks only:
dry-run safety + schema-shape validation + methodology grep.
"""
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))


def _seed_w17b3_run(tmp_path: Path) -> Path:
    """Create a minimal W17B3 success-run dir + tiny PNG file."""
    src = tmp_path / "w17b3_fake"
    src.mkdir()
    (src / "run_meta.json").write_text(json.dumps({
        "run_id": "w17b3_fake",
        "stage": "w17b_floor_plan_image_smoke_slice",
        "run_status": "succeeded", "exit_code": 0,
        "stage_status": "generated", "model": "gpt-image-2",
        "image_api_call_count": 1, "image_generation_count": 1,
        "args": {
            "derive_image_smoke_from": str(tmp_path / "w17a_fake"),
        },
        "derived_from": "w17a_fake",
        "png_relative_path": "png/fp_l05_01.png",
    }))
    (src / "generated_image_meta.json").write_text(json.dumps({
        "fp_smoke_result": {
            "fp_id": "fp_l05_01", "model": "gpt-image-2",
            "size": "1024x1024", "quality": "high",
            "assembled_prompt": (
                "Marker mapping contract ...\n"
                "#1 = u1 area | kind=spatial_unit | visual=filled_area | "
                "unit=U1 | placement=central core"
            ),
            "legend_entry_count": 3, "placement_carry_count": 3,
            "png_relative_path": "png/fp_l05_01.png",
            "status": "success",
        },
    }))
    png_dir = src / "png"
    png_dir.mkdir()
    # Tiny valid 1x1 PNG (raw bytes).
    tiny_png = bytes.fromhex(
        "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c489"
        "0000000d49444154789c6300010000000500010d0a2db40000000049454e44ae42"
        "6082"
    )
    (png_dir / "fp_l05_01.png").write_bytes(tiny_png)
    return src


def _seed_w17a_with_w15e(tmp_path: Path) -> None:
    """Mirror the W17A + W15e chain that the W17B3 reader walks."""
    w15e = tmp_path / "w15e_fake"
    w15e.mkdir()
    (w15e / "floor_plan_prompt_candidate.json").write_text(json.dumps({
        "candidate_floor_plans": {
            "fp_l05_01": {
                "fp_id": "fp_l05_01", "group_id_pointer": "Gx",
                "candidate_diagram_t2i_prompt": "schematic.",
                "candidate_key_elements": [],
                "candidate_numbered_elements": [
                    {"number": 1, "label": "u1 area", "category": "area",
                     "position_hint": "central core",
                     "unit_id_pointer": "U1"},
                    {"number": 2, "label": "u2 area", "category": "area",
                     "position_hint": "nw nook",
                     "unit_id_pointer": "U2"},
                    {"number": 3, "label": "u3 area", "category": "area",
                     "position_hint": "east room",
                     "unit_id_pointer": "U3"},
                ],
                "candidate_camera_recommendations": [],
                "reconciliation_notes_vs_production": "",
            },
        },
    }))
    w17a = tmp_path / "w17a_fake"
    w17a.mkdir()
    (w17a / "floor_plan_image_prompt.json").write_text(json.dumps({
        "floor_plan_image_prompt_by_fp": {
            "fp_l05_01": {
                "t2i_prompt_text": "Plan with #1 #2 #3.",
                "numbered_marker_legend": [
                    {"marker_number": 1, "source_candidate_number": 1,
                     "element_kind": "spatial_unit",
                     "visual_encoding": "filled_area",
                     "label": "u1 area", "priority": "must_show",
                     "source_refs": [], "must_be_legible": True},
                    {"marker_number": 2, "source_candidate_number": 2,
                     "element_kind": "spatial_unit",
                     "visual_encoding": "filled_area",
                     "label": "u2 area", "priority": "must_show",
                     "source_refs": [], "must_be_legible": True},
                    {"marker_number": 3, "source_candidate_number": 3,
                     "element_kind": "spatial_unit",
                     "visual_encoding": "filled_area",
                     "label": "u3 area", "priority": "must_show",
                     "source_refs": [], "must_be_legible": True},
                ],
            },
        },
    }))
    (w17a / "run_meta.json").write_text(json.dumps({
        "run_id": "w17a_fake",
        "stage": "w17a_floor_plan_image_prompt_slice",
        "run_status": "succeeded", "exit_code": 0,
        "stage_status": "generated", "model_used": "gpt-5.5",
        "args": {"derive_image_prompt_from": str(w15e)},
        "derived_from": w15e.name,
    }))


def test_w17c_dry_run_makes_no_api_call_and_writes_no_readback(tmp_path):
    """Default (no --generate) is a pure dry-run: no VLM call, no image
    API call, vlm_api_call_count == 0, readback file is a stub."""
    import experiment_floor_plan_vlm_readback_slice as mod

    _seed_w17a_with_w15e(tmp_path)
    src = _seed_w17b3_run(tmp_path)
    out_root = tmp_path / "out"
    exit_code = mod.main([
        "--derive-readback-from", str(src),
        "--target-fp-ids", "fp_l05_01",
        "--output-root", str(out_root),
    ])
    assert exit_code == 0
    run_dir = sorted(out_root.iterdir())[0]
    meta = json.loads((run_dir / "run_meta.json").read_text())
    assert meta["run_status"] == "succeeded"
    assert meta["stage_status"] == "dry_run"
    assert meta["vlm_api_call_count"] == 0
    assert meta.get("image_api_call_count", 0) == 0


def test_w17c_schema_shape_validation(tmp_path, monkeypatch):
    """Faked VLM caller returns a synthetic readback. The validator must
    accept the well-formed shape and produce a comparison report keyed
    by marker_number. Missing or extra markers must appear in
    `readback_conflicts[]` or `missing_or_ambiguous_markers[]`."""
    import experiment_floor_plan_vlm_readback_slice as mod

    _seed_w17a_with_w15e(tmp_path)
    src = _seed_w17b3_run(tmp_path)
    monkeypatch.setenv("OPENAI_API_KEY", "sk-test-fake-key")
    monkeypatch.setattr(mod, "_load_backend_env", lambda: None, raising=False)

    def _fake_vlm(*, llm_input, model, retry_once=False):
        # Well-formed partition (1+2 read, 3 missing) is the well-behaved
        # baseline case — coverage invariant PASSES.
        return {
            "read_markers": [
                {"marker_number": 1, "visible": True, "confidence": "high",
                 "approximate_region_or_cell": "central core",
                 "observed_unit_label_or_area": "u1 area",
                 "notes": ""},
                {"marker_number": 2, "visible": True, "confidence": "medium",
                 "approximate_region_or_cell": "northwest",
                 "observed_unit_label_or_area": "u2 area",
                 "notes": ""},
            ],
            "missing_or_ambiguous_markers": [
                {"marker_number": 3, "reason": "marker not visible"}
            ],
            "unit_boundary_summary": "three rooms; central + nw + east",
            "readback_conflicts": [],
        }
    monkeypatch.setattr(mod, "_vlm_caller", _fake_vlm, raising=False)

    out_root = tmp_path / "out_gen"
    exit_code = mod.main([
        "--derive-readback-from", str(src),
        "--target-fp-ids", "fp_l05_01",
        "--output-root", str(out_root),
        "--generate",
    ])
    assert exit_code == 0
    run_dir = sorted(out_root.iterdir())[0]
    meta = json.loads((run_dir / "run_meta.json").read_text())
    assert meta["stage_status"] == "generated"
    assert meta["vlm_api_call_count"] == 1
    assert meta["image_api_call_count"] == 0
    readback = json.loads((run_dir / "vlm_readback.json").read_text())
    assert isinstance(readback.get("read_markers"), list)
    assert isinstance(readback.get("missing_or_ambiguous_markers"), list)
    assert "unit_boundary_summary" in readback
    assert "readback_conflicts" in readback
    cmp_path = run_dir / "vlm_readback_comparison_report.json"
    cmp_rep = json.loads(cmp_path.read_text())
    inv = cmp_rep["invariants"]
    # Schema-shape invariant must pass; partition invariant must pass
    # because marker 3 IS covered by the missing list.
    assert inv["vlm_output_schema_shape_valid"]["pass"] is True
    assert inv["read_markers_partition_full_legend"]["pass"] is True
    detail = inv["read_markers_partition_full_legend"]["detail"]
    assert detail["read_count"] == 2 and detail["missing_count"] == 1

    # Now flip to a misbehaved case: marker 3 missing from BOTH lists
    # (unaccounted) AND an extra marker 4 invented → partition FAILS.
    def _fake_vlm_bad(*, llm_input, model, retry_once=False):
        return {
            "read_markers": [
                {"marker_number": 1, "visible": True, "confidence": "high",
                 "approximate_region_or_cell": "central core",
                 "observed_unit_label_or_area": "u1 area",
                 "notes": ""},
                {"marker_number": 4, "visible": True, "confidence": "low",
                 "approximate_region_or_cell": "south",
                 "observed_unit_label_or_area": "invented area",
                 "notes": "marker_number invented by VLM"},
            ],
            "missing_or_ambiguous_markers": [],
            "unit_boundary_summary": "",
            "readback_conflicts": [],
        }
    monkeypatch.setattr(mod, "_vlm_caller", _fake_vlm_bad, raising=False)
    out_root2 = tmp_path / "out_gen_bad"
    exit_code2 = mod.main([
        "--derive-readback-from", str(src),
        "--target-fp-ids", "fp_l05_01",
        "--output-root", str(out_root2),
        "--generate",
    ])
    cmp_bad = json.loads(
        sorted(out_root2.iterdir())[0]
        .joinpath("vlm_readback_comparison_report.json").read_text()
    )
    inv_bad = cmp_bad["invariants"]
    assert inv_bad["read_markers_partition_full_legend"]["pass"] is False
    bad_detail = inv_bad["read_markers_partition_full_legend"]["detail"]
    assert 2 in bad_detail.get("missing_marker_numbers", []) \
        and 3 in bad_detail.get("missing_marker_numbers", [])
    assert 4 in bad_detail.get("extra_marker_numbers", [])


def test_w17c_methodology_grep_no_scenario_specific_static_tokens():
    """Static script body must not embed scenario-specific tokens.
    Per-char assembly so this assertion source does not match itself."""
    script_path = _SCRIPTS_DIR / "experiment_floor_plan_vlm_readback_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", "su" + "ri-young",
    ]
    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 ''}"
    )
