"""W17B floor-plan image smoke slice — minimal safety tests only.

Codex specified: "테스트 너무 강하게 늘리지 말 것". This file keeps three
safety-only checks (dry-run safety, caller monkeypatch seam, methodology
grep). No coverage tests, no enum tests, no image-rendering tests.
"""
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_w17a_run(run_dir: Path) -> Path:
    """Create a minimal W17A success run dir layout the W17B loader needs."""
    src = run_dir / "w17a_fake"
    src.mkdir(parents=True, exist_ok=True)
    (src / "floor_plan_image_prompt.json").write_text(json.dumps({
        "floor_plan_image_prompt_by_fp": {
            "fp_l05_01": {
                "t2i_prompt_text": (
                    "Schematic plan. Markers #1 #2 #3. Do not renumber, "
                    "omit, or invent."
                ),
                "numbered_marker_legend": [
                    {"marker_number": 1, "source_candidate_number": 1,
                     "element_kind": "spatial_unit",
                     "visual_encoding": "filled_area",
                     "label": "u1", "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", "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", "priority": "must_show",
                     "source_refs": [], "must_be_legible": True},
                ],
            },
        },
    }))
    (src / "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",
    }))
    return src


def test_w17b_dry_run_makes_no_api_call_and_writes_no_png(tmp_path):
    """Default (no --generate) must be a pure dry-run: no PNG, no openai
    import, image_api_call_count == 0."""
    import experiment_floor_plan_image_smoke_slice as mod

    src = _seed_w17a_run(tmp_path)
    out_root = tmp_path / "out"
    exit_code = mod.main([
        "--derive-image-smoke-from", str(src),
        "--target-fp-ids", "fp_l05_01",
        "--output-root", str(out_root),
    ])
    assert exit_code == 0
    runs = sorted(out_root.iterdir())
    assert len(runs) == 1
    run_dir = runs[0]
    meta = json.loads((run_dir / "run_meta.json").read_text())
    assert meta["run_status"] == "succeeded"
    assert meta["stage_status"] == "dry_run"
    assert meta["image_api_call_count"] == 0
    assert meta["image_generation_count"] == 0
    assert not (run_dir / "png").exists() or not any((run_dir / "png").iterdir())


def test_w17b_generate_uses_monkeypatched_caller_once(tmp_path, monkeypatch):
    """--generate must (a) refuse to run when OPENAI_API_KEY is absent
    and the openai SDK import is skipped, (b) when patched with a fake
    caller, invoke it EXACTLY ONCE for fp_l05_01, (c) write the PNG
    bytes returned by the caller to run-local png/."""
    import experiment_floor_plan_image_smoke_slice as mod

    # Fail-closed: no API key set AND `_load_backend_env` patched to a
    # no-op so the test does not silently pick up `.env`. --generate must
    # then exit with validation_failed.
    monkeypatch.delenv("OPENAI_API_KEY", raising=False)
    monkeypatch.setattr(mod, "_load_backend_env", lambda: None, raising=False)
    src = _seed_w17a_run(tmp_path)
    out_root = tmp_path / "out_nokey"
    exit_code = mod.main([
        "--derive-image-smoke-from", str(src),
        "--target-fp-ids", "fp_l05_01",
        "--output-root", str(out_root),
        "--generate",
    ])
    assert exit_code == 1
    meta_nokey = json.loads(
        sorted(out_root.iterdir())[0].joinpath("run_meta.json").read_text()
    )
    assert meta_nokey["run_status"] == "validation_failed"
    assert "openai_api_key" in str(meta_nokey.get("failed_invariants", [])).lower() \
        or "OPENAI_API_KEY" in str(meta_nokey)

    # Now patch a fake caller. The script must call it exactly once.
    monkeypatch.setenv("OPENAI_API_KEY", "sk-test-fake-key")
    attempts = {"count": 0}

    def _fake_caller(*, fp_id, prompt, model, size, quality, target_path, client):
        attempts["count"] += 1
        target_path.parent.mkdir(parents=True, exist_ok=True)
        target_path.write_bytes(b"\x89PNG\r\n\x1a\nFAKEW17B")
        return {
            "status": "success",
            "png_size_bytes": target_path.stat().st_size,
            "actual_api_response_meta": {"latency_ms": 5},
            "error_meta": {},
            "cost_meta": {},
        }

    monkeypatch.setattr(mod, "_openai_caller", _fake_caller, raising=False)
    monkeypatch.setattr(mod, "_create_openai_client", lambda: object(), raising=False)

    out_root2 = tmp_path / "out_gen"
    exit_code2 = mod.main([
        "--derive-image-smoke-from", str(src),
        "--target-fp-ids", "fp_l05_01",
        "--output-root", str(out_root2),
        "--generate",
    ])
    assert exit_code2 == 0
    assert attempts["count"] == 1, (
        f"expected exactly 1 openai call, got {attempts['count']}"
    )
    run_dir = sorted(out_root2.iterdir())[0]
    meta = json.loads((run_dir / "run_meta.json").read_text())
    assert meta["run_status"] == "succeeded"
    assert meta["stage_status"] == "generated"
    assert meta["image_api_call_count"] == 1
    assert meta["image_generation_count"] == 1
    png_path = run_dir / "png" / "fp_l05_01.png"
    assert png_path.exists() and png_path.stat().st_size > 0


