"""Phase 2.5 acceptance tests — end-to-end scenarios that the hardening must catch."""

from unittest.mock import MagicMock, patch
from pathlib import Path


def test_simulate_vision_api_outage_routes_to_pending_qc(tmp_path):
    """Scenario: Vision API down for 5 shots. All 5 should be in pending_qc, not silently passed."""
    from recoil.execution.step_runner import StepRunner
    from recoil.execution.step_types import ProjectPaths
    from recoil.core.critic import Outcome, CriticResult

    paths = ProjectPaths(
        project="test",
        project_root=tmp_path,
        frames_dir=tmp_path / "frames",
        video_dir=tmp_path / "video",
        plans_dir=tmp_path / "plans",
        previs_dir=tmp_path / "previs",
    )
    for d in [paths.frames_dir, paths.video_dir, paths.plans_dir, paths.previs_dir]:
        d.mkdir()

    store = MagicMock()
    store.get_shot.return_value = {"status": "video_processing"}
    runner = StepRunner(store=store, paths=paths)

    # Vision API errors on every call
    error_result = CriticResult(
        critic_name="start_frame", outcome=Outcome.ERROR, error="vision API timeout"
    )

    pending_calls = []

    def track_pending(shot_id, **kwargs):
        if kwargs.get("status") == "pending_qc":
            pending_calls.append(shot_id)

    store.update_shot.side_effect = track_pending

    start = tmp_path / "start.png"
    start.write_bytes(b"fake")

    with (
        patch(
            "recoil.pipeline._lib.critics.start_frame_critic.StartFrameCritic.run",
            return_value=("a", error_result),
        ),
        patch("recoil.execution.step_runner.get_client") as mock_get_client,
    ):
        mock_client = MagicMock()
        mock_get_client.return_value = mock_client

        for i in range(5):
            runner.execute_video(
                shot_id=f"EP001_SH0{i}",
                prompt="test",
                model="kling-v3",
                start_frame=start,
                inputs_snapshot={"characters": []},
            )

    # All 5 shots routed to pending_qc, none submitted to API
    assert len(pending_calls) == 5
    mock_client.submit.assert_not_called()


def test_simulate_unfixable_failure_terminates_immediately():
    """Scenario: Identity drift with no matching feedback fix. Should escalate after 1 attempt."""
    from recoil.execution.step_runner import StepRunner
    from recoil.execution.step_types import ProjectPaths, GateVerdict
    from recoil.execution.feedback import FeedbackAgent
    import tempfile

    with tempfile.TemporaryDirectory() as tmp:
        tmp_path = Path(tmp)
        paths = ProjectPaths(
            project="test",
            project_root=tmp_path,
            frames_dir=tmp_path / "frames",
            video_dir=tmp_path / "video",
            plans_dir=tmp_path / "plans",
            previs_dir=tmp_path / "previs",
        )
        for d in [paths.frames_dir, paths.video_dir, paths.plans_dir, paths.previs_dir]:
            d.mkdir()

        store = MagicMock()
        store.get_shot.return_value = {"status": "keyframe_generating"}
        runner = StepRunner(store=store, paths=paths)

        fail_verdict = GateVerdict(
            passed=False,
            gate_name="gate_2a",
            reason="weird unmatched failure",
            details={},
            cost=0.0,
            retriable=True,
        )
        fake_gate = MagicMock(return_value=fail_verdict)
        # CP-2 Phase 6: execute_keyframe now dispatches via resolve_adapter +
        # adapter.direct_submit_image instead of get_client(...).generate_keyframe.
        fake_adapter = MagicMock()
        fake_adapter.direct_submit_image.return_value = {
            "image_bytes": b"fake",
            "native_id": "test-id",
            "operation": None,
        }

        with (
            patch(
                "recoil.execution.providers.resolve_adapter",
                return_value=(fake_adapter, "default"),
            ),
            patch.object(FeedbackAgent, "diagnose", return_value=None),
        ):
            result = runner.execute_keyframe(
                shot_id="EP001_SH01",
                prompt="A character",
                model="gemini-3-pro-image-preview",
                gates=[fake_gate],
                max_gate_retries=4,
            )

        # Only 1 generation, terminal status
        assert fake_adapter.direct_submit_image.call_count == 1
        assert result.final_state == "icu_escalated"


def test_morning_recheck_promotes_recovered_shots(tmp_path):
    """Scenario: 3 shots in pending_qc. Vision API back. Re-check promotes 2, fails 1."""
    from tools.recheck_pending_qc import recheck_shot
    from recoil.core.critic import Outcome, CriticResult, Dimension, Severity

    store = MagicMock()
    store.project = "test_project"
    pending_shots = [
        {
            "shot_id": "EP001_SH01",
            "status": "pending_qc",
            "output_path": "video/v1.mp4",
            "pipeline": "video",
        },
        {
            "shot_id": "EP001_SH02",
            "status": "pending_qc",
            "output_path": "video/v2.mp4",
            "pipeline": "video",
        },
        {
            "shot_id": "EP001_SH03",
            "status": "pending_qc",
            "output_path": "video/v3.mp4",
            "pipeline": "video",
        },
    ]

    pass_result = CriticResult(critic_name="vf", outcome=Outcome.PASS)
    fail_result = CriticResult(
        critic_name="vf",
        outcome=Outcome.FAIL,
        dimensions=[
            Dimension(
                name="EXTRA_LIMBS",
                severity=Severity.HARD,
                passed=False,
                message="3 arms",
            )
        ],
    )

    results = []
    with patch(
        "tools.recheck_pending_qc._run_critic_for_shot",
        side_effect=[pass_result, pass_result, fail_result],
    ):
        for shot in pending_shots:
            results.append(recheck_shot(store, shot, project="test_project"))

    assert results == ["promoted", "promoted", "failed"]
