"""덜어내기가 **조립이 실제로 쓴 판**을 찾는다 (2026-09-18 컨트리로드 실측).

## 무엇이 결함이었나

안전 사다리의 교차 백엔드 단계가 S71sh13 에서 한 번도 못 발화했다:

    safety ladder cross_backend 단계 백엔드 비호환
    (grok 텍스트 총량 8653B > 8000B(모델 상한))

압축 루틴은 **있었는데 한 절도 못 덜어냈다.** 조립은 절을
`guidance_version or prompt_version` 판으로 렌더하는데,

* grok 이 **주 백엔드**면 guidance=컴팩트(v17) → 덜어내기가 찾는 판과 같다
* nb2 가 주 백엔드고 grok 이 **사다리의 교차 백엔드**면 프롬프트엔 v1/3/4/5
  판 문안이 들어 있어 **컴팩트 문자열이 하나도 안 맞는다**

그래서 `shed=[]` 로 돌아오고 8,653B 그대로 상한에 걸려 단계가 죽었다.
실측한 그 프롬프트를 다른 판으로 찾으면 두 절이 빠져 7,461B — 들어간다.

## 이 시험이 잠그는 것

① nb2 조립 프롬프트도 덜어내진다(결함 재현 — 고치기 전엔 `shed==[]`)
② grok 조립 프롬프트는 **종전 그대로**(컴팩트 판이 1순위)
③ 일반 규칙 절이 없는 글은 한 글자도 안 건드린다
④ 끝점: 상한을 넘던 nb2 프롬프트가 **실제로 나간다**(payload 로 확인)
"""
from __future__ import annotations

import json

import pytest

from app.modules.pipeline.still_recipe import (
    GROK_SHED_STEM_SELECTOR,
    STILL_COMPACT_PROMPT_VERSION,
    build_still_prompt,
    fit_grok_prompt,
    resolve_prompt_version,
)
from app.modules.prompt_loader import load_prompt

_MODULE = "still_recipe"


def _assemble(guidance: str) -> str:
    """프로덕션 조립 그대로 — 대역을 안 쓴다(같은 함수가 실제로 만든 글)."""
    return build_still_prompt(
        shot_desc="a man stands by the window " * 60,
        place_text="a narrow room with a low ceiling " * 40,
        time_of_day_en="night", world_anchor="", prompt_version="1",
        prev_used=False, bg_only=False,
        conduct_version="10", guidance_version=guidance,
    )


def _b(text: str) -> int:
    return len(text.encode("utf-8"))


# ── ① 결함: nb2 조립 프롬프트 ───────────────────────────────────────

def test_a_prompt_from_the_nb2_pack_gets_shed():
    """고치기 전엔 여기서 `shed == []` 이었고 롤이 죽었다."""
    prompt = _assemble(guidance="")
    limit = _b(prompt) - 600
    out, shed = fit_grok_prompt(prompt, 0, limit)
    assert shed, "nb2 판으로 조립한 글에서 한 절도 못 덜어냈다"
    assert _b(out) <= limit
    assert _b(out) < _b(prompt)


def test_the_shed_clause_really_leaves_the_prompt():
    """덜어냈다고 적은 절이 실제로 글에서 사라졌는지 — 이름만 쌓지 않게."""
    prompt = _assemble(guidance="")
    out, shed = fit_grok_prompt(prompt, 0, _b(prompt) - 600)
    for stem in shed:
        gone = [v for v in ("1", "10", STILL_COMPACT_PROMPT_VERSION)
                if _text(stem, v) and _text(stem, v) in prompt
                and _text(stem, v) not in out]
        assert gone, f"{stem} 을 덜어냈다는데 글에 그대로 있다"


def _text(stem: str, selector: str) -> str:
    try:
        return load_prompt(
            _MODULE, stem, version=resolve_prompt_version(selector)).strip()
    except Exception:
        return ""


# ── ② grok 조립은 종전 그대로 ───────────────────────────────────────

def test_the_grok_assembly_path_still_sheds_the_compact_texts():
    """1순위가 컴팩트 판이라 grok2 주행이 보내는 바이트는 안 변한다."""
    prompt = _assemble(guidance=STILL_COMPACT_PROMPT_VERSION)
    out, shed = fit_grok_prompt(prompt, 0, _b(prompt) - 400)
    assert shed
    for stem in shed:
        sel = GROK_SHED_STEM_SELECTOR.get(stem, STILL_COMPACT_PROMPT_VERSION)
        compact = _text(stem, sel)
        if compact and compact in prompt:
            assert compact not in out, (
                f"{stem}: 컴팩트 판이 글에 있는데 다른 판을 지웠다")


