"""참조 이미지를 다시 쓸 때 **사람을 더하지 않는다** (2026-09-19).

## 무엇이 났나 — 실측 (컨트리로드 2판)

앰버(10세)의 `severely_injured` 참조가 안전 필터에 막혔다. 사니타이저가
프롬프트를 다시 썼고, 그 결과가 이랬다::

    ... clean and intact, with no bruises, cuts, wounds, blood, or signs of injury.
    ... Include the guardian naturally beside her without obscuring her face.

두 가지가 동시에 났다.

1. **요청한 상태가 지워졌다** — 「다친 모습」을 만들라고 했는데 「말짱한
   모습」이 나왔고, 그것이 `severely_injured` 참조로 저장됐다.
2. **사람이 둘이 됐다** — 참조는 신원을 정하는 그림이다. 둘이 들어가면
   하류 샷에서 신원이 섞인다(앞 샷 신원 혼입과 같은 부류).

뿌리는 사니타이저 팩의 한 줄이다::

    - 아동: 아동 위험 상황 → 보호자와 함께 있는 안전한 장면으로

씬 그림이면 맞는 정책인데, 참조에도 그대로 먹었다. 사니타이저가 씬용과
참조용을 안 가렸다.

## 어떻게 고쳤나 — 이미 있던 통로를 쓴다

`semantic_constraints` 와 `_STATE_GUIDANCE` 는 **이미 있었다**. 참조 경로만
그걸 안 넘기고 있었다. 새 재료를 만들지 않고 그 통로에 실었다.
"""
from __future__ import annotations

from app.modules.prompt_sanitizer import _apply_semantic_override


def test_the_override_says_exactly_one_figure():
    out = _apply_semantic_override("원본 프롬프트", {
        "semantic_mode": "reference_identity",
        "override_strategy_prefix": True,
        "subject": "앰버",
    })
    assert "Exactly ONE figure" in out
    assert "앰버" in out
    assert out.startswith("원본 프롬프트")


def test_the_override_keeps_the_requested_state():
    """★부상 묘사를 지우지 말라는 말은 `_STATE_GUIDANCE` 에 이미 있었다."""
    out = _apply_semantic_override("원본", {
        "semantic_mode": "reference_identity",
        "override_strategy_prefix": True,
        "subject": "앰버",
        "source_states": {"앰버": "severely_injured"},
    })
    assert "severely_injured" in out
    assert "Do not rewrite as unharmed" in out


def test_other_modes_are_untouched():
    """참조 모드를 더했다고 기존 갈래가 달라지면 안 된다."""
    assert _apply_semantic_override("원본", None) == "원본"
    assert _apply_semantic_override("원본", {"semantic_mode": "unknown"}) == "원본"


def test_the_reference_path_actually_passes_it(monkeypatch):
    """★★만들고 안 읽으면 소용없다 — **막혔을 때 실제로 나가는지** 태운다.

    소스에 문자열이 있나 보는 시험은 이 저장소에서 이미 결함을 통과시킨
    적이 있다. 그래서 `ModerationError` 를 한 번 일으키고, 사니타이저가
    무슨 제약을 받았는지, 두 번째 호출에 무엇이 실려 나갔는지 본다.
    """
    from app.modules.llm.gemini_image_client import ModerationError
    from app.modules.pipeline import ref_image_pipeline as rip

    seen: dict = {}
    sent: list = []

    class _Client:
        def set_context(self, **kw): pass
        def generate_image(self, prompt, **kw):
            sent.append(prompt)
            if len(sent) == 1:
                raise ModerationError("SAFETY", [], {})
            return b"\x89PNG", 1.0

    class _San:
        def __init__(self, *a, **k): pass
        def sanitize(self, prompt, reason, cats, attempt=1,
                     semantic_constraints=None):
            seen.update(semantic_constraints or {})
            return {"sanitized_prompt": prompt + " (다시 씀)",
                    "strategy": "film_previs"}

    # ★검열 대체 제공자 갈래를 **막는다** — 이 시험이 보려는 것은
    #  사니타이저에 무엇이 실려 나가는가다. 안 막으면 바깥으로 나간다
    #  (돈 가드가 잡아 줬다: openrouter 2회).
    monkeypatch.setattr(
        "app.modules.pipeline.cine_provider.build_cine_client",
        lambda *a, **k: (_ for _ in ()).throw(RuntimeError("시험에선 안 쓴다")))
    monkeypatch.setattr(rip, "PromptSanitizer", _San, raising=False)
    monkeypatch.setattr("app.modules.prompt_sanitizer.PromptSanitizer", _San)
    monkeypatch.setattr("app.modules.llm.openai_client.OpenAIClient",
                        lambda *a, **k: None)
    monkeypatch.setattr(rip, "_load_ref_image_prompt", lambda *a, **k: "")
    monkeypatch.setattr(rip, "validate_reference_image",
                        lambda *a, **k: {"severity": "ok", "score": 90})
    # ★함수 안에서 import 하므로 **원래 모듈**을 바꿔야 먹는다
    #  (모듈 속성으로 패치하면 AttributeError — 이 저장소의 지역 import 부류).
    monkeypatch.setattr("app.modules.png_metadata.embed_png_metadata",
                        lambda *a, **k: None)

    import pathlib, tempfile
    with tempfile.TemporaryDirectory() as d:
        rip.generate_and_validate_reference(
            gemini_client=_Client(),
            entity_name="앰버 (severely_injured)",
            entity_description="10세 여자아이",
            entity_type="character_state_variant",
            t2i_prompt="다친 앰버",
            output_dir=pathlib.Path(d),
            state_hint="severely_injured",
        )

    assert len(sent) == 2, "재시도가 안 돌았다 — 이 시험이 뜻을 잃는다"
    assert seen.get("semantic_mode") == "reference_identity", seen
    assert seen.get("single_subject_only") is True, (
        "참조인데 「사람 하나」 제약을 안 넘겼다: " + repr(seen))
    assert seen.get("source_states") == {"앰버 (severely_injured)":
                                         "severely_injured"}, seen
    assert seen.get("forbid_unharmed_rewrite") is True, seen
