"""scene_detail prompt continuity alignment 회귀 가드 — G4.4 (2026-05-05).

Area C Task 4 (2026-05-12) — v21 → v22 bump (reproduction_surface applies bool).
area-frame-spatial-contract T5 (2026-05-14) — v22 → v23 bump
(applied_frame_spatial_constraint_ids echo).
framing_scale enum SOT v1 (2026-05-15) — v23 → v24 bump (framing_scale enum 소비).
Area #1 W5 (2026-05-16) — v24 → v25 bump (id_policy.subject_reference_policy SOT).
Area #5 W1 (2026-05-18) — v25 → v26 bump (reference_phrase_kinds sidecar).
Area #7b W3 (2026-05-19) — v26 → v27 bump (canonical VWR SOT reference clause).
Carry-D7d-1 (2026-05-19) — v27 → v28 bump (camera_effect.description align with Area #7d fallback policy).
C1 v1 (2026-05-20) — v28 → v29 bump (perception_mode consumer prompt line 64 4-token closed list → REPRODUCTION_PERCEPTION_MODES helper SOT cross-reference; cp config_hash invalidation accepted).
C9 v1 (2026-05-20) — v30 → v31 bump (Rule B body-light verb closed list + 예시 목록 추상화 → generic semantic principle; prompt-only, schema_version 12 preserve).
C5 v1 (2026-05-20) — v31 → v32 bump (action/still-moment dynamic verb 닫힌 목록 추상화 → generic motion principle; prompt-only, schema_version 12 preserve).
FINDING 5 / e2e-bughunt-v1 W3 (2026-05-21) — v32 → v33 bump (owned_object_usage echo full-coverage 1:1 → partial declare; code 가 deterministic skeleton/merge 소유; prompt-only, schema_version 12 preserve).

G4.4 continuity lift 4-point sync 강제 (FINDING 5 후 latest):

  (1) prompt directory `prompts/_base/scene_detail/33.<timestamp>/` 존재
  (2) `detail_steps.SCENE_DETAIL_PROMPT_VERSION.startswith("33.")` —
      checkpoint config_hash 계산용 (cp invalidation gate)
  (3) `version_registry.MODULE_VERSIONS["scene_detail_composer"] == "1.33.0"`
  (4) `version_registry._MODULE_INFO["scene_detail_composer"]["prompt_dependency"]
      == "scene_detail/v33"` (major suffix only — Trap #5 carry)

추가 검증:
  - `SCENE_DETAIL_SCHEMA_VERSION == 12` (Area #5 W1 후 baseline; G4.4 시점에는
    Override O-13 으로 7 lock — cp shape unchanged by continuity lift.
    D6 T5d (2026-05-09, commit 0cb52f7) 가 consumed_bg_catalog_hash +
    consumed_shot_binding_hash 추가로 7→8 bump. area-frame-spatial-contract
    T5 (2026-05-14, commit b1916f2) 가
    t2i_variations[].applied_frame_spatial_constraint_ids required 추가로
    8→9 bump. Area #1 W5 (2026-05-16) 가 id_policy.subject_reference_policy
    required 추가로 9→10 bump. Area #5 W1 (2026-05-18) 가
    t2i_variations[].reference_phrase_kinds required 추가로 10→11 bump.
    Area #7b W3 (2026-05-19) v26 → v27 + Carry-D7d-1 (2026-05-19) v27 → v28 =
    schema_version 11 preserve.)
  - `_card_metadata.lift_status` 4 신규 G4.4 keys (Override O-9 canonical
    names — abbrev 금지)

**중요 — Trap #13 silent fallback 금지** (`feedback_no_silent_fallback.md`
carry): `_MODULE_INFO.get(..., {})` / `lift_status.get(..., False)` 류 패턴
절대 금지. 모두 explicit `assert "key" in dict` + dict access (`d["key"]`)
— key 부재 시 KeyError fail-fast.

Reference: docs/superpowers/specs/2026-05-05-g4.4-continuity-lift-design.md
            docs/superpowers/plans/2026-05-05-g4.4-continuity-lift-implementation.md
"""
from __future__ import annotations

import os

from app.modules.prompt_loader import _list_module_versions


# ──────────────────────────────────────────────────────────────────────────
# 4-point sync alignment (Trap #13 — strict explicit dict access; NO
# `.get(..., default)` silent fallback).
# ──────────────────────────────────────────────────────────────────────────


