"""Prompt 버전 regression — Evidence/Inference 4-field G3.1 + Area #4 v7."""
import json
import re
from pathlib import Path

REPO_ROOT = Path(__file__).resolve().parent.parent.parent
V6_DIR = REPO_ROOT / "prompts" / "_base" / "scene_consistency" / "6.202605031033"
V14_DIR = REPO_ROOT / "prompts" / "_base" / "scene_detail" / "14.202605031033"

# Area #4 W1 — scene_consistency v7 (element_scope SOT) — Carry-P021 W1 historical pin (transitioned to v8 active prompt-pack by Carry-P021)
SC_BASE = REPO_ROOT / "prompts" / "_base" / "scene_consistency"


def _scene_consistency_v7_historical_dir() -> Path:
    """historical v7 디렉토리 literal pin — Area #4 W1 anchor (Carry-P021 W1 transitioned to historical pin, v8 active)."""
    return REPO_ROOT / "prompts" / "_base" / "scene_consistency" / "7.202605181210"


# Codex plan iter 1 I1 fix — old closed-list signature 만 금지 (개별 Korean body-part noun 단독 허용)
FORBIDDEN_PROMPT_PATTERNS = [
    r"_close_up",
    r"_wrist",
    r"전신형.*확대형|확대형.*전신형",
    r"손[·/]손목[·/]이마[·/]눈[·/]입[·/]목",
    r"손목[·/]이마[·/]눈",
]


class TestSceneConsistencyV7Prompt:
    """Area #4 W1 — element_scope SOT prompt v7 regression."""

    def test_prompt_v7_historical_loader_anchor(self):
        """Historical v7 anchor proof — element_scope presence + enum + system.md canonical clause preserved (Carry-P021 W1 pinned)."""
        v7_dir = _scene_consistency_v7_historical_dir()
        system_text = (v7_dir / "system.md").read_text()
        schema = json.loads((v7_dir / "schema.json").read_text())
        # element_scope required field exists
        item_props = schema["properties"]["fixed_elements"]["items"]["properties"]
        assert "element_scope" in item_props
        assert item_props["element_scope"]["enum"] == ["full", "close"]
        # system.md mentions element_scope decision (canonical clause)
        assert "element_scope" in system_text
        assert "element_scope 결정 (필수 emit)" in system_text

    def test_prompt_v7_historical_no_closed_framing_list(self):
        """Historical v7 anchor — A037 closed-list signature 0 proof (Carry-P021 W1 pinned; Codex plan iter 1 I1 false positive 회피)."""
        v7_dir = _scene_consistency_v7_historical_dir()
        system_text = (v7_dir / "system.md").read_text()
        for pattern in FORBIDDEN_PROMPT_PATTERNS:
            assert not re.search(pattern, system_text), (
                f"forbidden pattern {pattern!r} found in v7 system.md "
                f"(Area #4 closed-list signature 재도입 위반)"
            )

    def test_prompt_v7_historical_no_element_id_suffix_guide(self):
        """Historical v7 anchor — A038 element_id opaque snake_case 가이드 proof (Carry-P021 W1 pinned)."""
        v7_dir = _scene_consistency_v7_historical_dir()
        system_text = (v7_dir / "system.md").read_text()
        # opaque 명시 + suffix 금지 wording
        assert "opaque snake_case" in system_text
        assert "암시하지 말" in system_text or "암시하지말" in system_text
        # _close_up suffix 자체는 FORBIDDEN_PROMPT_PATTERNS 가 잡음 (dup check 불요)


class TestEvidenceFieldsInPrompts:
    def test_scene_consistency_v6_system_has_4_fields(self):
        text = (V6_DIR / "system.md").read_text()
        for kw in ["source_facts", "visual_inferences", "creative_decisions", "confidence"]:
            assert kw in text, f"missing {kw}"

    def test_scene_consistency_v6_system_blocks_legacy_output(self):
        text = (V6_DIR / "system.md").read_text()
        assert "legacy" in text
        assert "출력 금지" in text or "출력하지" in text

    def test_scene_detail_v14_system_has_4_fields(self):
        text = (V14_DIR / "system.md").read_text()
        for kw in ["source_facts", "visual_inferences", "creative_decisions", "confidence"]:
            assert kw in text

    def test_scene_detail_v14_system_blocks_legacy_output(self):
        text = (V14_DIR / "system.md").read_text()
        assert "legacy" in text

    def test_scene_detail_v14_distinguishes_source_from_inference(self):
        # PROBLEM #2 fix: fixed_element 가 source_facts 가 아니라 visual_inferences
        text = (V14_DIR / "system.md").read_text()
        assert "fixed_element" in text and "visual_inferences" in text


