"""④비평·⑤수정본·⑥재판정을 **한 스위치**로 (2026-08-29 사용자 지시).

> "4번부터 6번까지를 한꺼번에 disable / enable 가능하게하고 default 는 disable"

★새 플래그를 만들지 않았다(Codex 설계 리뷰). `still_recipe_critique_enabled`
 가 이미 같은 일을 한다 — 없는 것을 새로 만들면 같은 계약이 두 벌이 된다.

여기서 재는 것 셋:
  ① 기본이 **꺼짐**인가
  ② master OFF 면 ⑥(fix_rejudge)도 **같이** 꺼지는가 — 보조 플래그가 켜져
    있어도. 「한꺼번에」가 계약이다
  ③ master OFF 면 repair 전용 항목이 **지문에 안 접히는가** — 안 도는 단계의
    팩을 바꾼 것만으로 선정 롤이 stale 이 되면 안 된다
"""
from __future__ import annotations

import ast
import pathlib

import pytest

from app.core.config import Settings

BACKEND = pathlib.Path(__file__).resolve().parents[2]


# ── ① 기본값 ────────────────────────────────────────────────────

def test_repair_master_defaults_to_off():
    """★**코드 기본값**을 직접 읽는다 — 인스턴스를 만들어 재면 안 된다.

    첫 판은 `Settings(_env_file=None).still_recipe_critique_enabled` 를 봤다.
    **그것은 코드 기본값을 안 잰다** — `.env` 나 환경변수가 이기면 그 값이
    나오고, 마침 `.env` 도 false 라 **코드를 True 로 되돌려도 초록이었다**
    (양성 확인에서 잡혔다). 재려는 것을 안 재고 초록인 부류다.

    `model_fields[...].default` 는 클래스 선언의 값 그대로다.
    """
    default = Settings.model_fields["still_recipe_critique_enabled"].default
    assert default is False, (
        f"④~⑥ master 의 **코드 기본값**이 {default} 다 — "
        f"사용자 지시는 default disable")


# ── ② ⑥이 master 에 종속되는가 (AST — 조립부를 본다) ──────────────

def _guard_names(src: str, needle: str) -> set[str]:
    """`needle` 을 언급하는 `if` 조건 안에 함께 오는 이름들."""
    tree = ast.parse(src)
    out: set[str] = set()
    for node in ast.walk(tree):
        if not isinstance(node, ast.If):
            continue
        text = ast.dump(node.test)
        if needle not in text:
            continue
        for sub in ast.walk(node.test):
            if isinstance(sub, ast.Constant) and isinstance(sub.value, str):
                out.add(sub.value)
    return out


def test_fix_rejudge_wiring_is_gated_by_the_master():
    """★배선부 — master 가 꺼지면 ⑥ 함수 자체를 안 넘긴다.

    보조 플래그(`multiroll_fix_rejudge_enabled`)만 보고 켜면 master 를 꺼도
    ⑥만 남는다. 그러면 「한꺼번에」가 아니다.
    """
    src = (BACKEND / "app/services/still_recipe_service.py").read_text(
        encoding="utf-8")
    names = _guard_names(src, "multiroll_fix_rejudge_enabled")
    assert "still_recipe_critique_enabled" in names, (
        "⑥ 배선이 master 를 안 본다 — 조건에 있는 것: " + str(sorted(names)))


# ── ③ repair OFF 면 repair 전용 항목이 지문에 안 접히는가 ──────────

REPAIR_ONLY_OUTER = (
    "gg46_critique_pack",
    "gg46_fix_image_model",
    "fix_missing_pack",
    "fix_ref_contract",
    "multiroll_fix_rejudge",
    "fix_rejudge_header_pack",
)


