"""Gemini 가 정수 `enum` 스키마를 거부하던 것 (2026-08-28 주행 중단).

## 무엇이 결함이었나

최소 검증판 주행이 **8번째 스텝에서 통째로 멎었다**. `beat_extract` 가
`gemini-3.1-pro-preview` 로 5회 재시도 끝에 전부 400:

    Invalid value at '…properties[0].value.enum[0]' (TYPE_STRING), 1
    Invalid value at '…properties[0].value.enum[1]' (TYPE_STRING), 2
    Invalid value at '…properties[0].value.enum[2]' (TYPE_STRING), 3

`beat_shot_steps.py:228·531` 이 `scene_index` 에
`{"type": "integer", "enum": [1, 2, 3]}` 을 주입한다. Gemini 의
`response_schema.enum` 은 **문자열 배열**이라 정수를 못 받는다.
08-24 주행에는 통했으니 **provider 쪽이 조인 것**으로 본다.

## 고친 자리

`_sanitize_response_format_for_model` — 이미 있던 **provider 경계** 손질
자리다(OpenAI 의 `uniqueItems` 를 걷던 곳). 원본 스키마는 안 건드리므로
`_validate_local_schema` 는 계속 원본으로 검증한다.

## 왜 문자열로 바꾸지 않고 걷나

Gemini enum 은 `type: STRING` 전용이다. 정수를 문자열로 바꿔 보내면 모델이
문자열을 돌려줄 수 있고, 그러면 호출부의 `set(returned) != set(expected)`
대조가 통째로 어긋난다. enum 은 **안내**이고 계약은 뒤에서 지킨다 —
반환 인덱스 대조 → 순서 재매핑 → 재시도 → 빠진 씬 빈 beat
(`beat_shot_steps.py:248-280`). 안내 하나를 잃고 스텝을 살린다.
"""
from __future__ import annotations

import copy

from app.modules.llm.llm_client import _sanitize_response_format_for_model

# 프로덕션이 실제로 주입하는 모양 (`beat_shot_steps.py:224-229`)
SCHEMA = {
    "type": "object",
    "properties": {
        "scenes": {
            "type": "array",
            "items": {
                "type": "object",
                "properties": {
                    "scene_index": {"type": "integer", "enum": [1, 2, 3]},
                    "beats": {
                        "type": "array",
                        "items": {
                            "type": "object",
                            "properties": {
                                "kind": {"type": "string",
                                         "enum": ["action", "reaction"]},
                            },
                        },
                    },
                },
            },
        },
    },
}


def _sanitized(model: str) -> dict:
    kwargs = {"response_format": {
        "type": "json_schema",
        "json_schema": {"name": "beat_extract",
                        "schema": copy.deepcopy(SCHEMA), "strict": True}}}
    _sanitize_response_format_for_model(model, kwargs)
    return kwargs["response_format"]["json_schema"]["schema"]


def _props(schema: dict) -> dict:
    return schema["properties"]["scenes"]["items"]["properties"]


def test_gemini_loses_the_integer_enum():
    """★400 을 내던 바로 그 칸."""
    got = _props(_sanitized("gemini-pro"))["scene_index"]
    assert "enum" not in got, "정수 enum 이 그대로 나간다 — Gemini 가 400 을 낸다"
    assert got["type"] == "integer", "정수를 달라는 요구까지 잃으면 안 된다"


def test_gemini_keeps_the_string_enum():
    """문자열 enum 은 Gemini 가 받는다 — 같이 걷으면 안내를 헛되이 잃는다."""
    beats = _props(_sanitized("gemini-pro"))["beats"]["items"]["properties"]
    assert beats["kind"]["enum"] == ["action", "reaction"]


def test_the_openai_path_is_untouched():
    """★양성 확인 — 이 손질은 Gemini 전용이다."""
    assert _props(_sanitized("gpt"))["scene_index"]["enum"] == [1, 2, 3]


def test_the_callers_schema_is_not_mutated():
    """원본이 바뀌면 `_validate_local_schema` 가 다른 계약으로 검증한다."""
    _sanitized("gemini-pro")
    assert _props(SCHEMA)["scene_index"]["enum"] == [1, 2, 3]


def test_the_known_injection_sites_are_exactly_these_three():
    """★조립부가 정수 enum 을 **여전히 주입한다**는 것과, 그 자리가 **어디인지**.

    개수만 세면 새로 생긴 자리를 놓친다 — 파일까지 못박는다
    (Codex #40 리뷰가 내가 「두 곳」이라 적은 것을 잡았다. 실제는 셋이고,
     셋째(`scene_camera_flow`)는 예외를 빈 flow 로 삼켜 **실패가 안 보이는**
     자리라 오히려 이 수정의 실제 수혜처다).

    나중에 조립부가 문자열로 바뀌거나 새 자리가 생기면 이 시험이 빨강이
    되고, 그때 이 경계 손질이 아직 필요한지 다시 본다.
    """
    import ast
    import pathlib

    app = pathlib.Path(__file__).resolve().parents[2] / "app"
    found = []
    for path in sorted(app.rglob("*.py")):
        try:
            tree = ast.parse(path.read_text(encoding="utf-8"))
        except SyntaxError:
            continue
        for node in ast.walk(tree):
            if not isinstance(node, ast.Dict):
                continue
            keys = [k.value for k in node.keys
                    if isinstance(k, ast.Constant) and isinstance(k.value, str)]
            if "type" not in keys or "enum" not in keys:
                continue
            t = node.values[keys.index("type")]
            if isinstance(t, ast.Constant) and t.value in ("integer", "number"):
                found.append(f"{path.relative_to(app)}:{node.lineno}")

    assert found == [
        "core/steps/beat_shot_steps.py:226",
        "core/steps/beat_shot_steps.py:529",
        "core/steps/scene_camera_flow_step.py:240",
    ], f"정수 enum 주입 자리가 달라졌다: {found}"