# ─────────────────────────────────────────────
# Group E — shot_staging v10 (Area Frame Spatial Contract, Task 2)
# ─────────────────────────────────────────────


def test_shot_staging_v10_schema_has_frame_spatial_contract():
    """shot_staging v10 schema 의 shots[].items 에 frame_spatial_contract 필드 (oneOf null/object)."""
    base = REPO_ROOT / "prompts" / "_base" / "shot_staging"
    v10_dirs = sorted([d for d in base.iterdir() if d.name.startswith("10.")])
    assert v10_dirs, "shot_staging v10 디렉토리 없음"
    schema_file = v10_dirs[-1] / "schema.json"
    schema = json.loads(schema_file.read_text())
    item_props = schema["properties"]["shots"]["items"]["properties"]
    assert "frame_spatial_contract" in item_props
    assert "oneOf" in item_props["frame_spatial_contract"]
    # constraints maxItems = 3
    one_of = item_props["frame_spatial_contract"]["oneOf"]
    object_branch = next(b for b in one_of if b.get("type") == "object")
    assert object_branch["properties"]["constraints"]["maxItems"] == 3
    # 6 reason enum
    reason_enum = object_branch["properties"]["reason"]["enum"]
    assert set(reason_enum) == {
        "movement_direction", "points_to_anchor", "looks_to_anchor",
        "shared_space_relation", "required_background_position", "primary_subject_isolation",
    }


def test_shot_staging_v12_schema_has_subject_reference_policy_array():
    """ARCHIVAL NOTICE: validates v12 schema shape (Area #1).

    Current production = v13 (Area #2 W2 — 3 field 분리:
    gaze_direction_kind/gaze_target_id/subject_state; gaze_target field 폐기).
    Retained to ensure the v12 directory artefact remains intact for historical
    reproducibility; loader latest pick now targets v13.

    framing_scale enum (v11 carry) 유지 verify.
    """
    base = REPO_ROOT / "prompts" / "_base" / "shot_staging"
    v12_dirs = sorted([d for d in base.iterdir() if d.name.startswith("12.")])
    assert v12_dirs, "shot_staging v12 디렉토리 없음 (W2 atomic 미실행)"
    schema_file = v12_dirs[-1] / "schema.json"
    schema = json.loads(schema_file.read_text())
    item = schema["properties"]["shots"]["items"]
    item_props = item["properties"]

    # v11 carry — framing_scale 유지
    assert item_props["framing_scale"]["enum"] == ["close", "medium", "wide", "insert"]
    assert "framing_scale" in item["required"]

    # Area #1 신규 — subject_reference_policy (field-level required + schema-level fail-fast)
    assert "subject_reference_policy" in item_props
    assert "subject_reference_policy" in item["required"], (
        "shots[].items.required 에 subject_reference_policy 추가 의무 "
        "(Area #1: field 자체 required, empty array 허용; spec §5.2 fail-fast 정합)"
    )
    srp = item_props["subject_reference_policy"]
    assert srp["type"] == "array"
    srp_item = srp["items"]
    assert srp_item["type"] == "object"
    assert set(srp_item["required"]) == {"subject_id", "policy_type", "policy", "reason"}
    assert srp_item["properties"]["policy"]["enum"] == [
        "id_and_outlook_required",
        "base_id_required",
        "generic_descriptor_allowed",
    ]
    assert srp_item["properties"]["policy_type"]["enum"] == ["identity_reference"]
    assert srp_item["properties"]["subject_id"]["pattern"] == "^C\\d{2,3}(?:O\\d{2,3})?$"


def test_step_manifest_shot_staging_schema_version_6():
    """step_manifest.py shot_staging entry 에 schema_version == 6 (E2E v1).
    이전 cp 와 mismatch → 구 cp invalidation.
    이전: 4 (Area #1) → 5 (3 field 분리, Area #2) → 6 (v15: gaze_target_id
    required-null 승격, OpenAI strict 호환 oneOf 제거).
    """
    from app.core.step_manifest import STEP_MANIFEST
    assert STEP_MANIFEST["shot_staging"].get("schema_version") == 6, (
        "shot_staging schema_version = 6 필수 "
        "(v15: gaze_target_id required nullable 승격)"
    )


# ─────────────────────────────────────────────
# Group E — scene_detail v23 (Area Frame Spatial Contract, Task 5)
# ─────────────────────────────────────────────


