from types import SimpleNamespace
from unittest.mock import Mock, patch

import pytest

from recoil.execution.step_runner import StepRunner
from recoil.execution.step_types import GateVerdict, ProjectPaths
from recoil.execution.types import GenerationResult


def _paths(tmp_path):
    video_dir = tmp_path / "renders"
    video_dir.mkdir()
    return ProjectPaths(
        project="test",
        project_root=tmp_path,
        frames_dir=tmp_path / "frames",
        video_dir=video_dir,
        plans_dir=tmp_path / "plans",
        previs_dir=tmp_path / "previs",
    )


def _runner(tmp_path):
    return StepRunner(
        store=SimpleNamespace(),
        paths=_paths(tmp_path),
        validate_frames=False,
    )


class _FakeVideoModelClient:
    submit_calls = 0

    def __init__(self, *args, **kwargs):
        pass

    def submit(self, payload):
        type(self).submit_calls += 1
        return SimpleNamespace(result=None)

    def wait_for_job(self, job, timeout_s, on_status=None):
        return GenerationResult(
            success=True,
            video_data=b"mp4",
            model="seeddance-2.0",
            cost=0.1,
            metadata={},
        )


def _gate(_frame_path, _shot_data):
    return GateVerdict(
        passed=True,
        gate_name="gate_2_video",
        reason="ok",
        details={"total_score": 0},
    )


def _execute_pass(runner, gates):
    return runner.execute_pass(
        pass_id="EP001_PASS_TEST",
        prompt="test prompt",
        reference_image_paths=[],
        segment_shot_ids=["EP001_SH01"],
        expected_segment_timestamps=[(0.0, 3.0)],
        gates=gates,
        pass_counter=1,
        tag="TEST",
    )


def test_execute_pass_missing_boundary_frame_fails_closed(tmp_path):
    runner = _runner(tmp_path)
    _FakeVideoModelClient.submit_calls = 0

    with (
        patch(
            "recoil.execution.video_model_client.VideoModelClient",
            _FakeVideoModelClient,
        ),
        patch("recoil.execution.step_runner._ensure_ffmpeg_available", lambda: None),
        patch.object(runner, "_write_sidecar", lambda *args, **kwargs: None),
        patch.object(runner, "_extract_boundary_frames", lambda *args, **kwargs: [None]),
    ):
        result = _execute_pass(runner, gates=[_gate])

    assert _FakeVideoModelClient.submit_calls == 1
    assert result.success is False
    assert "frame_extract_failed:infra" in result.error
    assert result.segment_results[0].usable is False
    assert "infra" in result.segment_results[0].gate_error


def test_execute_pass_no_gates_allows_missing_boundary_frame(tmp_path):
    runner = _runner(tmp_path)
    _FakeVideoModelClient.submit_calls = 0

    with (
        patch(
            "recoil.execution.video_model_client.VideoModelClient",
            _FakeVideoModelClient,
        ),
        patch(
            "recoil.execution.step_runner._ensure_ffmpeg_available",
            Mock(side_effect=RuntimeError("should not probe without gates")),
        ),
        patch.object(runner, "_write_sidecar", lambda *args, **kwargs: None),
        patch.object(runner, "_extract_boundary_frames", lambda *args, **kwargs: [None]),
    ):
        result = _execute_pass(runner, gates=[])

    assert _FakeVideoModelClient.submit_calls == 1
    assert result.success is True
    assert result.segment_results[0].usable is True
    assert result.segment_results[0].gate_error is None


def test_execute_pass_probe_runs_before_dispatch(tmp_path):
    runner = _runner(tmp_path)
    client_cls = Mock()

    with (
        patch("recoil.execution.video_model_client.VideoModelClient", client_cls),
        patch(
            "recoil.execution.step_runner._ensure_ffmpeg_available",
            Mock(side_effect=RuntimeError("ffmpeg unavailable")),
        ),
    ):
        with pytest.raises(RuntimeError, match="ffmpeg unavailable"):
            _execute_pass(runner, gates=[_gate])

    client_cls.assert_not_called()


def test_execute_video_probe_runs_before_dispatch(tmp_path):
    runner = _runner(tmp_path)
    client_cls = Mock()

    with (
        patch("recoil.execution.video_model_client.VideoModelClient", client_cls),
        patch(
            "recoil.execution.step_runner._ensure_ffmpeg_available",
            Mock(side_effect=RuntimeError("ffmpeg unavailable")),
        ),
    ):
        with pytest.raises(RuntimeError, match="ffmpeg unavailable"):
            runner.execute_video(
                shot_id="EP001_SH01",
                prompt="test prompt",
                model="seeddance-2.0",
                gates=[_gate],
            )

    client_cls.assert_not_called()