@pytest.mark.parametrize("key", REPAIR_ONLY_OUTER)
def test_repair_only_keys_are_behind_the_master_in_outer_hash(key):
    """outer 지문에서 그 키를 넣는 `if` 가 master 를 함께 봐야 한다.

    ★안 도는 단계의 팩·모델을 바꾼 것만으로 **선정 롤이 stale** 이 되면
     무관한 재생성이 난다. master 기본이 OFF 로 바뀌어 그 갈래가 흔해졌다.
    """
    src = (BACKEND / "app/core/steps/image_steps.py").read_text(
        encoding="utf-8")
    tree = ast.parse(src)
    guarded = False
    for node in ast.walk(tree):
        if not isinstance(node, ast.If):
            continue
        body_text = "".join(ast.dump(n) for n in node.body)
        if f"'{key}'" not in body_text and f'"{key}"' not in body_text:
            continue
        if "still_recipe_critique_enabled" in ast.dump(node.test):
            guarded = True
            break
    assert guarded, (
        f"outer 지문의 '{key}' 가 master 밖에서 접힌다 — repair 를 꺼도 "
        f"그 팩을 바꾸면 선정 롤이 stale 이 된다")


def test_shot_fingerprint_gg46_keys_are_behind_the_master():
    """샷 지문의 GG46 repair 키 — `if critique_enabled:` 안에서만."""
    src = (BACKEND / "app/services/still_recipe_service.py").read_text(
        encoding="utf-8")
    tree = ast.parse(src)
    guarded = set()
    for node in ast.walk(tree):
        if not isinstance(node, ast.If):
            continue
        if "critique_enabled" not in ast.dump(node.test):
            continue
        body = "".join(ast.dump(n) for n in node.body)
        for key in ("gg46_critique_pack", "gg46_fix_image_model"):
            if f"'{key}'" in body or f'"{key}"' in body:
                guarded.add(key)
    assert guarded >= {"gg46_critique_pack", "gg46_fix_image_model"}, (
        "샷 지문의 GG46 repair 키가 master 밖에서 접힌다 — 잡힌 것: "
        + str(sorted(guarded)))


def test_shot_fingerprint_fix_ref_keys_are_behind_the_master():
    """★fix-ref 세 키도 master 뒤에 있어야 한다 (2026-08-29 Codex BLOCK).

    앞 판은 GG46 **두 키만** 보고 fix-ref 세 키를 안 봤다. 그래서
    `fix_ref_gate_on` 이 master 를 안 보는 것을 **못 잡았다** — 그리고
    현재 설정이 정확히 그 갈래였다(master OFF + gate ON).
    래칫을 넓혔다면 **그 결함의 이름이 목록에 있어야 한다.**

    ★값이 아니라 **`fix_ref_gate_on` 이라는 한 값**이 master 를 보는지
     본다 — 그 값 하나가 스키마·조립·`variant_kwargs`·지문을 함께 닫는다.
    """
    src = (BACKEND / "app/services/still_recipe_service.py").read_text(
        encoding="utf-8")
    tree = ast.parse(src)
    found = False
    for node in ast.walk(tree):
        if not isinstance(node, ast.Assign):
            continue
        names = {t.id for t in node.targets if isinstance(t, ast.Name)}
        if "fix_ref_gate_on" not in names:
            continue
        found = True
        assert "critique_enabled" in ast.dump(node.value), (
            "`fix_ref_gate_on` 이 master 를 안 본다 — master OFF 인데 "
            "샷 지문에 선별 팩·계약이 들어가 안 돌린 단계의 문안 변경만으로 "
            "롤이 stale 이 된다")
    assert found, "`fix_ref_gate_on` 을 못 찾았다 — 이름이 바뀌었나"


def test_fix_ref_keys_only_appear_when_the_gate_value_is_on():
    """세 키가 **그 한 값** 뒤에서만 나온다 — 다른 조건으로 새지 않게."""
    src = (BACKEND / "app/services/still_recipe_service.py").read_text(
        encoding="utf-8")
    tree = ast.parse(src)
    keys = ("fix_ref_gate", "fix_missing_pack", "fix_ref_contract")
    guarded = set()
    for node in ast.walk(tree):
        if not isinstance(node, ast.If):
            continue
        if "fix_ref_gate_on" not in ast.dump(node.test):
            continue
        body = "".join(ast.dump(n) for n in node.body)
        for key in keys:
            if f"'{key}'" in body or f'"{key}"' in body:
                guarded.add(key)
    assert guarded >= set(keys), (
        "fix-ref 지문 키가 `fix_ref_gate_on` 밖에서 접힌다 — 잡힌 것: "
        + str(sorted(guarded)))