def test_the_grok_path_is_byte_identical_to_the_no_fallback_run(monkeypatch):
    """★전후 등가 — 「다른 판도 본다」를 **꺼서** 종전 동작과 글자까지 같은지.

    Codex 리뷰 지적: 「컴팩트 문안이 사라졌다」만 보면 종전과 같은 바이트가
    나간다는 증명이 못 된다. 되돌릴 판이 하나도 없을 때(=옛 동작)와 결과
    (`out`, `shed`)가 **완전히 같아야** 한다 — 안 그러면 grok2 주행의 완료
    샷이 새 글로 다시 만들어진다.
    """
    prompt = _assemble(guidance=STILL_COMPACT_PROMPT_VERSION)
    limit = _b(prompt) - 400

    new_out, new_shed = fit_grok_prompt(prompt, 0, limit)
    monkeypatch.setattr(
        "app.modules.prompt_loader.pack_versions_with_stem",
        lambda *a, **kw: [])
    old_out, old_shed = fit_grok_prompt(prompt, 0, limit)

    assert new_shed == old_shed, "덜어낸 절 목록이 달라졌다"
    assert new_out == old_out, "grok 조립 경로가 보내는 글이 달라졌다"


# ── ③ 없는 것은 안 지운다 ───────────────────────────────────────────

def test_a_prompt_without_general_rules_is_untouched():
    prompt = "SHOT TEXT: " + ("어떤 남자가 창가에 서 있다. " * 300)
    out, shed = fit_grok_prompt(prompt, 0, 100)
    assert shed == []
    assert out == prompt, "맞는 절이 없으면 한 글자도 안 건드린다"


# ── ④ 끝점: 실제로 나가는 payload ───────────────────────────────────

def test_the_over_limit_prompt_actually_gets_sent(monkeypatch):
    """조립 자리 말고 **나가는 몸통**에서 잰다 — 상한 초과로 서면 안 된다."""
    import urllib.request

    from app.modules.llm import grok_image_client as gic

    prompt = _assemble(guidance="")
    label = "SOURCE IMAGE:"
    monkeypatch.setattr(gic, "GROK_TEXT_BYTE_LIMIT", _b(prompt) - 600)
    monkeypatch.setattr(gic, "GROK_TEXT_BYTE_HARD_LIMIT", _b(prompt) - 500)
    assert _b(prompt) + _b(label) > gic.GROK_TEXT_BYTE_HARD_LIMIT, (
        "덜어내지 않으면 못 보내는 입력이어야 이 시험이 뜻이 있다")

    sent: dict = {}

    class _Fail(RuntimeError):
        pass

    def _urlopen(req, timeout=None):
        sent["body"] = json.loads(req.data.decode("utf-8"))
        raise _Fail("여기까지 왔으면 상한은 통과한 것이다")

    monkeypatch.setattr(urllib.request, "urlopen", _urlopen)
    monkeypatch.setattr("app.core.config.settings.openrouter_api_key",
                        "k-test", raising=False)
    monkeypatch.setattr("app.core.config.settings.llm_max_retries", 0,
                        raising=False)

    client = gic.GrokImageClient()
    with pytest.raises(Exception) as exc:
        client.generate_image(prompt, labeled_references=[(label, b"\x89PNG")])
    assert not isinstance(exc.value, gic.GrokPromptOverBudget), (
        "덜어낼 절이 글에 있는데도 상한 초과로 섰다")

    text_out = sent["body"]["messages"][0]["content"][0]["text"]
    assert _b(text_out) + _b(label) <= gic.GROK_TEXT_BYTE_HARD_LIMIT
    assert _b(text_out) < _b(prompt), "짧아지지 않은 글이 나갔다"


def test_it_still_refuses_when_shedding_is_not_enough(monkeypatch):
    """덜어내도 안 맞으면 종전대로 선다 — 사다리가 그 단계만 건너뛴다."""
    from app.modules.llm import grok_image_client as gic

    prompt = _assemble(guidance="")
    monkeypatch.setattr(gic, "GROK_TEXT_BYTE_LIMIT", 500)
    monkeypatch.setattr(gic, "GROK_TEXT_BYTE_HARD_LIMIT", 600)
    monkeypatch.setattr("app.core.config.settings.openrouter_api_key",
                        "k-test", raising=False)
    client = gic.GrokImageClient()
    with pytest.raises(gic.GrokPromptOverBudget):
        client.generate_image(prompt, labeled_references=[("SRC:", b"x")])