def test_module_versions_scene_detail_composer_strict():
    """4-point sync (3) — MODULE_VERSIONS["scene_detail_composer"] == "1.32.0".

    Area C Task 4 (2026-05-12) bump — v21 → v22 / 1.21.0 → 1.22.0.
    area-frame-spatial-contract T5 (2026-05-14) bump — v22 → v23 / 1.22.0 → 1.23.0
    (applied_frame_spatial_constraint_ids echo).
    framing_scale enum SOT v1 (2026-05-15) bump — v23 → v24 / 1.23.0 → 1.24.0.
    Area #1 W5 (2026-05-16) bump — v24 → v25 / 1.24.0 → 1.25.0
    (id_policy.subject_reference_policy SOT).
    Area #5 W1 (2026-05-18) bump — v25 → v26 / 1.25.0 → 1.26.0
    (reference_phrase_kinds sidecar).
    Area #7b W3 (2026-05-19) bump — v26 → v27 / 1.26.0 → 1.27.0
    (canonical VWR SOT reference clause).
    Carry-D7d-1 (2026-05-19) bump — v27 → v28 / 1.27.0 → 1.28.0
    (camera_effect.description align with Area #7d fallback policy).

    Trap #13 — strict `in` check + dict access (NOT `.get(..., "")`).
    """
    from app.core.version_registry import MODULE_VERSIONS

    assert "scene_detail_composer" in MODULE_VERSIONS, (
        "MODULE_VERSIONS['scene_detail_composer'] missing — version_registry "
        "stale (G4.4 4-point sync row 3 broken)"
    )
    # ★minor 는 프롬프트 major 판을 따른다 — 숫자를 박지 않고 파생한다.
    from app.modules.prompt_loader import _list_module_versions

    _major = _list_module_versions("scene_detail")[0].split(".")[0]
    assert f".{_major}." in MODULE_VERSIONS["scene_detail_composer"], (
        f"MODULE_VERSIONS['scene_detail_composer']="
        f"{MODULE_VERSIONS['scene_detail_composer']!r} 의 minor 가 v{_major} 와 정렬 안 됨 —"
        f"Phase3 W3 minor bump (1.33.0 → 1.34.0, v34 generic_descriptor_allowed "
        f"no-ID rule + Rule X-2 policy-conditional) not applied (4-point sync row 3)"
    )


def test_module_info_scene_detail_composer_prompt_dependency_strict():
    """4-point sync (4) — _MODULE_INFO["scene_detail_composer"]["prompt_dependency"]
    == "scene_detail/v32" (major suffix only — Trap #5 carry).

    Area C Task 4 (2026-05-12) bump — v21 → v22.
    area-frame-spatial-contract T5 (2026-05-14) bump — v22 → v23.
    framing_scale enum SOT v1 (2026-05-15) bump — v23 → v24.
    Area #1 W5 (2026-05-16) bump — v24 → v25.
    Area #5 W1 (2026-05-18) bump — v25 → v26.
    Area #7b W3 (2026-05-19) bump — v26 → v27.
    Carry-D7d-1 (2026-05-19) bump — v27 → v28.

    Trap #13 — strict `in` check + dict access. NO `.get(..., "")`.
    """
    from app.core.version_registry import _MODULE_INFO

    assert "scene_detail_composer" in _MODULE_INFO, (
        "_MODULE_INFO['scene_detail_composer'] missing — version_registry "
        "stale (G4.4 4-point sync row 4 broken)"
    )
    info = _MODULE_INFO["scene_detail_composer"]  # dict access (Trap #5)
    assert isinstance(info, dict), (
        f"_MODULE_INFO['scene_detail_composer'] is {type(info).__name__} "
        f"(expected dict) — R3R4-B1 contract violation"
    )
    assert "prompt_dependency" in info, (
        f"_MODULE_INFO['scene_detail_composer'] missing 'prompt_dependency' "
        f"— got {sorted(info.keys())!r}"
    )
    # Trap #5: major suffix only — NOT timestamp.
    # ★기대값을 최신 판에서 파생한다(숫자를 박으면 판마다 깨진다 — v38·v39
    #  에서 실제로 갱신이 누락돼 registry 가 stale 인 채 남았다).
    from app.modules.prompt_loader import _list_module_versions

    latest_major = _list_module_versions("scene_detail")[0].split(".")[0]
    assert info["prompt_dependency"] == f"scene_detail/v{latest_major}", (
        f"_MODULE_INFO['scene_detail_composer']['prompt_dependency']="
        f"{info['prompt_dependency']!r} != 'scene_detail/v{latest_major}' — "
        f"최신 판과 registry 가 어긋났다 (4-point sync row 4)"
    )


