"""background_unmanned — 무인 계약·게이트 결정론 테스트 (이식 ①)."""
from pathlib import Path

import pytest

from app.modules.pipeline.background_unmanned import (
    GATE_SCHEMA,
    build_no_people_clause,
    build_retry_clause,
    resolve_prompt_version,
    run_unmanned_gate,
)


def test_resolve_prompt_version():
    assert resolve_prompt_version("1").startswith("1.")
    with pytest.raises(ValueError):
        resolve_prompt_version("99")


def test_clauses_are_generic_english():
    clause = build_no_people_clause()
    retry = build_retry_clause()
    assert "NO people" in clause
    assert clause != retry
    # 범용 계약 — 시나리오 고유명 0 (계약 표면만 검증)
    assert "UNMANNED" in clause


def test_gate_schema_shape():
    assert GATE_SCHEMA["required"] == ["people_visible", "evidence_ko"]


def _ok_info(out: Path) -> dict:
    return {"status": "ok", "png_path": str(out)}


def test_gate_noop_on_failed_render(tmp_path):
    info = {"status": "failed"}
    out = run_unmanned_gate(
        info=info, out_path=tmp_path / "x.png",
        retry_fn=lambda: pytest.fail("retry 금지"),
        judge_fn=lambda p: pytest.fail("판정 금지"),
    )
    assert out is info and "unmanned_gate" not in out


def test_gate_pass_records_verdict(tmp_path):
    png = tmp_path / "bg.png"
    png.write_bytes(b"v1")
    info = _ok_info(png)
    out = run_unmanned_gate(
        info=info, out_path=png,
        retry_fn=lambda: pytest.fail("무위반은 재렌더 금지"),
        judge_fn=lambda p: {"people_visible": False, "evidence_ko": "무인"},
    )
    assert out["unmanned_gate"]["attempt1"]["people_visible"] is False
    assert "people_detected" not in out


def test_gate_judge_error_fails_open(tmp_path):
    png = tmp_path / "bg.png"
    png.write_bytes(b"v1")

    def boom(p):
        raise RuntimeError("vlm down")

    out = run_unmanned_gate(
        info=_ok_info(png), out_path=png,
        retry_fn=lambda: pytest.fail("판정 실패는 재렌더 금지"),
        judge_fn=boom,
    )
    assert out["status"] == "ok"
    assert "error" in out["unmanned_gate"]


def test_gate_violation_retries_once_and_rejudges(tmp_path):
    png = tmp_path / "bg.png"
    png.write_bytes(b"v1")
    verdicts = iter(
        [{"people_visible": True, "evidence_ko": "인물"},
         {"people_visible": False, "evidence_ko": "무인"}]
    )
    calls = {"retry": 0}

    def retry():
        calls["retry"] += 1
        png.write_bytes(b"v2")
        return _ok_info(png)

    out = run_unmanned_gate(
        info=_ok_info(png), out_path=png,
        retry_fn=retry, judge_fn=lambda p: next(verdicts),
    )
    assert calls["retry"] == 1
    assert out["unmanned_gate"]["attempt1"]["people_visible"] is True
    assert out["unmanned_gate"]["attempt2"]["people_visible"] is False
    assert "people_detected" not in out
    # 원본 백업 보존
    assert (tmp_path / "bg.unmanned_v1.png").read_bytes() == b"v1"
    assert png.read_bytes() == b"v2"


def test_gate_still_violating_marks_audit(tmp_path):
    png = tmp_path / "bg.png"
    png.write_bytes(b"v1")
    out = run_unmanned_gate(
        info=_ok_info(png), out_path=png,
        retry_fn=lambda: (_ok_info(png)),
        judge_fn=lambda p: {"people_visible": True, "evidence_ko": "인물"},
    )
    assert out["people_detected"] is True


def test_gate_retry_failure_restores_original(tmp_path):
    png = tmp_path / "bg.png"
    png.write_bytes(b"v1")

    def retry():
        png.unlink()  # 재렌더 실패로 산출 소실 시뮬레이션
        return {"status": "failed"}

    out = run_unmanned_gate(
        info=_ok_info(png), out_path=png,
        retry_fn=retry,
        judge_fn=lambda p: {"people_visible": True, "evidence_ko": "인물"},
    )
    assert out["people_detected"] is True
    # 백업 복원 — 산출 0 방지
    assert png.read_bytes() == b"v1"


