"""W17E downstream payload preview tests — minimal coverage.

Codex specified 3 tests only:
1. full coverage + guard_level shape
2. synthetic missing/conflict markers are escalated to BLOCKING
3. production/API guard + 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_w17e_chain(tmp_path: Path, *,
                     extra_synthesis_mut=None,
                     extra_readback_mut=None):
    """Build a minimal W17A → W17B3 → W17C → W17D chain on disk so the
    W17E script can walk it. Pass mutation callables to introduce
    missing/conflict scenarios."""
    w15e = tmp_path / "w15e"
    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": n, "label": f"u{n} area", "category": "area",
                     "position_hint": f"region {n}",
                     "unit_id_pointer": f"U{n}"}
                    for n in (1, 2, 3)
                ],
                "candidate_camera_recommendations": [],
                "reconciliation_notes_vs_production": "",
            },
        },
    }))
    w17a = tmp_path / "w17a"
    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": n, "source_candidate_number": n,
                     "element_kind": "spatial_unit",
                     "visual_encoding": "filled_area",
                     "label": f"u{n} area", "priority": "must_show",
                     "source_refs": [], "must_be_legible": True}
                    for n in (1, 2, 3)
                ],
            },
        },
    }))
    (w17a / "run_meta.json").write_text(json.dumps({
        "run_id": "w17a", "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,
    }))
    w17b3 = tmp_path / "w17b3"
    (w17b3 / "png").mkdir(parents=True)
    (w17b3 / "png" / "fp_l05_01.png").write_bytes(b"PNGPLACEHOLDER")
    (w17b3 / "run_meta.json").write_text(json.dumps({
        "run_id": "w17b3",
        "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(w17a)},
        "derived_from": w17a.name,
        "png_relative_path": "png/fp_l05_01.png",
    }))
    w17c = tmp_path / "w17c"
    w17c.mkdir()
    readback = {
        "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": "high",
             "approximate_region_or_cell": "northwest",
             "observed_unit_label_or_area": "u2 area", "notes": ""},
            {"marker_number": 3, "visible": True, "confidence": "high",
             "approximate_region_or_cell": "east",
             "observed_unit_label_or_area": "u3 area", "notes": ""},
        ],
        "missing_or_ambiguous_markers": [],
        "unit_boundary_summary": "three rooms",
        "readback_conflicts": [],
    }
    if extra_readback_mut:
        extra_readback_mut(readback)
    (w17c / "vlm_readback.json").write_text(json.dumps(readback))
    (w17c / "run_meta.json").write_text(json.dumps({
        "run_id": "w17c",
        "stage": "w17c_floor_plan_vlm_readback_slice",
        "run_status": "succeeded", "exit_code": 0,
        "stage_status": "generated", "model_used": "gpt-5.5",
        "vlm_api_call_count": 1, "image_api_call_count": 0,
        "args": {"derive_readback_from": str(w17b3)},
        "derived_from": w17b3.name,
        "target_fp_ids": ["fp_l05_01"],
    }))
    w17d = tmp_path / "w17d"
    w17d.mkdir()
    synthesis = {
        "synthesis_by_fp": {
            "fp_l05_01": {
                "fp_id": "fp_l05_01",
                "markers": [
                    {"marker_number": n, "label": f"u{n} area",
                     "kind": "spatial_unit",
                     "visual_encoding": "filled_area",
                     "expected_unit": f"U{n}",
                     "expected_placement": f"region {n}",
                     "visible": True, "confidence": "high",
                     "observed_unit_label_or_area": f"u{n} area",
                     "approximate_region_or_cell": f"region {n}",
                     "notes": "",
                     "heuristic_unit_match": True,
                     "bucket": "matched"}
                    for n in (1, 2, 3)
                ],
                "unit_boundary_summary": "three rooms",
                "summary_counts": {
                    "expected_total": 3, "matched": 3,
                    "unit_mismatch": 0, "missing": 0,
                },
            },
        },
    }
    mismatch = {
        "mismatch_by_fp": {
            "fp_l05_01": {
                "fp_id": "fp_l05_01",
                "unit_mismatch_markers": [],
                "vlm_reported_conflicts": [],
                "missing_marker_count": 0,
                "matched_marker_count": 3,
                "unit_mismatch_marker_count": 0,
                "diagnostic_note": "heuristic only",
            },
        },
    }
    if extra_synthesis_mut:
        extra_synthesis_mut(synthesis, mismatch)
    (w17d / "coarse_layout_synthesis.json").write_text(json.dumps(synthesis))
    (w17d / "mismatch_report.json").write_text(json.dumps(mismatch))
    (w17d / "run_meta.json").write_text(json.dumps({
        "run_id": "w17d",
        "stage": "w17d_floor_plan_readback_synthesis_slice",
        "run_status": "succeeded", "exit_code": 0,
        "args": {"derive_synthesis_from": str(w17c)},
        "derived_from": w17c.name,
        "target_fp_ids": ["fp_l05_01"],
    }))
    return w17d


def test_w17e_full_coverage_and_guard_level_shape(tmp_path):
    """Baseline (all markers matched + high confidence) — every marker
    must appear in the payload preview with guard_level=OK and a
    non-empty guard_reason. No LLM/VLM/image API calls."""
    import experiment_floor_plan_downstream_payload_preview_slice as mod

    w17d = _seed_w17e_chain(tmp_path)
    out_root = tmp_path / "out"
    exit_code = mod.main([
        "--derive-payload-from", str(w17d),
        "--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["vlm_api_call_count"] == 0
    assert meta["image_api_call_count"] == 0
    assert meta.get("llm_api_call_count", 0) == 0

    payload = json.loads(
        (run_dir / "downstream_floor_plan_payload_preview.json").read_text()
    )
    fp = payload["payload_by_fp"]["fp_l05_01"]
    markers = fp["markers"]
    assert {m["marker_number"] for m in markers} == {1, 2, 3}
    for m in markers:
        # Shape: all required fields present.
        for key in (
            "marker_number", "label", "element_kind", "visual_encoding",
            "expected_unit", "expected_placement",
            "observed_unit_label_or_area", "approximate_region_or_cell",
            "visible", "confidence", "bucket", "guard_level",
            "guard_reason", "source_truth", "observation_source",
        ):
            assert key in m, f"missing key {key} in marker {m['marker_number']}"
        # Provenance constants.
        assert m["source_truth"] == "w15e_candidate"
        assert m["observation_source"] == "w17d_synthesis"
        # Baseline → OK guard with non-empty reason.
        assert m["guard_level"] == "OK"
        assert m["guard_reason"]

    # Guard consumption dry-run trace present.
    trace = json.loads(
        (run_dir / "downstream_guard_consumption_dry_run.json").read_text()
    )
    fp_trace = trace["consumption_by_fp"]["fp_l05_01"]
    assert fp_trace["BLOCKING_count"] == 0
    assert fp_trace["OK_count"] == 3


def test_w17e_synthetic_missing_or_conflict_escalates_to_blocking(tmp_path):
    """Missing markers, VLM-reported conflict, and must_show + bucket≠matched
    all map to BLOCKING per Codex spec."""
    import experiment_floor_plan_downstream_payload_preview_slice as mod

    def _mut(synthesis, mismatch):
        fp = synthesis["synthesis_by_fp"]["fp_l05_01"]
        # Marker 1: keep as matched/high (→ OK).
        # Marker 2: flip to missing (priority must_show + bucket != matched).
        for m in fp["markers"]:
            if m["marker_number"] == 2:
                m["bucket"] = "missing"
                m["visible"] = False
                m["confidence"] = "unknown"
                m["observed_unit_label_or_area"] = ""
                m["approximate_region_or_cell"] = ""
                m["heuristic_unit_match"] = False
        fp["summary_counts"]["matched"] = 2
        fp["summary_counts"]["missing"] = 1
        mismatch["mismatch_by_fp"]["fp_l05_01"]["missing_marker_count"] = 1
        # Marker 3: VLM-reported conflict (separate from missing branch).
        mismatch["mismatch_by_fp"]["fp_l05_01"]["vlm_reported_conflicts"].append({
            "marker_number": 3, "expected_unit": "U3",
            "observed_unit_label_or_area": "wrong area",
            "note": "vlm-reported conflict",
        })

    w17d = _seed_w17e_chain(tmp_path, extra_synthesis_mut=_mut)
    out_root = tmp_path / "out_blocking"
    exit_code = mod.main([
        "--derive-payload-from", str(w17d),
        "--target-fp-ids", "fp_l05_01",
        "--output-root", str(out_root),
    ])
    assert exit_code == 0
    run_dir = sorted(out_root.iterdir())[0]
    payload = json.loads(
        (run_dir / "downstream_floor_plan_payload_preview.json").read_text()
    )
    by_n = {m["marker_number"]: m for m in
            payload["payload_by_fp"]["fp_l05_01"]["markers"]}
    assert by_n[1]["guard_level"] == "OK"
    assert by_n[2]["guard_level"] == "BLOCKING"
    assert "missing" in by_n[2]["guard_reason"].lower() \
        or "must_show" in by_n[2]["guard_reason"].lower() \
        or "visible" in by_n[2]["guard_reason"].lower()
    assert by_n[3]["guard_level"] == "BLOCKING"
    assert "conflict" in by_n[3]["guard_reason"].lower() \
        or "vlm" in by_n[3]["guard_reason"].lower()

    # Compatibility invariant 4 must surface this: missing/conflict ↔ BLOCKING.
    report = json.loads(
        (run_dir / "w17e_compatibility_report.json").read_text()
    )
    inv = report["invariants"]
    assert inv["blocking_guard_matches_missing_or_conflict_cases"]["pass"] is True
    trace = json.loads(
        (run_dir / "downstream_guard_consumption_dry_run.json").read_text()
    )
    fp_trace = trace["consumption_by_fp"]["fp_l05_01"]
    assert fp_trace["BLOCKING_count"] == 2


def test_w17e_methodology_grep_and_production_guard(tmp_path):
    """Production diff + no API call invariant; methodology grep."""
    import experiment_floor_plan_downstream_payload_preview_slice as mod

    w17d = _seed_w17e_chain(tmp_path)
    out_root = tmp_path / "out_guard"
    exit_code = mod.main([
        "--derive-payload-from", str(w17d),
        "--target-fp-ids", "fp_l05_01",
        "--output-root", str(out_root),
    ])
    assert exit_code == 0
    run_dir = sorted(out_root.iterdir())[0]
    report = json.loads(
        (run_dir / "w17e_compatibility_report.json").read_text()
    )
    inv = report["invariants"]
    assert inv["production_diff_zero_db_write_zero_vlm_zero_image_zero"]["pass"] is True

    script_path = _SCRIPTS_DIR / "experiment_floor_plan_downstream_payload_preview_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 ''}"
    )
