"""owned judge v5 (2026-07-02) — narrow exception 2종 결정론 배선 테스트.

(1) allowed_visual_state_change: shot-intent 가 명시 요구하는 owned 객체
    시각 상태/내용 변형 (generic fixture: 장식 표면 안 depicted content).
(2) allowed_prop_contact: 쥔/사용 중 객체의 물리적 상호작용 기하 서술
    (generic fixture: 필기구 ↔ 필기면 접촉).

LLM 판정 품질은 canary/육안 — 여기서는 verdict enum 수용/게이트 통과/
shot_intent 스레딩/negative(위반 유지) 배선만 결정론으로 잠근다.
"""
from __future__ import annotations

import pytest

from app.core.errors import AppError
from app.core.steps._owned_helpers import (
    assert_owned_sentinel_shape,
    build_owned_sentinel,
    has_redraw_violation,
)
from app.core.steps._owned_judge import run_owned_judge


def _v(verdict, obj="surface", phrase="p", reason="r"):
    return {"owned_object": obj, "violating_phrase": phrase,
            "reason": reason, "verdict": verdict}


# ─────────────── has_redraw_violation 게이트 ───────────────


def test_allowed_verdicts_do_not_count_as_redraw():
    """두 allowed verdict 는 contract_violation 카운트 대상이 아니다."""
    assert has_redraw_violation([
        _v("allowed_visual_state_change"),
        _v("allowed_prop_contact"),
        _v("anchor_reference"),
    ]) is False


def test_redraw_still_counts_alongside_allowed():
    """negative: allowed 와 섞여 있어도 redraw_violation 1개면 위반 유지."""
    assert has_redraw_violation([
        _v("allowed_visual_state_change"),
        _v("redraw_violation"),
    ]) is True


# ─────────────── sentinel enum 수용 ───────────────


def _sentinel(violations):
    return build_owned_sentinel(
        owned=["surface"], camera_direction="cam",
        t2i_prompt="prompt", is_close_framing=False,
        violations=violations,
        owned_object_usage=[{
            "owned_token": "surface", "usage_kind": "anchor",
            "source_phrase": "p",
        }],
    )


def test_sentinel_accepts_allowed_verdicts():
    s = _sentinel([_v("allowed_visual_state_change"), _v("allowed_prop_contact")])
    assert_owned_sentinel_shape(s, where="test")  # no raise


def test_sentinel_rejects_unknown_verdict():
    s = _sentinel([_v("narrative_transformation")])  # 미등록 값 — fail-fast
    with pytest.raises(AppError):
        assert_owned_sentinel_shape(s, where="test")


# ─────────────── run_owned_judge shot_intent 스레딩 ───────────────


def _fake_call(captured):
    def fn(**kwargs):
        captured.update(kwargs)
        return {"violations": [_v("allowed_visual_state_change")]}
    return fn


def test_judge_threads_shot_intent_into_user_prompt():
    """v5 template 의 {shot_intent} 가 실제 shot-intent 텍스트로 치환된다.

    generic fixture: 장식 표면(owned) 안 depicted content 의 변형을
    shot-intent 가 명시 요구하는 경우.
    """
    captured: dict = {}
    intent = ("A close view of the decorated surface where its depicted "
              "content appears visibly altered by the moment itself.")
    out = run_owned_judge(
        t2i_prompt="the depicted content on the decorated surface appears altered",
        owned=["surface"],
        owned_object_usage=[{
            "owned_token": "surface", "usage_kind": "anchor",
            "source_phrase": "decorated surface",
        }],
        camera_direction="close view",
        call_structured_fn=_fake_call(captured),
        shot_intent=intent,
    )
    assert intent in captured["user_prompt"]
    assert "{shot_intent}" not in captured["user_prompt"]  # placeholder 잔존 금지
    # v5 schema enum 에 allowed verdict 포함 (LLM 1차 스키마 검증 계약)
    enum = (captured["response_schema"]["properties"]["violations"]["items"]
            ["properties"]["verdict"]["enum"])
    assert set(enum) == {
        "redraw_violation", "anchor_reference",
        "allowed_visual_state_change", "allowed_prop_contact",
    }
    assert out and out[0]["verdict"] == "allowed_visual_state_change"


def test_judge_default_shot_intent_empty_backcompat():
    """shot_intent 미전달(기존 caller) → 빈 문자열 치환, 호출 계약 불변."""
    captured: dict = {}
    run_owned_judge(
        t2i_prompt="a writing tool contacting a writing surface",
        owned=["writing surface"],
        owned_object_usage=[{
            "owned_token": "writing surface", "usage_kind": "anchor",
            "source_phrase": "writing surface",
        }],
        camera_direction="macro",
        call_structured_fn=_fake_call(captured),
    )
    assert "{shot_intent}" not in captured["user_prompt"]


# ─────────────── v5 프롬프트 팩 계약 (파일) ───────────────


def test_v5_prompt_pack_contract():
    """v5 pack 이 latest 로 로드되고 default-deny 문구/placeholder 를 갖는다."""
    from app.modules.prompt_loader import load_prompt, load_schema
    system = load_prompt("scene_detail_owned_judge", "system")
    template = load_prompt("scene_detail_owned_judge", "user_template")
    schema = load_schema("scene_detail_owned_judge", "schema")
    assert "allowed_visual_state_change" in system
    assert "allowed_prop_contact" in system
    assert "default-deny" in system  # 불확실 → violation 유지 명시
    assert "{shot_intent}" in template
    enum = (schema["properties"]["violations"]["items"]["properties"]
            ["verdict"]["enum"])
    assert "allowed_visual_state_change" in enum and "allowed_prop_contact" in enum
    # negative 계약: 새 객체 추가/소유권 변경은 여전히 위반으로 남는 문구 존재
    assert "새 객체를 추가" in system
