"""run() 새 흐름 통합 — Block C C2 (claim 시점 + decision 분기).

AC-B1 / C1 / C2 / C7. minimum gate — plan v2.1.3 §3325~3552 의 핵심 7 test.
"""
from unittest.mock import MagicMock

import pytest

from app.core.errors import AppError
from app.core.step_runner import ResumeAction, ResumeDecision, StepRunner


def _make_runner_with_decision(action: ResumeAction, **kw) -> StepRunner:
    """run() 진입 직후의 fixture — gate/applicability OK, decision/claim mock."""
    runner = StepRunner.__new__(StepRunner)
    runner.step_id = "test"
    runner.project_id = "p1"
    runner.episode_id = "e1"
    runner.run_id = "r-self"
    runner.db = MagicMock()
    runner._resolve_model = lambda: "test-model"
    runner.check_gate = lambda: None
    runner.check_applicability = lambda: True
    runner._evaluate_resume_decision = lambda mode: ResumeDecision(
        action=action, reason="t", **kw
    )
    runner._try_claim_running = MagicMock(return_value=True)
    runner._execute_rerun_self = MagicMock(return_value={"status": "completed"})
    runner._execute_force = MagicMock(return_value={"status": "completed"})
    runner._mark_not_applicable = MagicMock()
    runner._record_recovery = MagicMock(return_value=1)
    runner._check_recovery_exhausted = MagicMock()
    runner.load_checkpoint = MagicMock(return_value=None)
    return runner


def test_skip_does_not_claim():
    """SKIP decision → claim 호출 0건."""
    runner = _make_runner_with_decision(ResumeAction.SKIP)
    result = runner.run("resume")
    assert result["status"] == "skipped"
    runner._try_claim_running.assert_not_called()


def test_block_raises_409_without_claim():
    """BLOCK decision → AppError(step.resume_invalid, 409) + claim 호출 0건."""
    runner = _make_runner_with_decision(ResumeAction.BLOCK, origin="contract_drift")
    with pytest.raises(AppError) as exc:
        runner.run("resume")
    assert exc.value.code == "step.resume_invalid"
    assert exc.value.status_code == 409
    runner._try_claim_running.assert_not_called()


def test_rerun_self_claims_then_executes():
    """RERUN_SELF decision → claim → _execute_rerun_self (cleanup 안 함)."""
    runner = _make_runner_with_decision(ResumeAction.RERUN_SELF, origin=None)
    runner.run("resume")
    runner._try_claim_running.assert_called_once_with(
        allow_stale_steal=False,
        expected_started_at=None,
        expected_run_id=None,
    )
    runner._execute_rerun_self.assert_called_once()
    runner._execute_force.assert_not_called()


def test_force_explicit_calls_execute_force():
    """FORCE_EXPLICIT decision → claim → _execute_force (cleanup + invalidate)."""
    runner = _make_runner_with_decision(ResumeAction.FORCE_EXPLICIT)
    runner.run("force")
    runner._try_claim_running.assert_called_once_with(
        allow_stale_steal=False,
        expected_started_at=None,
        expected_run_id=None,
    )
    runner._execute_force.assert_called_once()
    runner._execute_rerun_self.assert_not_called()


def test_stale_running_recovery_passes_expected_fields():
    """STALE_RUNNING_RECOVERY → atomic steal + _execute_rerun_self."""
    runner = _make_runner_with_decision(
        ResumeAction.STALE_RUNNING_RECOVERY,
        expected_started_at="2026-05-07T10:00:00+00:00",
        expected_run_id="r-old",
    )
    runner.run("resume")
    runner._try_claim_running.assert_called_once_with(
        allow_stale_steal=True,
        expected_started_at="2026-05-07T10:00:00+00:00",
        expected_run_id="r-old",
    )
    runner._execute_rerun_self.assert_called_once()
    runner._execute_force.assert_not_called()


def test_claim_failure_raises_already_running():
    """claim 실패 → AppError(step.already_running, 409)."""
    runner = _make_runner_with_decision(ResumeAction.RERUN_SELF, origin=None)
    runner._try_claim_running.return_value = False
    with pytest.raises(AppError) as exc:
        runner.run("resume")
    assert exc.value.code == "step.already_running"
    assert exc.value.status_code == 409


def test_check_applicability_false_calls_mark_not_applicable():
    """check_applicability=False → _mark_not_applicable + claim 0건."""
    runner = _make_runner_with_decision(ResumeAction.SKIP)  # 도달 안 함
    runner.check_applicability = lambda: False
    result = runner.run("resume")
    assert result["status"] == "not_applicable"
    runner._mark_not_applicable.assert_called_once()
    runner._try_claim_running.assert_not_called()
