"""W21B-wave-1: background_prompt v8 opt-in — minimal focused tests.

LLM / image / VLM API call 0. DB / ImageAsset 0.

검사 범위 (deterministic only, brief 8절 + Codex revision):
  - PROMPT_VERSION_MAP / resolve_prompt_version 가 v8 pack 으로 해소.
  - settings.background_prompt_version Literal 이 "8" 을 수용.
  - v8 pack 의 system / user_template / schema 가 prompt_loader 로 로드.
  - v8 user_template 에 ``{scene_segments_block}`` 과 ``{transient_markers_block}``
    placeholder 모두 부재 (Codex revision — BG purity 강화로 transient
    marker label 도 prompt 본문에 노출 안 함).
  - v8 build path 는 v7 의 overlay_payload SOT 를 그대로 받지만 (validation
    유지) prompt 본문에는 base markers 만 surface. dirty overlay 의
    transient label, clean overlay 의 sentinel, ignored marker 모두 prompt
    안에 등장 안 함.
  - v8 source_language fallback 이 scene_segments 언급을 포함하지 않는다.
  - v8 schema 가 v7 와 동일 shape (출력 schema 변경 0 → SCHEMA_VERSION 유지).
"""
from __future__ import annotations

import pytest

from app.modules.pipeline.background_prompt import (
    PROMPT_VERSION_MAP,
    BackgroundPromptOverlayError,
    _CLEAN_BG_SENTINEL,
    build_bg_user_prompt,
    resolve_prompt_version,
)
from app.modules.prompt_loader import load_prompt, load_schema


_V8_PACK_NAME = "8.202605282046"


def _bg_spec():
    return {
        "bg_id": "L01B01",
        "loc_id": "L01",
        "sub_location": "primary_unit",
        "state_label_raw": "day_norm",
        "applies_to_shots": ["S1_Shot1"],
    }


_OVERLAY_DIRTY = {
    "bg_id": "L01B01",
    "fp_id": "fp_a",
    "use_numbered_elements": [1, 2],
    "ignore_numbered_elements": [3],
    "base_markers_to_reference": [
        {
            "number": 1,
            "label": "primary unit",
            "category": "area",
            "position_hint": "center",
            "base_layer_decision": "base_structural_unit",
        }
    ],
    "transient_markers_to_describe": [
        {
            "number": 2,
            "label": "scattered debris",
            "category": "prop",
            "position_hint": "north wall floor",
            "base_layer_decision": "state_overlay_transient_object",
        }
    ],
    "ignored_state_overlay_markers": [
        {
            "number": 3,
            "label": "ghost cue",
            "category": "prop",
            "position_hint": "east wall",
            "base_layer_decision": "state_overlay_plot_cue",
        }
    ],
    "target_unit_marker_numbers": [1],
    "dominant_target_unit_marker_number": 1,
    "clean_background_expected": False,
    "diagnostics": [],
}

_OVERLAY_CLEAN = {
    "bg_id": "L01B01",
    "fp_id": "fp_a",
    "use_numbered_elements": [1],
    "ignore_numbered_elements": [2],
    "base_markers_to_reference": [
        {
            "number": 1,
            "label": "primary unit",
            "category": "area",
            "position_hint": "center",
            "base_layer_decision": "base_structural_unit",
        }
    ],
    "transient_markers_to_describe": [],
    "ignored_state_overlay_markers": [
        {
            "number": 2,
            "label": "ghost cue",
            "category": "prop",
            "position_hint": "north wall",
            "base_layer_decision": "state_overlay_plot_cue",
        }
    ],
    "target_unit_marker_numbers": [1],
    "dominant_target_unit_marker_number": 1,
    "clean_background_expected": True,
    "diagnostics": [],
}


def test_v8_selector_resolves_pack_name():
    """PROMPT_VERSION_MAP / resolve_prompt_version 가 v8 selector 를 신규 pack
    디렉토리 이름으로 해소한다. v6 / v7 entry 는 보존."""
    assert PROMPT_VERSION_MAP["8"] == _V8_PACK_NAME
    assert resolve_prompt_version("8") == _V8_PACK_NAME
    assert PROMPT_VERSION_MAP["6"] == "6.202605091200"
    assert PROMPT_VERSION_MAP["7"] == "7.202605262330"


def test_v8_unknown_selector_still_fails_fast():
    """unknown selector 는 silent latest-pack 폴백 없이 ValueError.

    ("12" 는 W21B-wave-4 C4 에서 등록됨 → 아직 미등록인 "13" 으로 검사.)
    """
    with pytest.raises(ValueError, match="unknown background_prompt selector"):
        resolve_prompt_version("13")


