"""gpt-image 는 **2.5 계열만** 쓴다 (사용자 지시 2026-09-17).

「gpt image 모델은 전부 2.5 만 사용하는지도 확인해 — 이전 버전 사용하면 안 돼」.

## 잠그는 것 둘

① **문** — `call_gpt_image_bytes` 가 2.5 가 아닌 모델을 **API 를 부르기 전에**
   거부한다. OpenAI 이미지 호출은 전부 이 함수로 모인다.

② **래칫** — app/ 코드의 문자열 상수에 2.5 가 아닌 gpt-image 이름이 들어오면
   실패한다. 모델 이름은 설정·상수·인자 기본값·기록 라벨 등 43곳에 흩어져
   있었다 — 문이 막아 주더라도 **기록 라벨**은 문을 안 지나므로 거짓말을
   남길 수 있다. 그래서 이름 자체도 잠근다.

주석·docstring 은 모델을 고르는 자리가 아니라서 안 본다.
"""
from __future__ import annotations

import ast
import base64
import io
import pathlib
import re

import pytest

APP = pathlib.Path(__file__).resolve().parents[2] / "app"
_NAME = re.compile(r"gpt-image-[0-9][0-9A-Za-z.\-]*")


def _png_b64() -> str:
    from PIL import Image
    buf = io.BytesIO()
    Image.new("RGB", (64, 64), "white").save(buf, "PNG")
    return base64.b64encode(buf.getvalue()).decode()


class _RecordingClient:
    """나가는 요청을 **세기만** 한다 — 진짜 API 는 안 부른다."""

    def __init__(self):
        self.calls = 0
        b64 = _png_b64()
        outer = self

        class _Images:
            def generate(self, **kw):
                outer.calls += 1
                item = type("X", (), {"b64_json": b64})()
                return type("R", (), {"data": [item]})()

            edit = generate

        self.images = _Images()


@pytest.mark.parametrize("model", [
    "gpt-image-2", "gpt-image-2-2026-04-21", "gpt-image-1.5", "gpt-image-1",
    "gpt-image-1-mini", "chatgpt-image-latest", "gpt-image-2.5", "",
])
def test_the_door_refuses_every_older_model_before_calling(model):
    from app.modules.llm.gpt_image_primitive import (
        GptImageModelNotAllowed, call_gpt_image_bytes)

    cli = _RecordingClient()
    with pytest.raises(GptImageModelNotAllowed):
        call_gpt_image_bytes(cli, mode="generate", prompt="x",
                             call_kwargs={"model": model, "size": "1024x1024",
                                          "n": 1})
    assert cli.calls == 0, f"{model!r} 가 거부되기 전에 API 가 불렸다"


@pytest.mark.parametrize("model", [
    "gpt-image-2.5-sunburst", "gpt-image-2.5-flare",
    "gpt-image-2.5-sunburst-2026-09-08", "gpt-image-2.5-flare-2026-09-08",
])
def test_the_door_lets_2_5_through(model):
    from app.modules.llm.gpt_image_primitive import call_gpt_image_bytes

    cli = _RecordingClient()
    png = call_gpt_image_bytes(cli, mode="generate", prompt="x",
                               call_kwargs={"model": model,
                                            "size": "1024x1024", "n": 1})
    assert png and cli.calls == 1


def _docstring_nodes(tree: ast.AST) -> set:
    ids = set()
    for node in ast.walk(tree):
        if isinstance(node, (ast.Module, ast.ClassDef, ast.FunctionDef,
                             ast.AsyncFunctionDef)):
            body = getattr(node, "body", None) or []
            if (body and isinstance(body[0], ast.Expr)
                    and isinstance(body[0].value, ast.Constant)
                    and isinstance(body[0].value.value, str)):
                ids.add(id(body[0].value))
    return ids


def test_no_older_gpt_image_name_is_left_in_app_code():
    """★코드 상수에 2.5 가 아닌 gpt-image 이름이 하나라도 있으면 실패한다."""
    from app.modules.llm.gpt_image_primitive import GPT_IMAGE_ALLOWED_MODEL

    bad = []
    seen = 0
    for p in sorted(APP.rglob("*.py")):
        tree = ast.parse(p.read_text(encoding="utf-8"))
        docs = _docstring_nodes(tree)
        for node in ast.walk(tree):
            if (isinstance(node, ast.Constant) and isinstance(node.value, str)
                    and id(node) not in docs
                    # 잠금 자신의 정규식 원문은 이름이 아니라 규칙이다
                    and node.value != GPT_IMAGE_ALLOWED_MODEL.pattern):
                for m in _NAME.finditer(node.value):
                    seen += 1
                    if not GPT_IMAGE_ALLOWED_MODEL.fullmatch(m.group(0)):
                        bad.append(f"{p.relative_to(APP.parent)}:{node.lineno} "
                                   f"{m.group(0)!r}")
    # ★아무것도 못 찾았으면 스캔이 망가진 것이다 — 「0 개라 통과」를 막는다.
    assert seen >= 20, f"스캔이 gpt-image 이름을 {seen}개밖에 못 봤다 — 경로 확인"
    assert not bad, "2.5 가 아닌 gpt-image 이름이 코드에 남았다:\n  " + \
        "\n  ".join(bad)


def test_the_setting_itself_is_2_5():
    from app.core.config import settings
    from app.modules.llm.gpt_image_primitive import GPT_IMAGE_ALLOWED_MODEL

    for field in ("openai_image_model", "floor_plan_light_sidecar_render_model",
                  "dwelling_zone_map_render_model",
                  "immobilized_registered_pose_guide_model",
                  "indoor_shared_pose_guide_model",
                  "outdoor_composition_guide_model",
                  "space_set_bg_render_model"):
        val = str(getattr(settings, field))
        assert GPT_IMAGE_ALLOWED_MODEL.fullmatch(val), f"{field}={val!r}"
