"""실내 shared-model pose guide service 결정론 테스트 (Wave5, T5·T6).

gate/cache_key/no-guide 분기 + guide QC 판정. 실제 gpt-image-2 edit 은
monkeypatch(생성은 canary 육안). 순수 분기만 검증.
"""
import app.services.indoor_shared_pose_guide_service as svc
from app.services.indoor_shared_pose_guide_service import (
    build_indoor_pose_guide,
    evaluate_guide_qc,
    guide_cache_key,
)

_BRIEF = {"figures": [{"slot": "foreground figure", "screen_zone": "left",
                       "depth_plane": "foreground", "gesture": None, "pose": None, "facing": None}],
          "support_clause": "each figure must be supported by a visible surface beneath it",
          "framing": "wide", "contact_locked": True, "skipped_reason": None,
          "field_diagnostics": {}}


# ── Task 5: gate / cache key / no-guide 분기 ────────────────────────────

def test_cache_key_changes_with_inputs():
    k1 = guide_cache_key(scene_index=1, bg_id="bgA", member_keys=[(1, 1), (1, 2)],
                         pose_brief=_BRIEF, prompt_version="1", model="gpt-image-2", bg_asset_hash="h1")
    k2 = guide_cache_key(scene_index=1, bg_id="bgA", member_keys=[(1, 1), (1, 2)],
                         pose_brief=_BRIEF, prompt_version="1", model="gpt-image-2", bg_asset_hash="h2")
    assert k1 != k2


def test_gate_no_plate_returns_none(tmp_path):
    png, diag = build_indoor_pose_guide(group_id="g1", pose_brief=_BRIEF, env_bg_bytes=None,
                                        bg_key="bgA", cache_dir=tmp_path)
    assert png is None and diag["reason"] == "no_environment_bg_plate"


def test_gate_no_figures_returns_none(tmp_path):
    empty = {**_BRIEF, "figures": []}
    png, diag = build_indoor_pose_guide(group_id="g1", pose_brief=empty, env_bg_bytes=b"x",
                                        bg_key="bgA", cache_dir=tmp_path)
    assert png is None and diag["reason"] == "no_figures"


def test_qc_fail_returns_none(tmp_path, monkeypatch):
    monkeypatch.setattr(svc, "_generate_guide_png", lambda **kw: b"\x89PNG_fake_guide_bytes_over_1k" + b"0" * 1100)
    png, diag = build_indoor_pose_guide(group_id="g1", pose_brief=_BRIEF, env_bg_bytes=b"x",
                                        bg_key="bgA", cache_dir=tmp_path,
                                        qc_fn=lambda png, **_k: (False, "photoreal_person"))
    assert png is None and diag["status"] == "qc_failed" and diag["qc_reason"] == "photoreal_person"


def test_qc_pass_writes_and_returns(tmp_path, monkeypatch):
    fake = b"\x89PNG_fake_guide_bytes_over_1k" + b"0" * 1100
    monkeypatch.setattr(svc, "_generate_guide_png", lambda **kw: fake)
    png, diag = build_indoor_pose_guide(group_id="g1", pose_brief=_BRIEF, env_bg_bytes=b"x",
                                        bg_key="bgA", cache_dir=tmp_path,
                                        qc_fn=lambda png, **_k: (True, None))
    assert png == fake and diag["status"] == "generated"


# ── disposition capture (QC 결과 → accepted|rejected, 거부본도 캔버스 노출) ─────

def _capture_recorder(monkeypatch):
    calls = []
    monkeypatch.setattr(
        svc, "_capture_guide",
        lambda png, *, disposition, input_image_ids, prompt, pipeline_metadata: calls.append(
            {"disposition": disposition, "input_image_ids": input_image_ids,
             "pipeline_metadata": pipeline_metadata}))
    return calls


