"""Gemini 가 **본문 없이 사유만** 돌려주는 거절을 검열로 분류한다 (2026-08-18).

실측(S5sh2): grok 주 롤이 검열로 막혀 후퇴 사다리가 교차 백엔드(Gemini)로
갔는데, Gemini 응답이 `{"candidates":[{"content":{},"finishReason":
"PROHIBITED_CONTENT"}]}` 였다. 거부 문구가 part 로 오지 않아 기존 코드는
일반 RuntimeError 를 냈고, 사다리의 검열 판정이 그것을 못 알아봐 **남은 두
단계(순화·교차+순화)를 건너뛰고 샷이 죽었다.**
"""
import json
import io

import pytest

from app.modules.llm import gemini_image_client as gic
from app.modules.llm.gemini_image_client import GeminiImageClient, ModerationError


class _Resp(io.BytesIO):
    def __enter__(self):
        return self

    def __exit__(self, *a):
        return False


def _client_with_payload(monkeypatch, payload):
    class _Noop:
        def log(self, *a, **kw):
            return None

    from app.modules.llm import image_tracer as _it
    monkeypatch.setattr(_it, "get_image_tracer", lambda *a, **kw: _Noop())
    monkeypatch.setattr(gic, "log_llm_call", lambda *a, **kw: "call-id")
    monkeypatch.setattr(
        gic.urllib.request, "urlopen",
        lambda req, timeout=None: _Resp(json.dumps(payload).encode()))
    return GeminiImageClient(api_key="test-key")


REFUSAL = {
    "candidates": [{"content": {}, "finishReason": "PROHIBITED_CONTENT",
                    "index": 0}],
    "usageMetadata": {"promptTokenCount": 984},
}


def test_finish_reason_only_refusal_raises_moderation_error(monkeypatch):
    client = _client_with_payload(monkeypatch, REFUSAL)
    with pytest.raises(ModerationError) as ei:
        client.generate_image("a shot")
    # 사다리의 검열 판정이 문구로 가른다 — 'moderation' 이 들어 있어야 한다.
    assert "moderation" in str(ei.value).lower()
    assert "PROHIBITED_CONTENT" in str(ei.value)


@pytest.mark.parametrize("reason", ["IMAGE_PROHIBITED_CONTENT", "IMAGE_SAFETY",
                                    "SAFETY", "RECITATION", "BLOCKLIST",
                                    "SPII"])
def test_other_refusal_reasons_are_moderation_too(monkeypatch, reason):
    payload = {"candidates": [{"content": {}, "finishReason": reason}]}
    client = _client_with_payload(monkeypatch, payload)
    with pytest.raises(ModerationError):
        client.generate_image("a shot")


def test_non_refusal_empty_response_stays_a_plain_error(monkeypatch):
    """거절이 아닌 빈 응답까지 검열로 부르면 안 된다 — 유료 후퇴가 헛돈다."""
    payload = {"candidates": [{"content": {}, "finishReason": "STOP"}]}
    client = _client_with_payload(monkeypatch, payload)
    with pytest.raises(RuntimeError) as ei:
        client.generate_image("a shot")
    assert not isinstance(ei.value, ModerationError)


def test_ladder_moderation_check_accepts_the_new_error():
    """후퇴 사다리가 이 예외를 검열로 알아보는지 — 문구 계약을 잠근다."""
    msg = str(ModerationError(block_reason="finish_refusal:PROHIBITED_CONTENT",
                              block_categories=["PROHIBITED_CONTENT"],
                              raw_response={})).lower()
    assert ("moderation" in msg or "safety" in msg
            or "blocked" in msg or "content_policy" in msg)
