"""shot_staging schema/version sync tests.

Area #2 (W2)에서 신설, E2E v1 (v15)에서 갱신:
v15 는 OpenAI strict structured-output 호환을 위해 schema 의 oneOf 조건부를
제거했다 — frame_spatial_contract 는 type:[object,null] nullable, character_angles
.items 는 gaze_target_id 를 required nullable 로 승격. kind↔target pairing 강제는
shot_staging 코드측 validate_pairing 으로 이동.
"""
import json
from pathlib import Path
import pytest

from app.modules.prompt_loader import load_prompt, load_schema
from app.core.step_manifest import STEP_MANIFEST
from app.core.version_registry import MODULE_VERSIONS, get_module_info


REPO_ROOT = Path(__file__).resolve().parent.parent.parent  # backend/tests/test_*.py → repo root
SHOT_STAGING_DIR = REPO_ROOT / "prompts" / "_base" / "shot_staging"

# OpenAI strict structured-output 미지원 schema combinator/conditional 키.
_OPENAI_STRICT_FORBIDDEN_KEYS = ("oneOf", "allOf", "not", "if", "then", "else")


def _walk_schema_objects(node):
    """schema tree 의 모든 dict node 를 yield (recursive)."""
    if isinstance(node, dict):
        yield node
        for v in node.values():
            yield from _walk_schema_objects(v)
    elif isinstance(node, list):
        for v in node:
            yield from _walk_schema_objects(v)


class TestShotStagingV15DirectoryExists:
    def test_v15_directory_present(self):
        v15_dirs = list(SHOT_STAGING_DIR.glob("15.*"))
        assert len(v15_dirs) >= 1

    def test_v15_system_md_loaded(self):
        # loader signature: load_prompt(module, name, ...) -> str
        # latest pick 검증: 신 field 명 포함 + legacy gaze_target section 미포함
        system_text = load_prompt("shot_staging", "system")
        assert isinstance(system_text, str)
        assert "gaze_direction_kind" in system_text, "신 field gaze_direction_kind 부재"
        assert "subject_state" in system_text, "신 field subject_state 부재"
        # legacy: v12 gaze_target prose section 폐기 verify
        assert "시선 방향 (gaze_target)" not in system_text, "v12 legacy gaze_target prose 잔존"

    def test_v15_schema_loaded(self):
        schema = load_schema("shot_staging", "schema")
        assert isinstance(schema, dict) and "properties" in schema
        # latest pick 검증: character_angles.items.properties 안 3 field 있고 gaze_target 없음
        char_angles_props = schema["properties"]["shots"]["items"]["properties"]["character_angles"]["items"]["properties"]
        assert "gaze_direction_kind" in char_angles_props
        assert "gaze_target_id" in char_angles_props
        assert "subject_state" in char_angles_props
        assert "gaze_target" not in char_angles_props, "v12 legacy gaze_target field 잔존"


class TestShotStagingV15Schema:
    @pytest.fixture
    def schema(self):
        # 활성(latest) schema — prompt_loader 가 numeric-highest version dir 선택.
        return load_schema("shot_staging", "schema")

    def _char_angle_items(self, schema):
        return schema["properties"]["shots"]["items"]["properties"]["character_angles"]["items"]

    def test_gaze_target_removed(self, schema):
        assert "gaze_target" not in self._char_angle_items(schema)["properties"]

    def test_three_new_fields_present(self, schema):
        props = self._char_angle_items(schema)["properties"]
        assert "gaze_direction_kind" in props
        assert "gaze_target_id" in props
        assert "subject_state" in props

    def test_gaze_direction_kind_enum_8_entries(self, schema):
        kind = self._char_angle_items(schema)["properties"]["gaze_direction_kind"]
        assert set(kind["enum"]) == {
            "camera", "down", "up", "distant", "closed_eyes", "off_screen",
            "looks_at_character", "looks_at_object",
        }

    def test_subject_state_enum_4_entries(self, schema):
        state = self._char_angle_items(schema)["properties"]["subject_state"]
        assert set(state["enum"]) == {"alive", "unconscious", "dead", "severely_injured"}

    def test_required_includes_kind_state_and_target(self, schema):
        # v15: gaze_target_id 가 required nullable 로 승격 (이전엔 oneOf 조건부라 required 제외).
        required = self._char_angle_items(schema)["required"]
        assert "gaze_direction_kind" in required
        assert "subject_state" in required
        assert "gaze_target_id" in required, "v15: gaze_target_id 는 required nullable"

    def test_gaze_target_id_nullable(self, schema):
        gt = self._char_angle_items(schema)["properties"]["gaze_target_id"]
        assert gt["type"] == ["string", "null"]

    def test_frame_spatial_contract_type_array_nullable(self, schema):
        # v15: oneOf:[{null},{object}] → type:[object,null] (OpenAI strict 호환).
        fsc = schema["properties"]["shots"]["items"]["properties"]["frame_spatial_contract"]
        assert fsc["type"] == ["object", "null"]
        assert "oneOf" not in fsc
        assert fsc["required"] == ["reason", "constraints"]

    def test_no_openai_strict_forbidden_combinators(self, schema):
        # Gate #1 + #2: 활성 schema 전체에 oneOf/allOf/not/if/then/else 0.
        for node in _walk_schema_objects(schema):
            for key in _OPENAI_STRICT_FORBIDDEN_KEYS:
                assert key not in node, (
                    f"OpenAI strict 미지원 키 '{key}' 가 shot_staging 활성 schema 에 존재"
                )

    def test_every_object_strict_compatible(self, schema):
        # OpenAI strict: properties 가진 모든 object 는 additionalProperties:false +
        # 모든 property key 가 required 에 포함.
        for node in _walk_schema_objects(schema):
            if isinstance(node, dict) and "properties" in node and isinstance(node["properties"], dict):
                assert node.get("additionalProperties") is False, (
                    f"properties object 에 additionalProperties:false 누락: keys={list(node['properties'])}"
                )
                required = set(node.get("required", []))
                missing = set(node["properties"]) - required
                assert not missing, (
                    f"OpenAI strict: 모든 property 가 required 여야 함 — 누락: {sorted(missing)}"
                )


class TestShotStagingVersionSync:
    def test_step_manifest_schema_version_6(self):
        # E2E v1: 5 → 6 (v15 schema — gaze_target_id required-null field-shape 변경).
        assert STEP_MANIFEST["shot_staging"]["schema_version"] == 6

    def test_version_registry_2_8_0(self):
        # E2E v1: 2.7.0 → 2.8.0 (v15 prompt, schema oneOf 제거).
        assert MODULE_VERSIONS["shot_staging"] == "2.10.0"  # E2E13 fix①⑧

    def test_prompt_dependency_v15(self):
        # version_registry export = get_module_info() public accessor (private dict = _MODULE_INFO).
        # E2E v1: shot_staging/v14 → shot_staging/v15.
        info = get_module_info("shot_staging")
        assert info["prompt_dependency"] == "shot_staging/v17"  # E2E13 fix①⑧
