"""
Area #7d — cinematography closed-list SOT dual-site W3 integrated test wave.

5 verify gate (closed-world exact phrase membership, no live LLM, NO VLM, no semantic regex):
(1) Gate 1 — Site 2 R1-R4 strict residue 0 in scene_extractor_v2/20.<UTC>/turn_scene_detail.md.
(2) Gate 2 — Site 1 R5-R6 strict residue 0 in variation_recommender/v2/system.md + variation_recommender_v2.py.
(3) Gate 3 — E6 R7-R9 strict residue 0 in backend/app/services/prompt_service.py.
(4) Gate 4 — carrier shape preserve (camera_effect string + _VARIATION_ITEM_SCHEMA shape + consumer call signature).
(5) Gate 5 — loadsite + 4-Gate compliance (latest v20 numeric-desc activation + fallback path preserve).

Standalone token global ban X (Codex iter 0 권고 6 흡수): low angle / warm amber / bird's eye 등
standalone token 0-hit 검사 절대 금지. R1-R9 full exact phrase only.
"""

from __future__ import annotations

from pathlib import Path

import pytest


REPO_ROOT = Path(__file__).resolve().parents[3]
PROMPTS_BASE = REPO_ROOT / "prompts" / "_base"
SCENE_EXTRACTOR_V2_BASE = PROMPTS_BASE / "scene_extractor_v2"
VARIATION_RECOMMENDER_V2 = PROMPTS_BASE / "variation_recommender" / "v2"
PROMPT_SERVICE = REPO_ROOT / "backend" / "app" / "services" / "prompt_service.py"
VARIATION_RECOMMENDER_V2_PY = REPO_ROOT / "backend" / "app" / "modules" / "variation_recommender_v2.py"


# ============================================================================
# spec §2.6 R1-R9 strict residue catalog (글자-단위 verbatim from spec, NO paraphrase)
# Codex iter 0 권고 3 흡수: Python implicit string concat 금지, single-line literal only.
# regex literal R7-R9 = plain string membership 검사, regex pattern 실행 X.
# ============================================================================

# R1 — Site 2 carrier line (full-phrase strict)
R1_SITE2_CAMERA_LIST = "카메라 구도 선택지: low angle / high angle / dutch angle / over-the-shoulder / bird's eye / extreme wide / tight medium"

# R2 — Site 2 carrier line
R2_SITE2_COLOR_LIST = "색감 선택지: warm amber / cold blue / high contrast / desaturated / golden hour / neon-lit / silhouette backlight"

# R3 — Site 2 carrier description line
R3_SITE2_CARRIER_DESC = '각 변형의 camera_effect 필드에 "카메라구도 + 색감"을 명시하세요.'

# R4 — Site 2 verbatim 예시 line
R4_SITE2_EXAMPLE_LINE = '예: "low angle, warm amber lighting" / "high angle, cold blue-toned" / "dutch angle, high contrast silhouette"'

# R5 — Site 1 composition examples 4-phrase block (4 separate phrase, all 4 must be absent)
R5_SITE1_COMPOSITION_EXAMPLES: tuple[str, ...] = (
    '"tight close-up on face"',
    '"wide establishing shot"',
    '"over-shoulder framing"',
    '"low angle hero shot"',
)

# R6 — Site 1 color examples 4-phrase block
R6_SITE1_COLOR_EXAMPLES: tuple[str, ...] = (
    '"warm golden hour lighting"',
    '"cold blue moonlight"',
    '"high contrast noir shadows"',
    '"desaturated muted tones"',
)

# R7 — E6 prompt_service regex literal first line (spec §2.6 line 123 verbatim full source literal,
#       prefix `r"`, suffix `\s*"` 포함; regex pattern로 실행 X, plain string membership only)
R7_E6_REGEX_LITERAL_A = 'r"In a (low angle|dutch angle|high angle|bird eye|wide|tracking|over the shoulder)\\s*"'

# R8 — E6 prompt_service regex literal second line (spec §2.6 line 124 verbatim full source literal,
#       prefix `r"`, suffix `\s*",` 포함; comma 끝까지 verbatim)
R8_E6_REGEX_LITERAL_B = 'r"(frame|composition|shot|view)\\s*",'

# R9 — E6 prompt_service comment line (line 345 verbatim)
R9_E6_COMMENT = "# 카메라 앵글 제거"


