"""FINDING 7 (e2e-bughunt-v1) W1 — owned redraw repair building block tests.

W1 building block 단위 검증:
- G1: `scene_detail_owned_repair` 가 `_PIPELINE_STEP_EXTENSIONS` 에 등록되어
  gpt-mini 로 라우팅 (2026-07-11 Gemini 원복) (`test_g3_2_judge_step_extension.py` 패턴 mirror).
- G2: `scene_detail_owned_repair` prompt-pack (system / user_template / schema)
  load + schema strict shape.
- G3: `format_redraw_violations_block` pure helper.
- G4: `run_owned_repair` 1-call wrapper (fake call_structured) + fail-fast.

detail_steps.py 통합은 W2 (test_finding7_owned_redraw_repair.py).
"""
from __future__ import annotations

from unittest.mock import MagicMock

import pytest

from app.core.errors import AppError
from app.core.steps._owned_helpers import format_redraw_violations_block
from app.core.steps._owned_repair import run_owned_repair
from app.modules.llm.llm_client import (
    PIPELINE_STEPS,
    _PIPELINE_STEP_EXTENSIONS,
    _resolve_model,
)
from app.modules.prompt_loader import load_prompt, load_schema


_STEP = "scene_detail_owned_repair"


def _redraw_violation(
    owned_object: str = "circle mark",
    violating_phrase: str = "a red circle mark sits on the tiles",
    reason: str = "prompt explicitly redraws the circle mark",
) -> dict:
    """owned judge redraw_violation entry fixture."""
    return {
        "owned_object": owned_object,
        "violating_phrase": violating_phrase,
        "reason": reason,
        "verdict": "redraw_violation",
    }


# ---------------------------------------------------------------------------
# G1 — _PIPELINE_STEP_EXTENSIONS registration
# ---------------------------------------------------------------------------


def test_g1_repair_step_in_extensions() -> None:
    assert _STEP in _PIPELINE_STEP_EXTENSIONS


def test_g1_repair_default_model_is_gpt_mini() -> None:
    info = _PIPELINE_STEP_EXTENSIONS[_STEP]
    # 등록 키는 'default' (NOT 'default_model') — owned_judge 와 동일.
    assert "default_model" not in info
    assert info["default"] == "gpt-mini"


def test_g1_repair_category_is_analysis_sub() -> None:
    assert _PIPELINE_STEP_EXTENSIONS[_STEP]["category"] == "analysis_sub"


def test_g1_repair_label_is_korean_str() -> None:
    label = _PIPELINE_STEP_EXTENSIONS[_STEP]["label"]
    assert label and isinstance(label, str)


def test_g1_repair_resolves_to_gpt_mini() -> None:
    assert _resolve_model(_STEP) == "gpt-mini"


def test_g1_repair_in_pipeline_steps_view() -> None:
    assert _STEP in PIPELINE_STEPS
    info = PIPELINE_STEPS[_STEP]
    assert info["default"] == "gpt-mini"
    assert info["category"] == "analysis_sub"


# ---------------------------------------------------------------------------
# G2 — prompt-pack load + schema strict shape
# ---------------------------------------------------------------------------


def test_g2_prompt_pack_loads() -> None:
    system = load_prompt(_STEP, "system")
    template = load_prompt(_STEP, "user_template")
    schema = load_schema(_STEP, "schema")
    assert system.strip()
    assert template.strip()
    assert isinstance(schema, dict)


def test_g2_user_template_has_placeholders() -> None:
    template = load_prompt(_STEP, "user_template")
    for ph in (
        "{t2i_prompt}",
        "{owned_list_block}",
        "{redraw_violations_block}",
        "{camera_direction}",
    ):
        assert ph in template, f"missing placeholder {ph}"


def test_g2_schema_strict_shape() -> None:
    schema = load_schema(_STEP, "schema")
    assert schema["type"] == "object"
    assert schema["additionalProperties"] is False
    assert set(schema["required"]) == {"t2i_prompt", "owned_object_usage"}
    # owned_object_usage item shape = scene_detail detail_schema 와 동일.
    item = schema["properties"]["owned_object_usage"]["items"]
    assert set(item["required"]) == {"owned_token", "usage_kind", "source_phrase"}
    assert item["additionalProperties"] is False
    assert item["properties"]["usage_kind"]["enum"] == ["redraw", "anchor", "absent"]


# ---------------------------------------------------------------------------
# G3 — format_redraw_violations_block (pure helper)
# ---------------------------------------------------------------------------


def test_g3_format_block_includes_owned_and_phrase() -> None:
    block = format_redraw_violations_block([_redraw_violation()])
    assert "circle mark" in block
    assert "a red circle mark sits on the tiles" in block
    assert "prompt explicitly redraws the circle mark" in block


def test_g3_format_block_multi_line() -> None:
    block = format_redraw_violations_block(
        [
            _redraw_violation(owned_object="circle mark"),
            _redraw_violation(owned_object="marking"),
        ]
    )
    lines = block.split("\n")
    assert len(lines) == 2
    assert "circle mark" in lines[0]
    assert "marking" in lines[1]


def test_g3_format_block_empty() -> None:
    assert format_redraw_violations_block([]) == ""


# ---------------------------------------------------------------------------
# G4 — run_owned_repair 1-call wrapper
# ---------------------------------------------------------------------------


