"""FINDING 7 (e2e-bughunt-v1) W2 — owned redraw variation-level atomic repair tests.

`SceneDetailStep._attempt_owned_redraw_repair` 의 atomic repair 동작을 직접
구동한다. owned judge `else` 분기 wiring 은 이 메서드를 호출/재바인딩만 하므로
메서드 단위 검증으로 atomic repair contract 를 정밀하게 본다.

Codex 필수 test 6건 → G1-G6. + G7 회귀 (_check_prompts 재사용) + G8 NARROW
amend 1 (repair 검증 judge 실패 catch/revert).

harness: `_attempt_owned_redraw_repair` 는 self 에서 `project_config` /
`build_opik_metadata` 만 쓰므로 SimpleNamespace stand-in 으로 호출한다. LLM 호출은
`detail_steps.call_structured` monkeypatch 로 step 별 분기. `run_owned_repair` /
`run_owned_judge` 는 실제 prompt-pack 을 load 하고 fake call_structured 를 쓴다.
"""
from __future__ import annotations

import types

import pytest

import app.core.steps.detail_steps as detail_steps_mod
import app.core.visible_entities_validator as vev_mod
from app.core.errors import AppError
from app.core.steps._owned_helpers import has_redraw_violation
from app.core.steps.detail_steps import SceneDetailStep


OWNED = ["circle mark"]
ORIG_T2I = (
    "A highly saturated red circle mark sits sharply on the reflected sterile tiles."
)
REPAIRED_T2I = (
    "Two figures stand near the existing red circle mark on the sterile tiles."
)


def _redraw_violation(owned_object: str = "circle mark") -> dict:
    return {
        "owned_object": owned_object,
        "violating_phrase": "a red circle mark sits sharply",
        "reason": "prompt explicitly redraws the circle mark onto the tiles",
        "verdict": "redraw_violation",
    }


def _anchor_violation(owned_object: str = "circle mark") -> dict:
    return {
        "owned_object": owned_object,
        "violating_phrase": "near the existing circle mark",
        "reason": "referenced as already present in the background, not redrawn",
        "verdict": "anchor_reference",
    }


def _anchor_echo() -> list:
    return [
        {
            "owned_token": "circle mark",
            "usage_kind": "anchor",
            "source_phrase": "near the existing circle mark",
        }
    ]


def _variation(t2i: str = ORIG_T2I) -> dict:
    return {"t2i_prompt": t2i, "owned_object_usage": [], "variant_label": "technique_1"}


def _result(variation: dict) -> dict:
    return {
        "scene_index": 18,
        "_shot_index": 11,
        "t2i_variations": [variation],
        "visible_entities": [],
    }


def _fake_step():
    """`_attempt_owned_redraw_repair` 가 쓰는 self 멤버만 가진 stand-in."""
    s = types.SimpleNamespace()
    s.project_config = None
    s.build_opik_metadata = lambda **kw: {"extra_tags": kw.get("extra_tags", [])}
    return s


def _clean_check_prompts(result):
    """repair 후보가 invalid ID 를 만들지 않은 정상 케이스 stub."""
    return set()


class _FakeCS:
    """step 별 분기 fake call_structured.

    응답이 BaseException 이면 raise, callable 이면 kwargs 로 호출, dict 면 반환.
    """

    def __init__(self, *, repair=None, judge=None):
        self._repair = repair
        self._judge = judge
        self.calls = []

    def __call__(self, **kwargs):
        self.calls.append(kwargs)
        step = kwargs["step"]
        resp = {
            "scene_detail_owned_repair": self._repair,
            "scene_detail_owned_judge": self._judge,
        }.get(step)
        if resp is None:
            raise AssertionError(f"unexpected call_structured step: {step}")
        if isinstance(resp, BaseException):
            raise resp
        if callable(resp):
            return resp(kwargs)
        return resp

    def steps(self):
        return [c["step"] for c in self.calls]


@pytest.fixture
def no_visible_check(monkeypatch):
    """validate_visible_entities_contract no-op — result fixture 최소화."""
    monkeypatch.setattr(
        vev_mod, "validate_visible_entities_contract", lambda *a, **k: None
    )


def _call_repair(
    fake_cs, monkeypatch, *, variation, judge_violations,
    check_prompts_fn=_clean_check_prompts,
):
    monkeypatch.setattr(detail_steps_mod, "call_structured", fake_cs)
    result = _result(variation)
    return SceneDetailStep._attempt_owned_redraw_repair(
        _fake_step(),
        variation=variation,
        original_t2i=variation["t2i_prompt"],
        owned=OWNED,
        judge_violations=judge_violations,
        cam_dir="eye-level medium",
        result=result,
        name_by_short_id={},
        check_prompts_fn=check_prompts_fn,
        si=18,
        shot_idx=11,
    )


# ---------------------------------------------------------------------------
# G1 — repair input 에 redraw evidence 포함 (Codex 필수 #1)
# ---------------------------------------------------------------------------