def test_scene_detail_prompt_version_matches_directory():
    """4-point sync (1) + (2) — SCENE_DETAIL_PROMPT_VERSION == directory.

    Area #7b W3 (2026-05-19) bump — `SCENE_DETAIL_PROMPT_VERSION` startswith
    "27." (TIMESTAMP 가 W 마다 갱신되므로 strict equality 대신 prefix 검사).
    Carry-D7d-1 (2026-05-19) bump — `SCENE_DETAIL_PROMPT_VERSION` startswith
    "28." (camera_effect.description align with Area #7d fallback policy).

    Prior history: Area #1 W5 (2026-05-16) — startswith "25." (id_policy.subject_reference_policy);
    Area #5 W1 (2026-05-18) — startswith "26." (reference_phrase_kinds sidecar).
    """
    from app.core.steps.detail_steps import SCENE_DETAIL_PROMPT_VERSION

    from app.modules.prompt_loader import _list_module_versions

    latest_major = _list_module_versions("scene_detail")[0].split(".")[0]
    assert SCENE_DETAIL_PROMPT_VERSION.startswith(f"{latest_major}."), (
        f"SCENE_DETAIL_PROMPT_VERSION={SCENE_DETAIL_PROMPT_VERSION!r} 가 최신 판 "
        f"v{latest_major} 를 가리키지 않는다 — 4-point sync row 2 "
        f"(detail_steps.py SCENE_DETAIL_PROMPT_VERSION) 미bump"
    )
    # Repo-root absolute path — backend/tests/prompts/<file> at parents[3].
    repo_root = os.path.dirname(
        os.path.dirname(
            os.path.dirname(
                os.path.dirname(os.path.abspath(__file__))
            )
        )
    )
    prompt_dir = os.path.join(
        repo_root, "prompts", "_base", "scene_detail",
        SCENE_DETAIL_PROMPT_VERSION,
    )
    assert os.path.isdir(prompt_dir), (
        f"prompt directory {prompt_dir!r} does not exist — "
        f"Phase3 W3 v34 prompt directory not created "
        f"(4-point sync row 1)"
    )


def test_scene_detail_schema_version_at_area_5_baseline():
    """Area #5 W1 bump — SCENE_DETAIL_SCHEMA_VERSION == 12 (preserved through
    Area #7b W3 + Carry-D7d-1).

    History:
      - G4.4 (2026-05-05): Override O-13 — cp shape unchanged by continuity
        lift, baseline locked at 7.
      - D6 T5d (2026-05-09, commit 0cb52f7): cp shape gained
        ``consumed_bg_catalog_hash`` + ``consumed_shot_binding_hash`` top-level
        fields → schema bump 7→8 (preflight T8 가 master_plan sibling 과 비교
        해 STALE_UPSTREAM 판단).
      - area-frame-spatial-contract T5 (2026-05-14, commit b1916f2):
        ``t2i_variations[].applied_frame_spatial_constraint_ids`` required 추가
        → schema bump 8→9 (per-variation echo of Frame Spatial Contract).
      - Area #1 W5 (2026-05-16): ``id_policy.subject_reference_policy`` required
        추가 + ``body_part_focus_rule`` / ``close_framing_face_phrasing`` 제거
        → schema bump 9→10 (per-subject identity policy SOT).
      - Area #5 W1 (2026-05-18): ``t2i_variations[].reference_phrase_kinds``
        array<enum [character|background|prop]> required 추가 → schema bump
        10→11 (per-variation sidecar for ref_contract_validator step 6
        phantom guard replacement).
        본 테스트는 Area #5 W1 이후 baseline 11 으로 갱신.
      - Area #7b W3 (2026-05-19): v26 → v27, schema_version 11 preserve.
      - Carry-D7d-1 (2026-05-19): v27 → v28, schema_version 11 preserve.
    """
    from app.core.steps.detail_steps import SCENE_DETAIL_SCHEMA_VERSION

    # reference-necessity Phase 2 (2026-05-23): render_prompt_card overlay → 12→13.
    assert SCENE_DETAIL_SCHEMA_VERSION == 13, (
        f"SCENE_DETAIL_SCHEMA_VERSION={SCENE_DETAIL_SCHEMA_VERSION!r} != 13 "
        f"— reference-necessity Phase 2 bump (episode_reference_policy overlay) violated"
    )