def test_scene_detail_v23_schema_has_applied_fsc_ids():
    """scene_detail v23 schema 의 t2i_variations[] item 에 applied_frame_spatial_constraint_ids required."""
    # plan §Step 5.5 는 `Path("prompts/_base/scene_detail")` 상대경로 — pytest cwd
    # (backend/) 에서 깨짐. 윗 shot_staging test (line 43) 와 동일하게 REPO_ROOT 사용.
    base = REPO_ROOT / "prompts" / "_base" / "scene_detail"
    v23_dirs = sorted([d for d in base.iterdir() if d.name.startswith("23.")])
    assert v23_dirs, "scene_detail v23 디렉토리 없음"
    schema_file = v23_dirs[-1] / "detail_schema.json"
    schema = json.loads(schema_file.read_text())
    var_props = schema["properties"]["t2i_variations"]["items"]["properties"]
    var_required = schema["properties"]["t2i_variations"]["items"]["required"]
    assert "applied_frame_spatial_constraint_ids" in var_props
    assert "applied_frame_spatial_constraint_ids" in var_required
    assert var_props["applied_frame_spatial_constraint_ids"]["type"] == "array"
    assert var_props["applied_frame_spatial_constraint_ids"]["items"]["type"] == "string"


def test_step_manifest_scene_detail_schema_version_13():
    # plan §Step 5.5 의 STEP_DEFINITIONS 는 typo — 실 export name 은 STEP_MANIFEST
    # (line 73). 동일 dict; 윗 shot_staging test (line 66) 와 일관성.
    # Area #1 W5 (2026-05-16): subject_reference_policy 추가 → bump 9→10.
    # Area #5 W1 (2026-05-18): reference_phrase_kinds 추가 → bump 10→11.
    # reference-necessity Phase 2 (2026-05-23): episode_reference_policy overlay → bump 12→13.
    from app.core.step_manifest import STEP_MANIFEST
    assert STEP_MANIFEST["scene_detail"]["schema_version"] == 13


def test_detail_steps_constants_match():
    from app.core.steps.detail_steps import SCENE_DETAIL_SCHEMA_VERSION, SCENE_DETAIL_PROMPT_VERSION
    assert SCENE_DETAIL_SCHEMA_VERSION == 13
    # Area #1 W5 (2026-05-16): v24 → v25 bump (subject_reference_policy SOT).
    # SCHEMA_VERSION 9 → 10 (id_policy.subject_reference_policy required).
    # Area #5 W1 (2026-05-18): v25 → v26 bump (reference_phrase_kinds sidecar).
    # SCHEMA_VERSION 10 → 11.
    # Area #7b W3 (2026-05-19): v26 → v27 bump (canonical VWR SOT reference clause).
    # SCHEMA_VERSION 11 preserved (schema 변경 0).
    # Carry-D7d-1 (2026-05-19): v27 → v28 bump (camera_effect.description align with Area #7d fallback policy).
    # SCHEMA_VERSION 11 preserved (schema 변경 0).
    # C1 v1 (2026-05-20): v28 → v29 bump (perception_mode consumer prompt → REPRODUCTION_PERCEPTION_MODES helper SOT cross-reference).
    # SCHEMA_VERSION 11 preserved (cp config_hash invalidation accepted, output cp data shape 변경 0).
    # C9 v1 (2026-05-20): v30 → v31 bump (Rule B body-light verb closed list + 예시 목록 추상화 → generic semantic principle).
    # SCHEMA_VERSION 12 preserved (prompt-only, output cp data shape 변경 0).
    # C5 v1 (2026-05-20): v31 → v32 bump (action/still-moment dynamic verb 닫힌 목록 추상화 → generic motion principle).
    # SCHEMA_VERSION 12 preserved (prompt-only, output cp data shape 변경 0).
    # 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 소유).
    # SCHEMA_VERSION 12 preserved (prompt-only, output cp data shape 변경 0).
    # reference-necessity Phase3 W3 (2026-05-23): v33 → v34 bump (generic_descriptor_allowed
    # no-ID rule promoted above Rule X-2; Rule X-2 made policy-conditional).
    # SCHEMA_VERSION 13 preserved (prompt-only, output cp data shape 변경 0).
    # prompt diet ③ (2026-08-03): v37 → v38 bump (RenderPromptCard 가 확정하는
    # 사안의 산문 중복 제거 — ID Policy / Rule X-2 두 섹션, 39,812 → 33,673자).
    # SCHEMA_VERSION 13 preserved (prompt-only, output cp data shape 변경 0).
    # prompt diet ③-fix (2026-08-04): v38 → v39 (재현 표면 조항 복원 224자).
    # 육안 판정 대응 (2026-08-04): v39 → v40 (점유율 관할·놓인 자세·bg_id 산문).
    #
    # 판 숫자를 여기 박아 두면 판마다 이 단언이 깨지고, 그래서 v38·v39 에서
    # registry 갱신이 통째로 누락됐다 (커밋 e481eb90 이 tests/prompts 4파일을
    # 파생으로 바꿨으나 이 파일은 남아 있었다). 지켜야 할 계약은 "몇 판인가"가
    # 아니라 **상수와 디렉토리가 일치하는가** 이므로 최신 판에서 파생한다.
    from app.modules.prompt_loader import _list_module_versions
    assert SCENE_DETAIL_PROMPT_VERSION == _list_module_versions("scene_detail")[0]


