"""multiroll_judge 팩 v2 — 프레이밍 우선 판정 계약 결정론 테스트.

E2E6 피드백 ⑥ 근본 대응 (S23sh1 판정 역선택). VLM 완성도=표적 canary/
E2E 육안 — 여기는 팩 해석·계약 문구·소비처 스탬프만 잠근다.
"""
from pathlib import Path

import pytest

from app.modules.pipeline.multiroll_gemini import (
    JUDGE_PACK_VERSION,
    resolve_judge_pack_version,
    resolve_judge_texts,
)


def _norm(text: str) -> str:
    return " ".join(text.lower().split())


def test_selectors_split_still_from_the_rest():
    """v7 은 **스틸 전용**이다 (2026-08-07 Codex 리뷰 수용).

    전역 기본을 올리면 구조물 씨드·배경 플레이트의 config_hash 까지 바뀌어
    인물이 없는 산출이 통째로 재실행 대상이 된다. 인물 물리 축을 그쪽에
    적용하려고 그 비용을 낼 이유가 없다.
    """
    from app.modules.pipeline.multiroll_gemini import (
        FIX_REJUDGE_HEADER_PACK_VERSION,
        STILL_FIX_REJUDGE_HEADER_PACK_VERSION,
        STILL_JUDGE_PACK_VERSION,
    )

    # ★#1-D 잔여 (2026-08-27): 전역 기본이 6 → **16** 으로 올라갔다.
    #  v16 은 **v6 를 바탕으로 `judge_still`·`critique` 두 stem 의 탈것
    #  낱말만 걷은 판**이다. v7~v15 의 스틸 전용 내용(인물 물리 축 등)은
    #  가져오지 않았다 — 아래 내용 시험이 그것을 잠근다.
    assert JUDGE_PACK_VERSION == "16"         # 전역 기본 — 비스틸 소비자
    # v14 (감사 1-D, 2026-08-27): BUILT SPACE 절에서 탈것 전용 낱말을
    # 걷었다. ★생성 팩(still_recipe v30)과 **같은 커밋에서** 올린다 —
    # 한쪽만 고치면 판정이 옛 기준으로 계속 반려해 재시도가 돈다.
    # ★#1-D 잔여 (2026-08-27): v14 는 `judge_still` 만 일반화했다.
    #  v15 가 `critique`·`gq_observe_sys` 까지 같이 올린 판이다.
    assert STILL_JUDGE_PACK_VERSION == "15"
    assert FIX_REJUDGE_HEADER_PACK_VERSION == "3"
    # 2026-08-13 #106: v10 = v7 수정 범위 축 + IDENTITY 자동 패배 축
    # (S39sh4 fix 인물 변형 채택이 근거).
    assert STILL_FIX_REJUDGE_HEADER_PACK_VERSION == "10"
    assert resolve_judge_pack_version("10") == "10.202608131121"
    assert resolve_judge_pack_version("12") == "12.202608141305"
    assert resolve_judge_pack_version("7") == "7.202608071100"
    assert resolve_judge_pack_version("6") == "6.202608062200"
    assert resolve_judge_pack_version("5") == "5.202607230000"
    assert resolve_judge_pack_version("2") == "2.202607161610"
    assert resolve_judge_pack_version("1") == "1.202607132300"
    with pytest.raises(ValueError):
        resolve_judge_pack_version("99")


def test_judge_v5_direction_and_fix_feasibility_contract():
    texts = resolve_judge_texts(3)
    j = _norm(texts["judge_sys"])
    assert "vertical (up/down) direction" in j
    assert "hard violation" in j
    c = _norm(texts["critique_sys"])
    assert "fix feasibility" in c
    assert '"unfixable": true' in c
    assert "never instruct the editor to relocate the camera" in c


def test_judge_v2_framing_priority_contract():
    texts = resolve_judge_texts(3)
    j = _norm(texts["judge_sys"])
    # 엄격 우선순위 명시 + 샷 텍스트 프레이밍=최상위 양(+) 축
    assert "strict priority order" in j
    assert "shot text's staging" in j
    assert "highest positive axis" in j
    # 와이드 가점·프레임 밖 감점 금지 계약
    assert "never penalise a candidate because the framing" in j
    assert "never reward a candidate for widening" in j
    # hard violation 이 최우선(실격)
    assert "hard violations" in j
    # 포맷 변수 치환 확인
    assert "three candidate" in j
    assert "a, b, c" in j


def test_critique_v2_visible_within_frame_rule():
    texts = resolve_judge_texts(3)
    c = _norm(texts["critique_sys"])
    assert "framing scope rule" in c
    assert "is not an issue" in c
    # fix 가 프레이밍을 넓히는 것 금지 + 기존 프레이밍 보존
    assert "never propose a fix that widens" in c
    assert "preserve the existing framing exactly" in c


def test_v1_pack_still_resolvable_for_audit():
    texts = resolve_judge_texts(3, pack_version="1")
    assert "strict priority order" not in _norm(texts["judge_sys"])


def test_v2_pack_full_file_set_and_neutrality():
    repo = Path(__file__).resolve().parents[3]
    d = repo / "prompts" / "_base" / "multiroll_judge" / (
        resolve_judge_pack_version("2"))
    names = sorted(p.name for p in d.glob("*.md"))
    assert names == sorted([
        "critique.md", "fix_head.md", "fix_label.md", "fix_tail.md",
        "judge_plate_ext.md", "judge_still.md",
    ])
    for p in d.glob("*.md"):
        text = p.read_text(encoding="utf-8").lower()
        for word in ("rooftop", "villa", "모니터", "pc방", "백팩"):
            assert word not in text, f"{p.name} 에 {word!r} 하드코딩"