def test_w17b2_dry_run_artifact_carries_marker_mapping_block(tmp_path):
    """Dry-run artifact (generated_image_meta.json) must surface the
    assembled image prompt with a deterministic marker mapping block.
    W17B3 extension: each mapping line must also carry `unit=...` and
    `placement=...` columns sourced from the W15e candidate (chained via
    W17A run_meta.args.derive_image_prompt_from). image_api_call_count
    must remain 0."""
    import experiment_floor_plan_image_smoke_slice as mod

    # Seed a fake W15e candidate dir AND a W17A run that points at it.
    w15e_dir = tmp_path / "w15e_fake"
    w15e_dir.mkdir()
    (w15e_dir / "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": "northwest run",
                     "unit_id_pointer": "U2"},
                    {"number": 3, "label": "u3 area", "category": "area",
                     "position_hint": "east enclosed",
                     "unit_id_pointer": "U3"},
                ],
                "candidate_camera_recommendations": [],
                "reconciliation_notes_vs_production": "",
            },
        },
    }))
    src = tmp_path / "w17a_fake_chained"
    src.mkdir()
    (src / "floor_plan_image_prompt.json").write_text(json.dumps({
        "floor_plan_image_prompt_by_fp": {
            "fp_l05_01": {
                "t2i_prompt_text": (
                    "Schematic plan. Markers #1 #2 #3. Do not renumber, "
                    "omit, or invent."
                ),
                "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},
                ],
            },
        },
    }))
    (src / "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_dir)},
        "derived_from": w15e_dir.name,
    }))

    out_root = tmp_path / "out_mapping"
    exit_code = mod.main([
        "--derive-image-smoke-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["image_api_call_count"] == 0
    art = json.loads((run_dir / "generated_image_meta.json").read_text())
    fp = art["fp_smoke_result"]
    assembled = fp.get("assembled_prompt") or ""
    # Mapping block lines per W17A legend entry, with placement columns
    # from the W15e candidate join.
    for n in (1, 2, 3):
        assert f"#{n} =" in assembled, (
            f"assembled prompt missing mapping line for #{n}"
        )
    assert "kind=" in assembled and "visual=" in assembled
    # W17B3 additions: room-local unit + placement.
    assert "unit=" in assembled
    assert "placement=" in assembled
    assert "unit=U1" in assembled and "unit=U2" in assembled and "unit=U3" in assembled
    assert "placement=central core" in assembled
    # The high-priority marker contract banner must appear above the
    # original prompt body.
    assert "Marker mapping contract" in assembled
    # Side-legend discouragement clause must be present.
    assert "side legend" in assembled.lower() or "no independent" in assembled.lower()
    # Room-local placement guidance must be present (W17B3).
    assert (
        "room-local placement" in assembled.lower()
        or "inside or on the boundary" in assembled.lower()
    )


def test_w17b_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_image_smoke_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 ''}"
    )