def test_v8_config_literal_accepts_eight(monkeypatch):
    """settings.background_prompt_version Literal 이 '8' 수용. 기본값 '6' 유지.

    backend/.env / shell 가 BACKGROUND_PROMPT_VERSION 을 영구화한 환경에서는
    Settings 가 그 값을 읽어오므로 env 를 명시적으로 비운 뒤 class default 만
    검사한다 (W21B-w1 의 hard-flip 정책 — default 6 유지가 핵심)."""
    monkeypatch.delenv("BACKGROUND_PROMPT_VERSION", raising=False)
    from app.core.config import Settings

    s_default = Settings(_env_file=None)
    assert s_default.background_prompt_version == "6"
    s_v8 = Settings(_env_file=None, background_prompt_version="8")
    assert s_v8.background_prompt_version == "8"


def test_v8_pack_files_load_and_have_required_placeholders():
    """v8 pack 의 system / user_template / schema 가 prompt_loader 경유 로드.

    user_template 에 v8 design intent 가 반영되어 있는지 확인:
      - {scene_segments_block} placeholder 가 제거되었어야 한다 (Codex 결정 3).
      - {transient_markers_block} placeholder 도 제거되었어야 한다 (Codex
        revision — BG purity 강화로 transient marker label 자체를 prompt
        본문에 노출하지 않는다).
      - base markers / camera / applies_to_shots / world rules placeholder
        는 보존 (overlay SOT validation 은 그대로 유지하므로 base 만 surface).
    """
    user_template = load_prompt(
        "background_prompt", "user_template", version=_V8_PACK_NAME
    )
    # Codex 결정 3: scene narrative verbatim inject 차단.
    assert "{scene_segments_block}" not in user_template
    # Codex revision: transient marker label 자체 노출 차단.
    assert "{transient_markers_block}" not in user_template
    # base markers / 기타 placeholder 는 유지 — overlay SOT 의 base layer 와
    # 주변 context (camera / shots / world rules) 만 prompt 본문에 surface.
    assert "{base_markers_block}" in user_template
    assert "{camera_recommendation_block}" in user_template
    assert "{applies_to_shots_block}" in user_template
    assert "{visual_world_rules}" in user_template

    system = load_prompt("background_prompt", "system", version=_V8_PACK_NAME)
    # v8 design — cinematic photoreal plate + BG purity 명시.
    assert "background plate" in system.lower()
    # negative noun pile-up 의존 차단 정책이 system 안에 명시되어 있다 (brief
    # 4.2 — positive-first description). 단어 자체보다 정책 표현을 검사.
    assert "positive" in system.lower()
    # Codex revision: transient marker withholding 정책이 system 에 명시.
    assert "withheld" in system.lower()


def test_v8_schema_keeps_v7_output_shape():
    """v8 schema 가 v7 와 동일 required field 5 개 유지 → background_prompt_step
    SCHEMA_VERSION 유지 가능 (출력 shape unchanged)."""
    schema = load_schema("background_prompt", "schema", version=_V8_PACK_NAME)
    required = set(schema.get("required") or [])
    assert required == {
        "bg_id",
        "t2i_prompt",
        "ref_guide",
        "shot_guides",
        "objects_owned_by_background",
    }
    # bg_id pattern 은 D6 v6 와 동일 (W21B-w1 에서 변경 없음).
    assert schema["properties"]["bg_id"]["pattern"] == r"^L\d{2,3}B\d{2,3}$"


def test_v8_missing_overlay_raises():
    """v8 build path 는 v7 와 동일 overlay payload SOT — None 입력 시 fail-closed."""
    with pytest.raises(BackgroundPromptOverlayError, match="received None"):
        build_bg_user_prompt(
            bg_spec=_bg_spec(),
            floor_plan_path="/p/fp.png",
            prior_bg_paths=[],
            scene_segments=[],
            visual_world_rules="",
            source_language="ko",
            prompt_version="8",
            overlay_payload=None,
        )


def test_v8_dirty_renders_base_but_withholds_transient_label():
    """v8 dirty overlay → base marker 는 prompt 본문에 노출되지만 transient
    label / ignored label 어떤 것도 본문에 누출되지 않는다 (Codex revision —
    BG purity 강화)."""
    rendered = build_bg_user_prompt(
        bg_spec=_bg_spec(),
        floor_plan_path="/p/fp.png",
        prior_bg_paths=[],
        scene_segments=[],
        visual_world_rules="",
        source_language="ko",
        prompt_version="8",
        overlay_payload=_OVERLAY_DIRTY,
    )
    # base marker 는 본문에 등장.
    assert "1. primary unit [area] — center" in rendered
    # transient marker label / position_hint 어떤 것도 본문에 누설 X.
    assert "scattered debris" not in rendered
    assert "north wall floor" not in rendered
    # ignored marker 도 마찬가지.
    assert "ghost cue" not in rendered
    # transient block placeholder 자체가 user_template 에 없으므로 raw
    # placeholder 가 본문에 남지 않아야 한다.
    assert "{transient_markers_block}" not in rendered


