"""Block B B7 — `_safe_verify_completion` AppError vs Exception split.

Plan v2.1.3 / spec V5 §4.3:
- AppError 계열 (loader contract / step.contract_violation 등) →
  CompletionReport(origin='contract_drift', is_complete=False) — caller 가
  BLOCK 처리 (B11 후속)
- AppError(code='step.verify_crashed') → propagate (B8 verify_completion 이
  card recompute 등에서 미리 격상한 케이스)
- 그 외 unexpected exception (KeyError / ValueError / AttributeError 등) →
  AppError(step.verify_crashed) raise (자동 force 금지, fail-fast)

scope:
- B1 (CompletionReport.origin) prerequisite ✓ (commit e282476)
- ResumeDecision helper 가 본 메서드 호출 (step_runner.py:589) — 후속 B11 에서
  origin 별 정책 분기. 본 commit 은 fail-fast 만 도입.
"""
from __future__ import annotations

import pytest

from app.core.errors import AppError
from app.core.integrity_report import CompletionReport
from app.core.step_runner import StepRunner


def _make_runner(verify_fn):
    """StepRunner instance — verify_completion 만 stub."""
    runner = StepRunner.__new__(StepRunner)
    runner.step_id = "test_step"
    runner.verify_completion = verify_fn
    return runner


class TestSafeVerifyCompletionAppErrorContractDrift:
    def test_apperror_contract_violation_returns_contract_drift(self):
        """AppError(code='step.contract_violation') → CompletionReport(origin='contract_drift').

        loader / shape / structural 위반은 caller (run() / ResumeDecision) 가
        BLOCK 처리. silent skip 차단 — feedback_no_silent_fallback.md.
        """
        def verify():
            raise AppError(code="step.contract_violation", message="loader violation")

        runner = _make_runner(verify)
        report = runner._safe_verify_completion()

        assert report.is_complete is False
        assert report.origin == "contract_drift"
        # missing 에 원인 노출 (B11 디스패치 시 운영 가시성)
        assert any("contract" in m.lower() or "loader violation" in m for m in report.missing)
        assert report.metadata.get("app_error_code") == "step.contract_violation"

    def test_apperror_other_code_returns_contract_drift(self):
        """일반 AppError (verify_crashed 외) → contract_drift default.

        verify_completion 내부에서 의도적으로 raise 하는 AppError 는 contract
        위반 신호로 해석.
        """
        def verify():
            raise AppError(code="loader.bg_owned_missing", message="bg_id absent")

        runner = _make_runner(verify)
        report = runner._safe_verify_completion()

        assert report.is_complete is False
        assert report.origin == "contract_drift"


class TestSafeVerifyCompletionVerifyCrashed:
    def test_keyerror_raises_verify_crashed(self):
        """KeyError → AppError(step.verify_crashed) raise (자동 force 금지).

        unexpected exception 은 silent recovery 가 사고 패턴 — fail-fast 의무.
        """
        def verify():
            raise KeyError("missing key 'foo'")

        runner = _make_runner(verify)

        with pytest.raises(AppError) as exc_info:
            runner._safe_verify_completion()

        assert exc_info.value.code == "step.verify_crashed"
        assert "KeyError" in exc_info.value.message
        assert "foo" in exc_info.value.message

    def test_value_error_raises_verify_crashed(self):
        def verify():
            raise ValueError("bad value")

        runner = _make_runner(verify)

        with pytest.raises(AppError) as exc_info:
            runner._safe_verify_completion()

        assert exc_info.value.code == "step.verify_crashed"

    def test_attribute_error_raises_verify_crashed(self):
        def verify():
            raise AttributeError("foo has no attr 'bar'")

        runner = _make_runner(verify)

        with pytest.raises(AppError) as exc_info:
            runner._safe_verify_completion()

        assert exc_info.value.code == "step.verify_crashed"

    def test_apperror_verify_crashed_propagates_unchanged(self):
        """이미 step.verify_crashed AppError 는 원형 전파 (이중 wrap 금지).

        B8 의 SceneDetailStep.verify_completion 이 card recompute 단에서 unexpected
        Exception 을 verify_crashed 로 격상한 케이스. _safe_verify_completion 이
        한 번 더 wrap 하면 message 와 chain 이 망가짐.
        """
        original = AppError(
            code="step.verify_crashed",
            message="build_render_prompt_card crashed: KeyError: ...",
        )

        def verify():
            raise original

        runner = _make_runner(verify)

        with pytest.raises(AppError) as exc_info:
            runner._safe_verify_completion()

        assert exc_info.value.code == "step.verify_crashed"
        # original message 보존 (이중 wrap 시 "build_render_prompt_card" 손실)
        assert "build_render_prompt_card" in exc_info.value.message


class TestSafeVerifyCompletionPassThrough:
    def test_no_exception_returns_completion_report_unchanged(self):
        """예외 없음 → verify_completion 의 report 그대로 반환."""
        expected = CompletionReport(
            is_complete=True,
            missing=[],
            severity="clean",
            metadata={"total": 5},
            origin="clean",
        )

        def verify():
            return expected

        runner = _make_runner(verify)
        report = runner._safe_verify_completion()

        assert report is expected
        assert report.is_complete is True
        assert report.origin == "clean"

    def test_failure_report_passes_through(self):
        """verify_completion 이 직접 is_complete=False report 반환 → 그대로 전파."""
        expected = CompletionReport(
            is_complete=False,
            missing=["sentinel drift"],
            severity="partial",
            metadata={},
            origin="invariant_drift",
        )

        def verify():
            return expected

        runner = _make_runner(verify)
        report = runner._safe_verify_completion()

        assert report is expected
        assert report.origin == "invariant_drift"