def test_render_prompt_off_is_byte_identical(monkeypatch, tmp_path):
    """flag OFF: render_one_background 프롬프트에 무인 절 미부착."""
    from app.core.config import settings
    from app.modules.pipeline import background_render as br

    monkeypatch.setattr(
        settings, "background_no_people_enabled", False, raising=False)
    seen = {}

    def fake_call(client, **kw):
        seen["prompt"] = kw["prompt"]
        return b"png"

    monkeypatch.setattr(br, "call_gpt_image_bytes", fake_call)
    monkeypatch.setattr(br, "reserve_current_call", lambda **kw: None)
    info = br.render_one_background(
        openai_client=object(), image_model="gpt-image-2",
        prompt="BASE PROMPT", out_path=tmp_path / "o.png",
        fp_path=None, prior_bg_paths=[],
    )
    assert info["status"] == "ok"
    assert seen["prompt"] == "BASE PROMPT"
    assert "no_people_clause_attached" not in info


def test_render_prompt_on_attaches_clause(monkeypatch, tmp_path):
    from app.core.config import settings
    from app.modules.pipeline import background_render as br

    monkeypatch.setattr(
        settings, "background_no_people_enabled", True, raising=False)
    monkeypatch.setattr(
        settings, "background_no_people_gate_enabled", False, raising=False)
    seen = {}

    def fake_call(client, **kw):
        seen["prompt"] = kw["prompt"]
        return b"png"

    monkeypatch.setattr(br, "call_gpt_image_bytes", fake_call)
    monkeypatch.setattr(br, "reserve_current_call", lambda **kw: None)
    info = br.render_one_background(
        openai_client=object(), image_model="gpt-image-2",
        prompt="BASE PROMPT", out_path=tmp_path / "o.png",
        fp_path=None, prior_bg_paths=[],
    )
    assert info["status"] == "ok"
    assert seen["prompt"].startswith("BASE PROMPT\n\n")
    assert build_no_people_clause() in seen["prompt"]
    assert info["no_people_clause_attached"] is True


def test_gate_retry_exception_restores_and_audits(tmp_path):
    """Codex 리뷰 4: retry callable 예외 = 원본 복원 + 감사 (전파 금지)."""
    png = tmp_path / "bg.png"
    png.write_bytes(b"v1")

    def retry():
        png.unlink()
        raise RuntimeError("retry crashed")

    out = run_unmanned_gate(
        info=_ok_info(png), out_path=png,
        retry_fn=retry,
        judge_fn=lambda p: {"people_visible": True, "evidence_ko": "인물"},
    )
    assert png.read_bytes() == b"v1"  # 게이트는 렌더를 파괴하지 않는다
    assert out["people_detected"] is True
    assert "retry crashed" in out["unmanned_gate"]["retry_error"]


def test_gate_retry_budget_exception_restores_then_reraises(tmp_path):
    from app.core.image_call_budget import ImageCallBudgetExceeded

    png = tmp_path / "bg.png"
    png.write_bytes(b"v1")

    def retry():
        png.unlink()
        raise ImageCallBudgetExceeded(cap=1, used=1, source="test")

    with pytest.raises(ImageCallBudgetExceeded):
        run_unmanned_gate(
            info=_ok_info(png), out_path=png,
            retry_fn=retry,
            judge_fn=lambda p: {
                "people_visible": True, "evidence_ko": "인물"},
        )
    assert png.read_bytes() == b"v1"  # 전파 전 복원


def test_audit_fields_subset_and_off_empty():
    """E2E10 실측(감사 영속 결함): render info 의 무인 감사 3필드가 CP
    whitelist 에서 탈락 — people_detected 분포 acceptance 검증 불가.
    audit_fields 는 존재하는 감사 키만 추리고(OFF/부재=빈 dict — CP
    byte-identical), 값은 그대로 통과시킨다."""
    from app.modules.pipeline.background_unmanned import audit_fields

    assert audit_fields({}) == {}
    assert audit_fields({"status": "ok", "attempts": 2}) == {}
    info = {
        "status": "ok",
        "no_people_clause_attached": True,
        "unmanned_gate": {"attempt1": {"people_present": False}},
        "people_detected": True,
    }
    assert audit_fields(info) == {
        "no_people_clause_attached": True,
        "unmanned_gate": {"attempt1": {"people_present": False}},
        "people_detected": True,
    }


def test_step_entry_sites_all_spread_audit_fields():
    """Codex BLOCKING 재리뷰: render_one_background top-level CP entry
    조립 4곳 전부 감사 spread 필수 — 일부만 배선되면 shot-aware/stage-chain
    경로에서 감사 필드가 계속 탈락한다. 소스 잠금(focused): render 호출
    수 == spread 수."""
    import inspect

    from app.core.steps import background_render_step as mod

    src = inspect.getsource(mod)
    calls = src.count("info = render_one_background(")
    spreads = src.count("**_unmanned_audit_fields(info)")
    assert calls == spreads, (
        f"render_one_background 호출 {calls}곳 vs 감사 spread {spreads}곳 — "
        "새 entry 조립부에 **_unmanned_audit_fields(info) 누락")
    assert calls >= 4  # 현행 4 site — 줄어들면 구조 변경 재검토