def test_version_registry_scene_detail_composer_1_28_0():
    """version_registry.py:1 의 MODULE_VERSIONS dict 확인."""
    from app.core.version_registry import MODULE_VERSIONS
    # Area #1 W5 (2026-05-16): 1.24.0 → 1.25.0 bump (subject_reference_policy SOT).
    # Area #5 W1 (2026-05-18): 1.25.0 → 1.26.0 bump (reference_phrase_kinds sidecar).
    # Area #7b W3 (2026-05-19): 1.26.0 → 1.27.0 bump (canonical VWR SOT reference clause).
    # Carry-D7d-1 (2026-05-19): 1.27.0 → 1.28.0 bump (camera_effect.description align with Area #7d fallback policy).
    # C1 v1 (2026-05-20): 1.28.0 → 1.30.0 bump (perception_mode consumer prompt → REPRODUCTION_PERCEPTION_MODES helper SOT cross-reference).
    # C9 v1 (2026-05-20): 1.30.0 → 1.31.0 bump (Rule B body-light verb closed list 추상화 → generic semantic principle).
    # C5 v1 (2026-05-20): 1.31.0 → 1.32.0 bump (action/still-moment dynamic verb 닫힌 목록 추상화 → generic motion principle).
    # FINDING 5 / e2e-bughunt-v1 W3 (2026-05-21): 1.32.0 → 1.33.0 bump (owned_object_usage echo full-coverage 1:1 → partial declare; deterministic skeleton/merge).
    # reference-necessity Phase3 W3 (2026-05-23): 1.33.0 → 1.34.0 bump (generic_descriptor_allowed no-ID rule promoted above Rule X-2; Rule X-2 made policy-conditional).
    # ★판 숫자를 박지 않는다 — minor 는 프롬프트 major 판을 따르므로
    #  최신 판에서 파생한다(하드코딩이 v38·v39 갱신 누락을 낳았다).
    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"]


# ─────────────────────────────────────────────
# Group F — scene_extractor_v2 v19 (Area #1 W6 upstream hygiene)
# ─────────────────────────────────────────────


def test_scene_extractor_v2_v19_directory_exists():
    """Area #1 W6 — v19 디렉토리 + 5 stem 존재 (v18 pack 전체 복사 baseline)."""
    base = REPO_ROOT / "prompts" / "_base" / "scene_extractor_v2"
    v19_dirs = sorted([d for d in base.iterdir() if d.name.startswith("19.")])
    assert v19_dirs, "scene_extractor_v2 v19 디렉토리 없음 (W6 미실행)"
    # 5 stem 확인
    files = sorted(p.name for p in v19_dirs[-1].iterdir() if p.is_file())
    assert "system.md" in files
    assert "turn0_context.md" in files
    assert "turn1_split_long.md" in files
    assert "turn_scene_detail.md" in files
    assert "scene_detail_schema.json" in files


def test_scene_extractor_v2_v19_latest_loaded():
    """scene_extractor_v2 turn_scene_detail loader 가 v19 자동 pick (latest pick).

    Area #1 W6 — closed-list 약화 marker (`shot_staging.subject_reference_policy`)
    가 prompt 본문에 등장하면 loader 가 v19 를 잡고 있음을 의미.
    """
    from app.modules.prompt_loader import load_prompt
    content = load_prompt("scene_extractor_v2", "turn_scene_detail")
    assert "shot_staging.subject_reference_policy" in content, (
        "scene_extractor_v2 v19 prompt 가 loader 의 latest pick 가 아님 — "
        "Area #1 W6 closed-list 약화 wording 부재"
    )