# ============================================================================
# Helper — v20 latest dir resolution
# ============================================================================

def _latest_v20_dir() -> Path:
    """scene_extractor_v2 active latest prompt-pack dir (numeric-latest resolution).

    legacy name (_latest_v20_dir) — Area #7d W3 시점엔 latest 가 v20 이었으나, 본 helper 는
    이후 wave 의 prompt-pack bump 를 따라 실 active latest dir 를 반환한다 (C8 W2b: v21).
    """
    candidates = [
        p for p in SCENE_EXTRACTOR_V2_BASE.iterdir()
        if p.is_dir() and p.name.split(".")[0].isdigit()
    ]
    assert candidates, (
        f"no scene_extractor_v2 prompt-pack dir found under {SCENE_EXTRACTOR_V2_BASE}"
    )
    # numeric-latest = (major int, UTC string) 최대
    return max(candidates, key=lambda p: (int(p.name.split(".")[0]), p.name))


# ============================================================================
# Gate 1 — Site 2 R1-R4 strict residue 0 in v20 turn_scene_detail.md
# ============================================================================

def test_gate1_site2_r1_camera_list_absent() -> None:
    v20 = _latest_v20_dir()
    content = (v20 / "turn_scene_detail.md").read_text(encoding="utf-8")
    assert R1_SITE2_CAMERA_LIST not in content, (
        f"R1 7-token camera list still in v20 turn_scene_detail.md: {R1_SITE2_CAMERA_LIST!r}"
    )


def test_gate1_site2_r2_color_list_absent() -> None:
    v20 = _latest_v20_dir()
    content = (v20 / "turn_scene_detail.md").read_text(encoding="utf-8")
    assert R2_SITE2_COLOR_LIST not in content, (
        f"R2 7-token color list still in v20 turn_scene_detail.md: {R2_SITE2_COLOR_LIST!r}"
    )


def test_gate1_site2_r3_carrier_description_absent() -> None:
    v20 = _latest_v20_dir()
    content = (v20 / "turn_scene_detail.md").read_text(encoding="utf-8")
    assert R3_SITE2_CARRIER_DESC not in content, (
        f"R3 carrier 'camera_effect 필드에 카메라구도+색감 명시' line still in v20: {R3_SITE2_CARRIER_DESC!r}"
    )


def test_gate1_site2_r4_example_line_absent() -> None:
    v20 = _latest_v20_dir()
    content = (v20 / "turn_scene_detail.md").read_text(encoding="utf-8")
    assert R4_SITE2_EXAMPLE_LINE not in content, (
        f"R4 verbatim 예시 line still in v20 turn_scene_detail.md: {R4_SITE2_EXAMPLE_LINE!r}"
    )


# ============================================================================
# Gate 2 — Site 1 R5-R6 strict residue 0 in variation_recommender (system.md + .py schema-description)
# ============================================================================

def test_gate2_site1_r5_composition_examples_absent_in_system_md() -> None:
    content = (VARIATION_RECOMMENDER_V2 / "system.md").read_text(encoding="utf-8")
    for phrase in R5_SITE1_COMPOSITION_EXAMPLES:
        assert phrase not in content, (
            f"R5 composition example phrase still in variation_recommender/v2/system.md: {phrase!r}"
        )


def test_gate2_site1_r6_color_examples_absent_in_system_md() -> None:
    content = (VARIATION_RECOMMENDER_V2 / "system.md").read_text(encoding="utf-8")
    for phrase in R6_SITE1_COLOR_EXAMPLES:
        assert phrase not in content, (
            f"R6 color example phrase still in variation_recommender/v2/system.md: {phrase!r}"
        )


def test_gate2_site1_r5_r6_absent_in_schema_description() -> None:
    """variation_recommender_v2.py _VARIATION_ITEM_SCHEMA composition/color description examples hard-prime 제거 verify."""
    content = VARIATION_RECOMMENDER_V2_PY.read_text(encoding="utf-8")
    for phrase in R5_SITE1_COMPOSITION_EXAMPLES + R6_SITE1_COLOR_EXAMPLES:
        # quote-stripped substring membership (schema description string 안 quoted form 변형 가능성 회피, raw substring 검사)
        stripped = phrase.strip("'\"")
        assert stripped not in content, (
            f"R5/R6 example phrase still in variation_recommender_v2.py schema-description: {stripped!r}"
        )