# ──────────────────────────────────────────────────────────────────────────
# Defensive — _list_module_versions() agrees with SCENE_DETAIL_PROMPT_VERSION.
# (G4.3 carry — protects against directory-without-version-bump regression.)
# ──────────────────────────────────────────────────────────────────────────


def test_scene_detail_prompt_loader_resolves_to_latest_pinned_version():
    """`_list_module_versions("scene_detail")` 가 pinned latest 를 반환.
    SCENE_DETAIL_PROMPT_VERSION 과 정렬. FINDING 5 후 v33 latest."""
    from app.core.steps.detail_steps import SCENE_DETAIL_PROMPT_VERSION

    versions = _list_module_versions("scene_detail")
    assert versions, "scene_detail prompt directory empty — operational incident"
    latest = versions[0]
    assert latest == SCENE_DETAIL_PROMPT_VERSION, (
        f"prompt loader latest={latest!r} != "
        f"SCENE_DETAIL_PROMPT_VERSION={SCENE_DETAIL_PROMPT_VERSION!r} — "
        f"4-point sync drift (loader resolves different version than "
        f"detail_steps.py:106 constant)"
    )
    # ★major 숫자 자체는 단언하지 않는다 — 지켜야 할 것은 "네 곳이 서로
    #  일치하는가"이지 "지금이 몇 판인가"가 아니다. 위
    #  `latest == SCENE_DETAIL_PROMPT_VERSION` 단언이 그 계약을 담고 있다.
    assert latest.split(".")[0].isdigit(), (
        f"latest scene_detail version={latest!r} 의 major 가 숫자가 아니다 — "
        f"판 디렉토리 명명 규칙(버전.YYYYMMDDHHmm) 위반"
    )


# ──────────────────────────────────────────────────────────────────────────
# G4.4 _card_metadata.lift_status 4 신규 keys (Override O-9 canonical names).
# Trap #13 — explicit dict access (NOT `.get(..., False)` silent fallback).
# ──────────────────────────────────────────────────────────────────────────


def test_card_metadata_lift_status_g4_4_keys():
    """Override O-9 — 4 G4.4 신규 lift_status keys present + True.

    Canonical names (longer descriptive form — abbrev 금지):
      - continuity_cross_shot_id_substitution_lifted
      - continuity_ref_usage_constraints_lifted
      - continuity_view_consistency_lifted
      - continuity_constraints_extended

    Trap #13 — explicit `assert "key" in dict` + dict access. NO `.get(..., False)`
    silent fallback. key 부재 시 KeyError fail-fast.
    """
    from app.core.steps.render_prompt_card import build_render_prompt_card

    # Minimal fixture — production-shaped inputs (visible_entities/outlook_pairs/
    # bg_owned 모두 non-empty). G4.1+G4.2+G4.3+G4.4 모두 카드에 포함.
    card = build_render_prompt_card(
        scene_index=1,
        shot_index=1,
        seg={"index": 1, "text": "scene segment text content here"},
        shot_info={
            "shot_index": 1,
            "camera_direction": "medium shot",
            "primary_subject": "the subject at the doorway",
        },
        visible_entities=["C01", "C02"],
        outlook_pairs=[
            {"character_id": "C01", "outlook_id": "O01"},
            {"character_id": "C02", "outlook_id": "O02"},
        ],
        perception_mode=None,
        staging={
            "camera_direction": "medium shot",
            # framing_scale enum SOT v1 (2026-05-15): helper read fail-fast 정합.
            "framing_scale": "medium",
            "lighting_mood": "warm",
            "key_bg_elements": [],
            # Area #1 W5 (2026-05-16): shot_staging v12 top-level required field
            # — helper SOT graceful empty (default policy id_and_outlook_required
            # 가 visible_entities 에 적용).
            "subject_reference_policy": [],
        },
        bg_id="cb_test_001",
        bg_owned=["door", "window"],
        bg_camera_meta={
            "camera_position": "south",
            "camera_height": "eye-level",
            "lens_hint": "35mm",
            "framing_notes": "wide",
        },
        bg_guide="general view",
        is_close_framing=False,
        background_mode_on=True,
        fixed_elements=[],
        previous_shot_refs=[],
        forward_zoom_targets=[],
    )
    assert "_card_metadata" in card, (
        "card missing '_card_metadata' envelope-sibling — G4.2 R1-B1 carry "
        "broken"
    )
    metadata = card["_card_metadata"]  # dict access (Trap #13)
    assert "lift_status" in metadata, (
        "_card_metadata missing 'lift_status' — G4.2 R1-B1 envelope shape "
        "broken"
    )
    lift_status = metadata["lift_status"]  # dict access (Trap #13)
    # G4.2 / G4.3 carry 4 keys 보존 (regression).
    for carry_key in (
        "rule_a_lifted",
        "rule_c_lifted",
        "rule_h_lifted",
        "id_policy_composite_lifted",
        "id_policy_close_framing_face_lifted",
        "id_policy_reproduction_surface_lifted",
    ):
        assert carry_key in lift_status, (
            f"G4.2/G4.3 carry lift_status key {carry_key!r} missing — "
            f"got {sorted(lift_status.keys())!r}"
        )
        assert lift_status[carry_key] is True, (
            f"G4.2/G4.3 carry {carry_key!r} expected True, got "
            f"{lift_status[carry_key]!r}"
        )
    # G4.4 신규 4 keys (Override O-9 canonical names).
    for new_key in (
        "continuity_cross_shot_id_substitution_lifted",
        "continuity_ref_usage_constraints_lifted",
        "continuity_view_consistency_lifted",
        "continuity_constraints_extended",
    ):
        assert new_key in lift_status, (
            f"G4.4 lift_status key {new_key!r} missing — got "
            f"{sorted(lift_status.keys())!r}"
        )
        assert lift_status[new_key] is True, (
            f"G4.4 {new_key!r} expected True, got {lift_status[new_key]!r}"
        )


