"""scene_detail prompt spatial alignment 회귀 가드 — G4.5a (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.5a spatial-rules 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.5a 시점에는
    7 — render_strategy.spatial_consistency 신규 sub-field 는 cp shape 변경
    아님 / G4.2 R1-I6 carry. 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.)

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

Reference: docs/superpowers/specs/2026-05-05-g4.5a-spatial-rules-lift-design.md
            docs/superpowers/plans/2026-05-05-g4.5a-spatial-rules-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.5a 4-point sync row 3 broken)"
    )
    version = MODULE_VERSIONS["scene_detail_composer"]
    assert isinstance(version, str), (
        f"MODULE_VERSIONS['scene_detail_composer'] is "
        f"{type(version).__name__} (expected str) — type contract violation"
    )
    # ★minor 는 프롬프트 major 판을 따른다 — 숫자를 박지 않고 파생한다.
    major = _list_module_versions("scene_detail")[0].split(".")[0]
    assert f".{major}." in version, (
        f"MODULE_VERSIONS['scene_detail_composer']={version!r} 의 minor 가 "
        f"prompt v{major} 와 정렬 안 됨 (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.5a 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}"
    )
    pdep = info["prompt_dependency"]
    # Trap #5: major suffix only — NOT timestamp.
    # ★기대값을 **최신 판에서 파생**한다. 예전에는 "scene_detail/v37" 처럼
    #  숫자를 박아 두어 판을 올릴 때마다 이 파일이 깨졌고, 실제로 v38·v39
    #  에서 갱신이 누락돼 registry 가 v37 로 stale 인 채 통과했다.
    latest_major = _list_module_versions("scene_detail")[0].split(".")[0]
    assert pdep == f"scene_detail/v{latest_major}", (
        f"_MODULE_INFO['scene_detail_composer']['prompt_dependency']="
        f"{pdep!r} != 'scene_detail/v{latest_major}' — 최신 판과 registry 가 "
        f"어긋났다 (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

    assert isinstance(SCENE_DETAIL_PROMPT_VERSION, str), (
        f"SCENE_DETAIL_PROMPT_VERSION is "
        f"{type(SCENE_DETAIL_PROMPT_VERSION).__name__} (expected str)"
    )
    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():
    """SCENE_DETAIL_SCHEMA_VERSION == 12 (Area #5 W1 baseline; preserved
    through Area #7b W3 + Carry-D7d-1).

    History:
      - G4.5a (2026-05-05): G4.2 R1-I6 carry — render_strategy.spatial_consistency
        신규 sub-field 는 cp shape 변경 아님, baseline 7 lock.
      - D6 T5d (2026-05-09, commit 0cb52f7): cp shape 에
        consumed_bg_catalog_hash + consumed_shot_binding_hash top-level field
        추가 → schema bump 7→8 (preflight T8 가 master_plan sibling 과 비교).
      - 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/G4.4 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 과 정렬. C5 v1 후 v32 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) 위반"
    )