# ============================================================================
# Gate 3 — E6 R7-R9 strict residue 0 in prompt_service.py
# (regex literal = plain string membership only, regex pattern 실행 X)
# ============================================================================

def test_gate3_e6_r7_regex_literal_a_absent() -> None:
    content = PROMPT_SERVICE.read_text(encoding="utf-8")
    assert R7_E6_REGEX_LITERAL_A not in content, (
        f"R7 E6 regex literal first line still in prompt_service.py: {R7_E6_REGEX_LITERAL_A!r}"
    )


def test_gate3_e6_r8_regex_literal_b_absent() -> None:
    content = PROMPT_SERVICE.read_text(encoding="utf-8")
    assert R8_E6_REGEX_LITERAL_B not in content, (
        f"R8 E6 regex literal second line still in prompt_service.py: {R8_E6_REGEX_LITERAL_B!r}"
    )


def test_gate3_e6_r9_comment_absent() -> None:
    content = PROMPT_SERVICE.read_text(encoding="utf-8")
    assert R9_E6_COMMENT not in content, (
        f"R9 E6 comment '# 카메라 앵글 제거' still in prompt_service.py: {R9_E6_COMMENT!r}"
    )


def test_gate3_e6_no_cinematography_closed_list_resub_reintroduced() -> None:
    """W2 narrow/remove 후 cinematography 7-token closed-list re.sub( block 재도입 ban
    (4-Gate Semantic Regex Ban 정합, standalone token global ban X 정합 — R7-R9 exact literal 만 검사).
    """
    content = PROMPT_SERVICE.read_text(encoding="utf-8")
    # Codex iter 1 F-2 흡수: standalone token / partial token-group fragment 검사 X.
    # R7 full source literal 자체가 재도입 = 곧 cinematography 7-token closed-list re.sub( block 재도입.
    # Gate 3 R7/R8/R9 검사 (test_gate3_e6_r7_*/r8_*/r9_*) 이 본 contract 의 충분 조건.
    assert R7_E6_REGEX_LITERAL_A not in content, (
        f"R7 full source literal (= 7-token closed-list regex) re-introduced: {R7_E6_REGEX_LITERAL_A!r}"
    )


# ============================================================================
# Gate 4 — carrier shape preservation
# ============================================================================

def _camera_effect_schema_item() -> dict:
    """v20 scene_detail_schema.json 안 t2i_variations.items 위치 helper (camera_effect 가 nested under array items)."""
    import json
    v20 = _latest_v20_dir()
    schema = json.loads((v20 / "scene_detail_schema.json").read_text(encoding="utf-8"))
    return schema["properties"]["t2i_variations"]["items"]


def test_gate4_camera_effect_schema_shape_preserved() -> None:
    """scene_detail_schema.json nested t2i_variations.items camera_effect type=string preserve."""
    t2i_items = _camera_effect_schema_item()
    properties = t2i_items["properties"]
    assert "camera_effect" in properties, "camera_effect property missing under t2i_variations.items.properties"
    cam = properties["camera_effect"]
    assert cam.get("type") == "string", f"camera_effect type drift: {cam.get('type')!r}"


def test_gate4_camera_effect_schema_required_preserved() -> None:
    """scene_detail_schema.json nested t2i_variations.items.required contains camera_effect."""
    t2i_items = _camera_effect_schema_item()
    required = t2i_items.get("required", [])
    assert "camera_effect" in required, (
        f"camera_effect missing in t2i_variations.items.required (preserve obligatory): required={required!r}"
    )