# ──────────────────────────────────────────────────────────────────────────
# G4.4 R6-I1 fix iter 1 carry — v19 system.md must NOT contain dangling
# pointers to deleted v18 sections. Future v## bumps that delete more
# sections must NOT reintroduce stale references.
# ──────────────────────────────────────────────────────────────────────────


def test_v19_no_stale_v18_continuity_section_pointer():
    """G4.4 carry — v19 prompt must NOT contain dangling pointer to deleted v18 sections.
    Future v## bumps that delete more sections must NOT reintroduce stale references.
    """
    from app.core.steps.detail_steps import SCENE_DETAIL_PROMPT_VERSION
    repo_root = os.path.dirname(
        os.path.dirname(
            os.path.dirname(
                os.path.dirname(os.path.abspath(__file__))
            )
        )
    )
    v19_path = os.path.join(
        repo_root, "prompts", "_base", "scene_detail",
        SCENE_DETAIL_PROMPT_VERSION, "system.md",
    )
    with open(v19_path, encoding="utf-8") as f:
        v19_text = f.read()
    # The phrases that referenced the deleted v18 sections — must be absent in v19
    assert "위 C## 통합 규칙" not in v19_text, (
        "v19 system.md still contains stale pointer to deleted "
        "'교차 샷 고정 요소 통합 규칙' section"
    )
    assert '"앞쪽 참조 샷 처리"' not in v19_text, (
        "v19 system.md still contains stale pointer to deleted "
        "'앞쪽 참조 샷 처리' section"
    )


def test_v19_continuity_pointer_uses_new_section_name():
    """G4.4 carry — replacement pointer must reference the new '## Continuity' section."""
    from app.core.steps.detail_steps import SCENE_DETAIL_PROMPT_VERSION
    repo_root = os.path.dirname(
        os.path.dirname(
            os.path.dirname(
                os.path.dirname(os.path.abspath(__file__))
            )
        )
    )
    v19_path = os.path.join(
        repo_root, "prompts", "_base", "scene_detail",
        SCENE_DETAIL_PROMPT_VERSION, "system.md",
    )
    with open(v19_path, encoding="utf-8") as f:
        v19_text = f.read()
    # The replacement pointer text added in fix iter 1
    assert "## Continuity" in v19_text  # heading itself
    # Each pointer references the new Continuity section
    assert v19_text.count("`## Continuity`") >= 2  # both pointers + heading reference
