"""shot_staging 코드측 gaze pairing 검증 테스트 (E2E v1).

v15 schema 는 OpenAI strict 호환을 위해 character_angles.items 의 oneOf 조건부를
제거했다. kind↔target pairing 정합은 shot_staging._validate_gaze_pairings 가
post-LLM fail-fast 로 강제한다. validate_pairing 단위 동작은
tests/unit/test_gaze_direction_helper.py 가 담당 — 본 파일은 shot_staging 의
batch_shots 순회 통합(모든 shot × 모든 character_angle)을 검증.
"""
import pytest

from app.core.errors import AppError
from app.modules.pipeline.shot_staging import _validate_gaze_pairings


def _shot(*char_angles):
    return {"scene_index": 1, "shot_index": 2, "character_angles": list(char_angles)}


def _angle(kind, target_id):
    return {
        "character": "<인물>",
        "angle": "facing_camera",
        "body_pose": "leaning on doorframe",
        "gaze_direction_kind": kind,
        "gaze_target_id": target_id,
        "subject_state": "alive",
    }


class TestValidGazePairings:
    def test_looks_at_character_with_c_id_passes(self):
        _validate_gaze_pairings([_shot(_angle("looks_at_character", "C03"))])

    @pytest.mark.parametrize("tid", ["P01", "B02"])
    def test_looks_at_object_with_p_or_b_id_passes(self, tid):
        _validate_gaze_pairings([_shot(_angle("looks_at_object", tid))])

    @pytest.mark.parametrize("kind", ["camera", "down", "up", "distant", "closed_eyes", "off_screen"])
    def test_non_target_kind_with_null_passes(self, kind):
        _validate_gaze_pairings([_shot(_angle(kind, None))])

    def test_empty_and_missing_character_angles_pass(self):
        _validate_gaze_pairings([])
        _validate_gaze_pairings([{"scene_index": 1, "shot_index": 1}])
        _validate_gaze_pairings([_shot()])

    def test_multiple_shots_and_angles_all_valid(self):
        shots = [
            _shot(_angle("looks_at_character", "C01"), _angle("camera", None)),
            _shot(_angle("looks_at_object", "B07")),
        ]
        _validate_gaze_pairings(shots)


class TestInvalidGazePairings:
    def test_target_kind_missing_target_raises(self):
        with pytest.raises(AppError) as ei:
            _validate_gaze_pairings([_shot(_angle("looks_at_character", None))])
        assert ei.value.code == "step.contract_violation.gaze_target_id.missing_for_target_kind"

    def test_non_target_kind_with_target_raises(self):
        with pytest.raises(AppError) as ei:
            _validate_gaze_pairings([_shot(_angle("camera", "C01"))])
        assert ei.value.code == "step.contract_violation.gaze_target_id.present_for_non_target_kind"

    def test_looks_at_character_wrong_shape_raises(self):
        with pytest.raises(AppError) as ei:
            _validate_gaze_pairings([_shot(_angle("looks_at_character", "P01"))])
        assert ei.value.code == "step.contract_violation.gaze_target_id.shape"

    def test_looks_at_object_wrong_shape_raises(self):
        with pytest.raises(AppError) as ei:
            _validate_gaze_pairings([_shot(_angle("looks_at_object", "C01"))])
        assert ei.value.code == "step.contract_violation.gaze_target_id.shape"

    def test_invalid_kind_raises(self):
        with pytest.raises(AppError) as ei:
            _validate_gaze_pairings([_shot(_angle("staring", "C01"))])
        assert ei.value.code == "step.contract_violation.gaze_direction_kind.enum"

    def test_violation_in_later_shot_is_caught(self):
        shots = [
            _shot(_angle("camera", None)),
            _shot(_angle("looks_at_character", None)),  # violation in 2nd shot
        ]
        with pytest.raises(AppError) as ei:
            _validate_gaze_pairings(shots)
        assert ei.value.code == "step.contract_violation.gaze_target_id.missing_for_target_kind"