def test_g1_repair_input_includes_redraw_evidence(monkeypatch, no_visible_check):
    fake = _FakeCS(
        repair={"t2i_prompt": REPAIRED_T2I, "owned_object_usage": _anchor_echo()},
        judge={"violations": [_anchor_violation()]},
    )
    _call_repair(
        fake, monkeypatch, variation=_variation(),
        judge_violations=[_redraw_violation()],
    )
    repair_calls = [c for c in fake.calls if c["step"] == "scene_detail_owned_repair"]
    assert len(repair_calls) == 1
    user_p = repair_calls[0]["user_prompt"]
    # judge evidence (owned_object + violating_phrase) 가 repair input 에 inline.
    assert "circle mark" in user_p
    assert "a red circle mark sits sharply" in user_p


# ---------------------------------------------------------------------------
# G2 — 1회 repair 성공 → contract_violation 해소 (Codex 필수 #2)
# ---------------------------------------------------------------------------


def test_g2_repair_success_resolves_contract_violation(monkeypatch, no_visible_check):
    fake = _FakeCS(
        repair={"t2i_prompt": REPAIRED_T2I, "owned_object_usage": _anchor_echo()},
        judge={"violations": [_anchor_violation()]},  # 재검증 judge — redraw 없음
    )
    variation = _variation()
    out = _call_repair(
        fake, monkeypatch, variation=variation,
        judge_violations=[_redraw_violation()],
    )
    assert out is not None
    repaired_t2i, repaired_merged, repaired_violations = out
    assert repaired_t2i == REPAIRED_T2I
    assert not has_redraw_violation(repaired_violations)
    # variation 에 atomic commit — t2i_prompt + merged owned_object_usage.
    assert variation["t2i_prompt"] == REPAIRED_T2I
    assert len(variation["owned_object_usage"]) == len(OWNED)
    assert variation["owned_object_usage"] is repaired_merged


# ---------------------------------------------------------------------------
# G3 — repair 후에도 위반 잔존 → contract_violation 유지 (Codex 필수 #3)
# ---------------------------------------------------------------------------


def test_g3_repair_still_redraw_keeps_contract_violation(monkeypatch, no_visible_check):
    fake = _FakeCS(
        repair={
            "t2i_prompt": "still draws a fresh red circle mark on the tiles",
            "owned_object_usage": [
                {
                    "owned_token": "circle mark",
                    "usage_kind": "redraw",
                    "source_phrase": "a fresh red circle mark",
                }
            ],
        },
        judge={"violations": [_redraw_violation()]},  # 재검증 judge — 여전히 redraw
    )
    variation = _variation()
    out = _call_repair(
        fake, monkeypatch, variation=variation,
        judge_violations=[_redraw_violation()],
    )
    assert out is None
    # revert — 원본 t2i_prompt 보존.
    assert variation["t2i_prompt"] == ORIG_T2I


# ---------------------------------------------------------------------------
# G4 — non-violating / anchor-only path → repair 호출 0회 (Codex 필수 #4)
# ---------------------------------------------------------------------------


def test_g4_anchor_only_path_zero_repair_call(monkeypatch, no_visible_check):
    fake = _FakeCS(
        repair={"t2i_prompt": "x", "owned_object_usage": []},
        judge={"violations": []},
    )
    variation = _variation()
    out = _call_repair(
        fake, monkeypatch, variation=variation,
        judge_violations=[_anchor_violation()],  # redraw_violation 없음
    )
    assert out is None
    # redraw evidence 0 → repair / judge LLM 호출 자체 발생 안 함.
    assert fake.calls == []
    assert variation["t2i_prompt"] == ORIG_T2I


# ---------------------------------------------------------------------------
# G5 — fail-fast 미약화 (Codex 필수 #5)
# ---------------------------------------------------------------------------


def test_g5a_unknown_owned_token_merge_revert(monkeypatch, no_visible_check):
    # repair 가 owned_list 밖 token declare → merge_owned_object_usage AppError → revert.
    fake = _FakeCS(
        repair={
            "t2i_prompt": REPAIRED_T2I,
            "owned_object_usage": [
                {
                    "owned_token": "unknown gadget",
                    "usage_kind": "anchor",
                    "source_phrase": "x",
                }
            ],
        },
        judge={"violations": [_anchor_violation()]},
    )
    variation = _variation()
    out = _call_repair(
        fake, monkeypatch, variation=variation,
        judge_violations=[_redraw_violation()],
    )
    assert out is None
    assert variation["t2i_prompt"] == ORIG_T2I
    # merge 단계에서 revert — 후보 judge 까지 가지 않음.
    assert "scene_detail_owned_judge" not in fake.steps()


def test_g5b_invalid_id_in_repaired_prompt_revert(monkeypatch, no_visible_check):
    # repair 가 invalid ID 도입 → check_prompts_fn to_remove 검출 → revert.
    fake = _FakeCS(
        repair={"t2i_prompt": REPAIRED_T2I, "owned_object_usage": _anchor_echo()},
        judge={"violations": [_anchor_violation()]},
    )
    variation = _variation()
    out = _call_repair(
        fake, monkeypatch, variation=variation,
        judge_violations=[_redraw_violation()],
        check_prompts_fn=lambda result: {"C99"},  # repair 가 만든 invalid ID
    )
    assert out is None
    assert variation["t2i_prompt"] == ORIG_T2I