def test_qc_pass_captures_accepted(tmp_path, monkeypatch):
    fake = b"\x89PNG_fake_guide_bytes_over_1k" + b"0" * 1100
    monkeypatch.setattr(svc, "_generate_guide_png", lambda **kw: fake)
    calls = _capture_recorder(monkeypatch)
    build_indoor_pose_guide(group_id="g1", pose_brief=_BRIEF, env_bg_bytes=b"x",
                            bg_key="bgA", cache_dir=tmp_path, bg_asset_id="asset-1",
                            qc_fn=lambda png, **_k: (True, None))
    assert len(calls) == 1 and calls[0]["disposition"] == "accepted"
    assert calls[0]["input_image_ids"] == ["asset-1"]


def test_qc_fail_captures_rejected(tmp_path, monkeypatch):
    fake = b"\x89PNG_fake_guide_bytes_over_1k" + b"0" * 1100
    monkeypatch.setattr(svc, "_generate_guide_png", lambda **kw: fake)
    calls = _capture_recorder(monkeypatch)
    png, diag = build_indoor_pose_guide(group_id="g1", pose_brief=_BRIEF, env_bg_bytes=b"x",
                                        bg_key="bgA", cache_dir=tmp_path, bg_asset_id="asset-1",
                                        qc_fn=lambda png, **_k: (False, "text_or_marker_leakage"))
    assert png is None and diag["status"] == "qc_failed"
    assert len(calls) == 1 and calls[0]["disposition"] == "rejected"
    assert calls[0]["pipeline_metadata"]["qc_reason"] == "text_or_marker_leakage"


def test_atomic_write_failure_skips_accepted_capture(tmp_path, monkeypatch):
    """cache write(_atomic_write) 실패 시 accepted capture 안 함 (Codex NARROW 순서).

    accepted = "QC pass + cache 가능한 guide" 의미이므로, IO 실패면 캔버스에 보이는데
    pipeline 은 못 쓰는 정합성 깨진 row 가 남으면 안 된다.
    """
    import pytest

    fake = b"\x89PNG_fake_guide_bytes_over_1k" + b"0" * 1100
    monkeypatch.setattr(svc, "_generate_guide_png", lambda **kw: fake)

    def _boom(*_a, **_k):
        raise OSError("disk full")

    monkeypatch.setattr(svc, "_atomic_write", _boom)
    calls = _capture_recorder(monkeypatch)
    with pytest.raises(OSError):
        build_indoor_pose_guide(group_id="g1", pose_brief=_BRIEF, env_bg_bytes=b"x",
                                bg_key="bgA", cache_dir=tmp_path, bg_asset_id="asset-1",
                                qc_fn=lambda png, **_k: (True, None))
    assert calls == []  # accepted capture 발생 안 함


def test_no_bg_asset_id_marks_unresolved_inputs(tmp_path, monkeypatch):
    fake = b"\x89PNG_fake_guide_bytes_over_1k" + b"0" * 1100
    monkeypatch.setattr(svc, "_generate_guide_png", lambda **kw: fake)
    calls = _capture_recorder(monkeypatch)
    build_indoor_pose_guide(group_id="g1", pose_brief=_BRIEF, env_bg_bytes=b"x",
                            bg_key="bgA", cache_dir=tmp_path,
                            qc_fn=lambda png, **_k: (True, None))
    assert calls[0]["input_image_ids"] is None
    assert calls[0]["pipeline_metadata"]["unresolved_inputs"] == ["background_render:bgA"]


# ── Task 6: guide QC 판정 (핵심 분기만) ─────────────────────────────────

_GOOD_QC = {"layout_preserved": True, "photoreal_person": False, "clothing_or_face": False,
            "text_or_marker_leakage": False, "environment_redraw": False, "mannequin_count": 2}


def test_qc_pass_and_representative_fails():
    assert evaluate_guide_qc(_GOOD_QC, expected_figures=2) == (True, None)
    assert evaluate_guide_qc({**_GOOD_QC, "photoreal_person": True}, expected_figures=2)[0] is False
    assert evaluate_guide_qc({**_GOOD_QC, "text_or_marker_leakage": True}, expected_figures=2)[0] is False


def test_qc_mannequin_count_tolerance():
    assert evaluate_guide_qc({**_GOOD_QC, "mannequin_count": 3}, expected_figures=2) == (True, None)
    assert evaluate_guide_qc({**_GOOD_QC, "mannequin_count": 5}, expected_figures=2)[0] is False
