"""W21B-wave-4 C4 follow-up — camera_recommendation transient-marker sanitize.

Codex C4 gate Required: the camera recommendation block (shared by the
overlay-path packs v7-v12) can surface a transient marker-number cue into the
LLM input surface (e.g. ``framing_notes: ... depict number 18 as a transient
restocking cue ...``). The final t2i_prompt was clean, but a transient marker
must never reach the BG-plate prompt surface.

Fix is fail-closed and uses ONLY the structured transient marker-number set
from the overlay payload (the same self-defined marker-number token check C2's
``find_plate_prose_leaks`` uses — NOT lexical/semantic parsing, Korean
untouched). Base-marker number references stay (Rule 7 — the LLM translates
them); only fields that cite a TRANSIENT marker number are dropped.
"""
from __future__ import annotations

from app.modules.pipeline.background_prompt import (
    _extract_transient_marker_numbers,
    _format_camera_recommendation_block,
    build_bg_user_prompt,
)


_OVERLAY = {
    "bg_id": "L09B01",
    "fp_id": "fp_l09_main",
    "use_numbered_elements": [2, 4],
    "ignore_numbered_elements": [],
    "base_markers_to_reference": [
        {"number": 2, "label": "entrance threshold", "category": "opening",
         "base_layer_decision": "base_structural_unit",
         "render_role": "structural_skeleton", "top_down_legibility": "clear"},
        {"number": 4, "label": "staff display area", "category": "area",
         "base_layer_decision": "base_structural_unit",
         "render_role": "structural_skeleton", "top_down_legibility": "clear"},
    ],
    "transient_markers_to_describe": [
        {"number": 18, "label": "restocking cue", "category": "prop",
         "base_layer_decision": "state_overlay_plot_cue",
         "render_role": "not_applicable", "top_down_legibility": "not_applicable"},
    ],
    "ignored_state_overlay_markers": [
        {"number": 21, "label": "spilled item", "category": "prop"},
    ],
    "target_unit_marker_numbers": [2, 4],
    "dominant_target_unit_marker_number": None,
    "clean_background_expected": False,
    "diagnostics": [],
}

_CAM_WITH_TRANSIENT = {
    "bg_id": "L09B01",
    "camera_position": "near number 2 (entrance), facing toward number 4 (staff display)",
    "camera_height": "eye-level ~1.6m",
    "lens_hint": "35mm wide angle",
    "framing_notes": "keep entrance in foreground, and depict number 18 as a transient restocking cue for this background only",
}

_CAM_CLEAN = {
    "bg_id": "L09B01",
    "camera_position": "near number 2 (entrance), facing toward number 4 (staff display)",
    "camera_height": "eye-level ~1.6m",
    "lens_hint": "35mm wide angle",
    "framing_notes": "keep entrance in foreground, staff display area prominent",
}


def test_extract_transient_marker_numbers_from_overlay():
    nums = _extract_transient_marker_numbers(_OVERLAY)
    # transient_markers_to_describe (18) + ignored_state_overlay_markers (21).
    assert nums == frozenset({18, 21})
    # base markers (2, 4) are NOT transient.
    assert 2 not in nums and 4 not in nums


def test_extract_transient_safe_on_missing():
    assert _extract_transient_marker_numbers(None) == frozenset()
    assert _extract_transient_marker_numbers({}) == frozenset()


def test_camera_block_drops_field_citing_transient_number():
    block = _format_camera_recommendation_block(
        _CAM_WITH_TRANSIENT, transient_numbers=frozenset({18, 21})
    )
    # the transient cue (number 18) must NOT appear anywhere in the block.
    assert "number 18" not in block
    # narrow to the transient cue phrase — a base-marker label could
    # legitimately contain "restocking" elsewhere (Codex cleanup).
    assert "transient restocking cue" not in block
    # base-marker references (number 2 / number 4) are preserved (Rule 7).
    assert "number 2" in block
    assert "number 4" in block
    # height / lens always kept.
    assert "eye-level ~1.6m" in block
    assert "35mm wide angle" in block


def test_camera_block_clean_rec_is_byte_identical_without_transient_set():
    """A rec that cites no transient number is unchanged vs the no-arg call."""
    before = _format_camera_recommendation_block(_CAM_CLEAN)
    after = _format_camera_recommendation_block(
        _CAM_CLEAN, transient_numbers=frozenset({18, 21})
    )
    assert before == after
    assert "staff display area prominent" in after


def test_camera_block_no_transient_set_unchanged():
    """Default (empty transient set, e.g. v6 no-overlay path) → no sanitize."""
    block = _format_camera_recommendation_block(_CAM_WITH_TRANSIENT)
    # without a transient set the legacy verbatim behavior is preserved.
    assert "number 18" in block


def test_build_v12_prompt_strips_transient_camera_cue():
    spec = {
        "bg_id": "L09B01", "loc_id": "L09",
        "sub_location": "mart interior", "state_label_raw": "dusk interior",
        "surface_role": "interior_room", "applies_to_shots": ["S8_Shot2"],
    }
    rendered = build_bg_user_prompt(
        bg_spec=spec, floor_plan_path="/tmp/fp_l09_main.png",
        prior_bg_paths=[], scene_segments=[],
        visual_world_rules="regional rule",
        camera_recommendation=_CAM_WITH_TRANSIENT,
        source_language="ko", prompt_version="12",
        overlay_payload=_OVERLAY, projection_plate_prose=None,
    )
    # transient marker cue gone from the LLM input surface …
    assert "number 18" not in rendered
    assert "transient restocking cue" not in rendered
    # … but base-marker camera framing kept.
    assert "number 2" in rendered