def test_g4_run_repair_returns_prompt_and_usage() -> None:
    fake = MagicMock(
        return_value={
            "t2i_prompt": "fixed prompt anchoring the circle mark",
            "owned_object_usage": [
                {
                    "owned_token": "circle mark",
                    "usage_kind": "anchor",
                    "source_phrase": "near the existing circle mark",
                }
            ],
        }
    )
    out = run_owned_repair(
        t2i_prompt="a red circle mark sits on the tiles",
        owned=["circle mark"],
        redraw_violations=[_redraw_violation()],
        camera_direction="eye-level medium",
        call_structured_fn=fake,
    )
    assert out["t2i_prompt"] == "fixed prompt anchoring the circle mark"
    assert out["owned_object_usage"][0]["owned_token"] == "circle mark"


def test_g4_run_repair_passes_step_name() -> None:
    captured: dict = {}

    def fake(**kwargs):
        captured.update(kwargs)
        return {"t2i_prompt": "x", "owned_object_usage": []}

    run_owned_repair(
        t2i_prompt="orig",
        owned=["circle mark"],
        redraw_violations=[_redraw_violation()],
        camera_direction="wide",
        call_structured_fn=fake,
    )
    assert captured["step"] == _STEP
    assert captured["schema_name"] == _STEP
    assert "default_model" not in captured


def test_g4_run_repair_user_prompt_has_evidence() -> None:
    captured: dict = {}

    def fake(**kwargs):
        captured.update(kwargs)
        return {"t2i_prompt": "x", "owned_object_usage": []}

    run_owned_repair(
        t2i_prompt="a red circle mark sits on the tiles",
        owned=["circle mark", "marking"],
        redraw_violations=[_redraw_violation()],
        camera_direction="eye-level medium",
        call_structured_fn=fake,
    )
    user_p = captured["user_prompt"]
    # 원본 t2i_prompt / owned_list / redraw evidence / camera 모두 inline.
    assert "a red circle mark sits on the tiles" in user_p
    assert "- circle mark" in user_p
    assert "- marking" in user_p
    assert "prompt explicitly redraws the circle mark" in user_p
    assert "eye-level medium" in user_p
    # placeholder 잔존 금지.
    for ph in (
        "{t2i_prompt}",
        "{owned_list_block}",
        "{redraw_violations_block}",
        "{camera_direction}",
    ):
        assert ph not in user_p


def test_g4_run_repair_empty_violations_raises() -> None:
    with pytest.raises(AppError) as exc:
        run_owned_repair(
            t2i_prompt="x",
            owned=["circle mark"],
            redraw_violations=[],
            call_structured_fn=MagicMock(),
            camera_direction="wide",
        )
    assert exc.value.code == "step.contract_violation"


def test_g4_run_repair_missing_t2i_raises() -> None:
    fake = MagicMock(return_value={"owned_object_usage": []})
    with pytest.raises(AppError) as exc:
        run_owned_repair(
            t2i_prompt="x",
            owned=["circle mark"],
            redraw_violations=[_redraw_violation()],
            camera_direction="wide",
            call_structured_fn=fake,
        )
    assert exc.value.code == "step.contract_violation"
    assert "t2i_prompt" in exc.value.message


def test_g4_run_repair_empty_t2i_raises() -> None:
    fake = MagicMock(return_value={"t2i_prompt": "   ", "owned_object_usage": []})
    with pytest.raises(AppError) as exc:
        run_owned_repair(
            t2i_prompt="x",
            owned=["circle mark"],
            redraw_violations=[_redraw_violation()],
            camera_direction="wide",
            call_structured_fn=fake,
        )
    assert exc.value.code == "step.contract_violation"


def test_g4_run_repair_usage_not_list_raises() -> None:
    fake = MagicMock(return_value={"t2i_prompt": "x", "owned_object_usage": "bad"})
    with pytest.raises(AppError) as exc:
        run_owned_repair(
            t2i_prompt="x",
            owned=["circle mark"],
            redraw_violations=[_redraw_violation()],
            camera_direction="wide",
            call_structured_fn=fake,
        )
    assert exc.value.code == "step.contract_violation"
    assert "must be list" in exc.value.message


# ── 수리 실패 시 다른 모델로 재시도 (2026-08-07 사용자 지시) ────────────
#
# 실제로 겪은 일: 가벼운 모델이 쓴 수리본에 장소 번호(L07)가 섞여 ID 규칙에
# 걸렸고, 곧장 되돌려져 그 한 컷 때문에 256개 중 255개가 정상인데도 단계
# 전체가 막혔다. 같은 모델에 같은 식으로 다시 물으면 같은 답이 나오므로
# **다른(상위) 모델**로 넘긴다.


def _src():
    from pathlib import Path as _P
    return _P("app/core/steps/detail_steps.py").read_text("utf-8")


def test_repair_retries_with_upper_model_on_llm_failure():
    s = _src()
    assert "_RETRY_MODEL" in s
    assert "상위 모델" in s
    # 1차는 기본 설정, 2차는 모델을 갈아 끼운다
    assert '{**(self.project_config or {}), _TAG: {"model": _RETRY_MODEL}}' in s


def test_repair_retries_when_validation_rejects_not_only_on_exception():
    """이번에 겪은 실패는 LLM 호출 성공 + 검증 거부였다 — 그 경로도 재시도."""
    s = _src()
    assert "수리본 ID 위반" in s
    assert "retried_model=True" in s


def test_retry_call_does_not_recurse_forever():
    s = _src()
    # 재시도 호출이면 상위 모델만 쓰고, 거기서 또 거부되면 revert
    assert "_plan = (_upper,) if retried_model else" in s
    assert "재수리본도 ID 위반" in s


def test_contract_violation_shot_is_not_reused_on_resume():
    """규칙 위반 컷을 재사용하면 그 하나 때문에 단계가 영원히 막힌다."""
    s = _src()
    assert 'if s.get("status") == "contract_violation":' in s
    assert "재사용하지 않고 다시 만든다" in s