def test_consumer_hash_stamps_resolve_current_pack():
    """소비처 config_hash 스탬프=실사용 selector 해석값 (하드코딩 금지)."""
    import inspect

    from app.core.steps import background_render_step, image_steps
    from app.core.steps import outdoor_structure_seed_step

    for mod in (image_steps, background_render_step,
                outdoor_structure_seed_step):
        src = inspect.getsource(mod)
        assert 'judge_pack"] = "1.202607132300"' not in src
        assert '"multiroll_judge_pack": "1.202607132300"' not in src


def test_durable_fingerprints_resolve_current_pack():
    """Codex 배치 리뷰 HIGH-3: sidecar/records 지문도 judge 팩 해석값 —
    literal v1 잔존 금지 + resolve 호출 실재."""
    import inspect

    from app.modules.pipeline import plate_multiroll
    from app.services import still_recipe_service

    for mod in (plate_multiroll, still_recipe_service):
        src = inspect.getsource(mod)
        assert '"judge_pack": "1.202607132300"' not in src
        assert "resolve_judge_pack_version" in src


# ── 팩 v7 (2026-08-07) ────────────────────────────────────────────────
#
# 근거는 심판 4종 대조다. 걷는 자세를 가로로 눕혀 띄운 후보에 Opus·Sol·Qwen
# 셋이 최고점을 줬고 셋 다 부유를 한 줄도 쓰지 않았다 — v6 도 hard 예시
# 목록엔 적어 두었으나 목록에 있는 것과 보게 만드는 것은 다르다.


def test_judge_v7_physics_is_a_forced_reading_axis():
    j = _norm(resolve_judge_texts(3, pack_version="7")["judge_sys"])
    assert "four axes" in j                    # 세 축 → 네 축
    assert "physics" in j
    # 무엇이 받치는지를 이름으로 대게 한다
    assert "name the support you see" in j
    # 지지가 없으면 하드 위반 — 다만 어색함은 아니다
    assert "nothing supports it" in j
    # 샷 텍스트가 공중이라 해도 면책이 아니다
    assert "does not excuse this" in j
    # 세 축은 서로 상쇄되지 않는다
    assert "direction, built space or physics" in j


def test_judge_v7_keeps_v6_three_axes():
    j = _norm(resolve_judge_texts(3, pack_version="7")["judge_sys"])
    for axis in ("direction", "built space", "entities"):
        assert axis in j
    assert "a banknote has a country" in j


def test_v7_fix_rejudge_header_has_scope_axis():
    from app.modules.pipeline.multiroll_gemini import load_fix_rejudge_header

    from app.modules.pipeline.multiroll_gemini import (
        STILL_FIX_REJUDGE_HEADER_PACK_VERSION,
    )

    h = _norm(load_fix_rejudge_header(STILL_FIX_REJUDGE_HEADER_PACK_VERSION))
    # 중립 계약(v3)은 승계
    assert "do not assume or reward any production history" in h
    # 지시하지 않은 변경은 손실이고, 대등하면 덜 바꾼 쪽
    assert "a change the contract never asked for is a loss" in h
    assert "prefer the one that changed less" in h


def test_v7_fix_ref_label_exists_and_forbids_editing_it():
    lab = _norm(resolve_judge_texts(3, pack_version="7")["fix_ref_label"])
    assert lab                                  # 최신 팩엔 반드시 실재
    assert "not the photograph to edit" in lab
    assert "original label" in lab


def test_old_packs_have_no_fix_ref_label():
    """구 팩으로 내려도 부팅이 깨지지 않고, 빈 값이 '기능 없음'을 뜻한다."""
    assert resolve_judge_texts(3, pack_version="6")["fix_ref_label"] == ""


def test_stem_requirement_uses_numeric_version_compare():
    """문자열 비교면 "10" < "7" 이라 v10+ 에서 fail-closed 가 풀린다."""
    from app.modules.pipeline.multiroll_gemini import _selector_num

    assert _selector_num("10") > _selector_num("7")
    assert _selector_num("7") == 7
    assert _selector_num("이상한값") == -1   # 해석 불가는 도입 팩 미만 취급


def test_the_global_pack_does_not_carry_the_still_only_axis():
    """★**번호가 아니라 내용으로 잠근다** (2026-08-27).

    이 시험이 원래 지키던 것은 「전역 기본이 6 이다」가 아니라
    **「스틸 전용 내용이 전역으로 새지 않는다」**였다 — 새면 인물이
    없는 산출(배경 플레이트·구조물 씨드)까지 재실행 범위에 들어온다.

    번호만 못박으면 판을 올릴 때마다 이 시험이 무슨 뜻이었는지 잊고
    숫자만 고치게 된다. 그래서 실제 계약을 잰다: 스틸 판에만 있는
    **PHYSICS 축**이 전역 판에는 없어야 한다.
    """
    from app.modules.pipeline.multiroll_gemini import (
        JUDGE_PACK_VERSION,
        STILL_JUDGE_PACK_VERSION,
        resolve_judge_pack_version,
    )
    from app.modules.prompt_loader import load_prompt

    g = load_prompt("multiroll_judge", "judge_still",
                    version=resolve_judge_pack_version(JUDGE_PACK_VERSION))
    s_ = load_prompt("multiroll_judge", "judge_still",
                     version=resolve_judge_pack_version(
                         STILL_JUDGE_PACK_VERSION))
    assert "PHYSICS" in s_, "스틸 판에서 인물 물리 축이 사라졌다"
    assert "PHYSICS" not in g, (
        "스틸 전용 축이 전역 기본으로 샜다 — 인물 없는 산출까지 "
        "재실행 범위에 들어온다")