def test_v8_clean_overlay_does_not_emit_sentinel():
    """v8 clean overlay → clean sentinel / ignored marker 모두 prompt 본문에
    노출되지 않는다 (Codex revision — v8 은 transient 섹션 자체를 prompt 에
    내보내지 않으므로 sentinel 도 등장하지 않아야 한다)."""
    rendered = build_bg_user_prompt(
        bg_spec=_bg_spec(),
        floor_plan_path="/p/fp.png",
        prior_bg_paths=[],
        scene_segments=[],
        visual_world_rules="",
        source_language="ko",
        prompt_version="8",
        overlay_payload=_OVERLAY_CLEAN,
    )
    # base marker 는 그대로 surface.
    assert "1. primary unit [area] — center" in rendered
    # v7 의 clean sentinel 텍스트가 v8 prompt 본문에 등장해선 안 된다.
    assert _CLEAN_BG_SENTINEL not in rendered
    # ignored marker / transient label 누설 X.
    assert "ghost cue" not in rendered
    assert "{transient_markers_block}" not in rendered


def test_v8_does_not_inject_scene_segments_block():
    """user_template 에 {scene_segments_block} placeholder 가 없으므로 build 결과
    에 scene 본문이 verbatim 으로 등장하지 않는다 (Codex 결정 3 + brief 4.2).

    build_bg_user_prompt 가 내부에서 여전히 seg_block 을 계산하지만,
    template 의 .replace() 가 unused placeholder 를 no-op 으로 처리하므로
    렌더된 prompt body 에 scene narrative 가 누출되지 않는 것을 확인.
    """
    scene_text_marker = "VERBATIM_SCENE_PROBE_LINE"
    rendered = build_bg_user_prompt(
        bg_spec=_bg_spec(),
        floor_plan_path="/p/fp.png",
        prior_bg_paths=[],
        scene_segments=[
            {
                "scene_index": 1,
                "heading": "probe heading",
                "text": scene_text_marker,
            }
        ],
        visual_world_rules="",
        source_language="ko",
        prompt_version="8",
        overlay_payload=_OVERLAY_CLEAN,
    )
    assert scene_text_marker not in rendered
    # placeholder 자체도 raw 로 남아서는 안 됨 (.replace() 가 호출되지 않은
    # 흔적이 prompt body 에 남으면 LLM 이 placeholder 를 그대로 인용).
    assert "{scene_segments_block}" not in rendered


def test_v8_source_language_fallback_excludes_scene_segments_mention():
    """v8 의 source_language fallback 은 scene_segments 를 가리키지 않는다.

    v6/v7 fallback 은 ``(unknown — derive from scene_segments)`` 표현을
    그대로 유지하지만, v8 은 scene_segments section 자체가 user_template
    에서 빠졌으므로 같은 fallback 을 쓰면 LLM 이 존재하지 않는 section 을
    참조하게 된다 (Codex revision). v8 fallback 은 visual_world_rules +
    주변 context 를 가리켜야 한다.
    """
    rendered = build_bg_user_prompt(
        bg_spec=_bg_spec(),
        floor_plan_path="/p/fp.png",
        prior_bg_paths=[],
        scene_segments=[],
        visual_world_rules="generic world rule body",
        source_language="",  # 빈 문자열 → fallback 트리거.
        prompt_version="8",
        overlay_payload=_OVERLAY_CLEAN,
    )
    # v8 fallback 문구 자체가 본문에 들어가야.
    assert "visual_world_rules" in rendered  # fallback 텍스트의 핵심 단어.
    # v6/v7 fallback 문구 ("derive from scene_segments") 의 핵심 단어 noun
    # phrase 가 본문에 등장하지 않아야 한다 — v8 에선 scene_segments 자체가
    # prompt 본문 + section 양쪽에서 모두 사라졌으므로 LLM 이 그것을
    # 참조하라는 지시도 남지 말아야.
    assert "scene_segments" not in rendered


def test_v8_v6_source_language_fallback_unchanged():
    """v6 fallback 은 기존 그대로 ``scene_segments`` 표현을 carry — v8 변경이
    v6 path 에 회귀되지 않았음을 확인 (Codex revision 의 분리 정책)."""
    rendered = build_bg_user_prompt(
        bg_spec=_bg_spec(),
        floor_plan_path="/p/fp.png",
        prior_bg_paths=[],
        scene_segments=[],
        visual_world_rules="",
        source_language="",
        prompt_version="6",
        numbered_elements=[],
    )
    # v6 fallback 의 핵심 noun phrase 가 본문에 그대로 carry.
    assert "scene_segments" in rendered