def test_gate4_variation_item_schema_shape_preserved() -> None:
    """variation_recommender_v2._VARIATION_ITEM_SCHEMA shape preserve (type/angle/composition/color/reason)."""
    from app.modules.variation_recommender_v2 import _VARIATION_ITEM_SCHEMA, _RESPONSE_SCHEMA
    schema = _VARIATION_ITEM_SCHEMA
    assert schema["type"] == "object"
    expected_props = {"type", "angle", "composition", "color", "reason"}
    assert set(schema["properties"].keys()) == expected_props, (
        f"_VARIATION_ITEM_SCHEMA properties drift: {set(schema['properties'].keys())!r}"
    )
    assert schema["required"] == ["type", "angle", "composition", "color", "reason"], (
        f"_VARIATION_ITEM_SCHEMA required drift: {schema['required']!r}"
    )
    # 3D angle preserve
    angle = schema["properties"]["angle"]
    assert set(angle["properties"].keys()) == {"horizontal", "vertical", "zoom"}, (
        f"angle 3D drift: {set(angle['properties'].keys())!r}"
    )
    assert schema["properties"]["composition"]["type"] == "string"
    assert schema["properties"]["color"]["type"] == "string"
    # _RESPONSE_SCHEMA shape preserve
    assert set(_RESPONSE_SCHEMA["properties"].keys()) == {
        "variation_a", "variation_b", "recommended", "reasoning",
    }, f"_RESPONSE_SCHEMA properties drift: {set(_RESPONSE_SCHEMA['properties'].keys())!r}"


def test_gate4_scene_variation_service_consumer_signature_preserved() -> None:
    """scene_variation_service.py:344 v2 import + 403-411 var.get key list preserve."""
    svc = (REPO_ROOT / "backend" / "app" / "services" / "scene_variation_service.py").read_text(encoding="utf-8")
    assert "from app.modules.variation_recommender_v2 import VariationRecommenderV2" in svc, (
        "scene_variation_service.py v2 import missing/drift"
    )
    # var.get key list preserve (angle / composition / color persist)
    for key in ("angle", "composition", "color"):
        assert f'.get("{key}"' in svc or f".get('{key}'" in svc, (
            f"scene_variation_service.py var.get('{key}') missing/drift"
        )


# ============================================================================
# Gate 5 — loadsite + 4-Gate compliance
# ============================================================================

def test_gate5_scene_extractor_v2_latest_resolves_v20() -> None:
    """pipeline/scene_extractor_v2 load_prompt/load_schema latest = 21.<UTC> dir (numeric-desc latest; C8 W2b bump, 이전 v20)."""
    from app.modules.prompt_loader import _resolve_stem_in_pack
    for stem, ext in (
        ("turn_scene_detail", ".md"),
        ("scene_detail_schema", ".json"),
        ("system", ".md"),
        ("turn0_context", ".md"),
        ("turn1_split_long", ".md"),
    ):
        fp, ver, _all = _resolve_stem_in_pack("scene_extractor_v2", stem, ext=ext)
        assert fp is not None, f"stem {stem}{ext} not resolved"
        assert ver is not None and ver.startswith("21."), (
            f"stem {stem}{ext} ver drift (expected 21.* numeric-desc latest, got {ver!r})"
        )


def test_gate5_detail_steps_dual_owner_fallback_path_preserved() -> None:
    """detail_steps.py:1182-1188 primary scene_detail/27 + fallback scene_extractor_v2 dual-owner cascade preserve."""
    ds = (REPO_ROOT / "backend" / "app" / "core" / "steps" / "detail_steps.py").read_text(encoding="utf-8")
    # primary path scene_detail (Carry-D7d-1 boundary reaffirm — v1 변경 0)
    assert "scene_detail" in ds, "detail_steps.py primary scene_detail loadsite missing"
    # fallback path scene_extractor_v2 + turn_scene_detail (W1 변경 = scene_extractor_v2/20.<UTC> 활성화)
    assert "scene_extractor_v2" in ds, "detail_steps.py fallback scene_extractor_v2 loadsite missing"
    assert "turn_scene_detail" in ds, "detail_steps.py fallback turn_scene_detail stem missing"


def test_gate5_4_gate_inline_assertion() -> None:
    """4-Gate Boundary Doctrine inline assertion (Semantic Regex Ban / Prompt Closed-List Ban refined / Structured SOT Required / No Silent Fallback)."""
    # Gate 1 Semantic Regex Ban — E6 R7-R9 residue 0 (covered by test_gate3_*)
    # Gate 2 Prompt Closed-List Ban refined full-phrase only — R1-R6 residue 0 (covered by test_gate1_* + test_gate2_*)
    # Gate 3 Structured SOT Required — carrier shape preserve (covered by test_gate4_*)
    # Gate 4 No Silent Fallback — variation_recommender output shape preserve + scene_variation_service consumer signature preserve (covered by test_gate4_*)
    # Inline reaffirm assertion:
    assert True, "4-Gate Boundary Doctrine all 4 gate enforced by Gate 1-4 above"