def test_g5c_repair_llm_malformed_revert(monkeypatch, no_visible_check):
    # repair LLM 응답 t2i_prompt 누락 → run_owned_repair AppError → step1 catch → revert.
    fake = _FakeCS(
        repair={"owned_object_usage": []},  # t2i_prompt 누락
        judge={"violations": [_anchor_violation()]},
    )
    variation = _variation()
    out = _call_repair(
        fake, monkeypatch, variation=variation,
        judge_violations=[_redraw_violation()],
    )
    assert out is None
    assert variation["t2i_prompt"] == ORIG_T2I
    # repair 단계에서 revert — 후보 judge 미호출.
    assert "scene_detail_owned_judge" not in fake.steps()


def test_g5d_visible_contract_fail_revert(monkeypatch):
    # repair 후보가 visible_entities contract 위반 → step4 except AppError → finally revert.
    fake = _FakeCS(
        repair={"t2i_prompt": REPAIRED_T2I, "owned_object_usage": _anchor_echo()},
        judge={"violations": [_anchor_violation()]},
    )

    def _raise(*a, **k):
        raise AppError(
            code="step.scene_detail.contract_violation_missing_field",
            message="visible_entities contract broken by repaired prompt",
        )

    monkeypatch.setattr(vev_mod, "validate_visible_entities_contract", _raise)
    variation = _variation()
    out = _call_repair(
        fake, monkeypatch, variation=variation,
        judge_violations=[_redraw_violation()],
    )
    assert out is None
    # finally 블록이 swap 된 t2i_prompt 를 원본으로 revert.
    assert variation["t2i_prompt"] == ORIG_T2I


# ---------------------------------------------------------------------------
# G6 — retry count = 1, 무한 루프 없음 (Codex 필수 #6)
# ---------------------------------------------------------------------------


def test_g6_repair_called_exactly_once(monkeypatch, no_visible_check):
    fake = _FakeCS(
        repair={
            "t2i_prompt": "still redraws the red circle mark fresh on the floor",
            "owned_object_usage": [
                {
                    "owned_token": "circle mark",
                    "usage_kind": "redraw",
                    "source_phrase": "the red circle mark fresh",
                }
            ],
        },
        judge={"violations": [_redraw_violation()]},  # repair 실패 — 여전히 redraw
    )
    out = _call_repair(
        fake, monkeypatch, variation=_variation(),
        judge_violations=[_redraw_violation()],
    )
    assert out is None
    # repair LLM 은 정확히 1회 — repair 실패해도 재시도 없음.
    assert fake.steps().count("scene_detail_owned_repair") == 1


# ---------------------------------------------------------------------------
# G7 — repair 재검증이 _check_prompts closure 재사용 (회귀)
# ---------------------------------------------------------------------------


def test_g7_repair_reuses_check_prompts_closure(monkeypatch, no_visible_check):
    fake = _FakeCS(
        repair={"t2i_prompt": REPAIRED_T2I, "owned_object_usage": _anchor_echo()},
        judge={"violations": [_anchor_violation()]},
    )
    seen = []

    def recording_check_prompts(result):
        seen.append(result)
        return set()

    out = _call_repair(
        fake, monkeypatch, variation=_variation(),
        judge_violations=[_redraw_violation()],
        check_prompts_fn=recording_check_prompts,
    )
    assert out is not None
    # repair 후보 재검증이 caller 의 _check_prompts closure 를 그대로 재사용.
    assert len(seen) == 1


# ---------------------------------------------------------------------------
# G8 — repair 후보 judge 실패 → catch/revert (Codex NARROW amend 1)
# ---------------------------------------------------------------------------


def test_g8a_repair_verify_judge_apperror_revert(monkeypatch, no_visible_check):
    # 재검증 judge LLM 이 AppError → step3 except Exception catch → revert, crash 없음.
    fake = _FakeCS(
        repair={"t2i_prompt": REPAIRED_T2I, "owned_object_usage": _anchor_echo()},
        judge=AppError(code="step.contract_violation", message="judge LLM exploded"),
    )
    variation = _variation()
    out = _call_repair(
        fake, monkeypatch, variation=variation,
        judge_violations=[_redraw_violation()],
    )
    assert out is None
    assert variation["t2i_prompt"] == ORIG_T2I
    assert fake.steps().count("scene_detail_owned_repair") == 1
    assert fake.steps().count("scene_detail_owned_judge") == 1


def test_g8b_repair_verify_judge_malformed_revert(monkeypatch, no_visible_check):
    # 재검증 judge 응답 violations 키 누락 → run_owned_judge AppError → step3 catch → revert.
    fake = _FakeCS(
        repair={"t2i_prompt": REPAIRED_T2I, "owned_object_usage": _anchor_echo()},
        judge={},  # malformed — violations 키 없음
    )
    variation = _variation()
    out = _call_repair(
        fake, monkeypatch, variation=variation,
        judge_violations=[_redraw_violation()],
    )
    assert out is None
    assert variation["t2i_prompt"] == ORIG_T2I
