import subprocess
import tempfile
from pathlib import Path
from types import SimpleNamespace

import pytest

from recoil.execution import step_runner
from recoil.execution.step_types import GateVerdict


def _patch_inner_gate(monkeypatch, verdict: GateVerdict, calls: list[tuple[Path, dict]]):
    def fake_make_identity_gate(**_kwargs):
        def inner(frame_path: Path, shot_data: dict) -> GateVerdict:
            calls.append((frame_path, shot_data))
            return verdict

        return inner

    monkeypatch.setattr(step_runner, "make_identity_gate", fake_make_identity_gate)


def test_video_identity_gate_frame_extract_failure_fails_closed(monkeypatch, tmp_path):
    calls: list[tuple[Path, dict]] = []
    _patch_inner_gate(
        monkeypatch,
        GateVerdict(passed=True, gate_name="gate_2", reason="inner-pass"),
        calls,
    )

    def fake_run(*_args, **_kwargs):
        raise subprocess.TimeoutExpired(cmd="ffmpeg", timeout=30)

    monkeypatch.setattr(step_runner.subprocess, "run", fake_run)

    video_path = tmp_path / "take.mp4"
    video_path.write_bytes(b"video")
    gate = step_runner.make_video_identity_gate(ref_paths=[tmp_path / "ref.png"])
    verdict = gate(video_path, {"shot_id": "SH01"})

    assert calls == []
    assert verdict.passed is False
    assert verdict.gate_name == "gate_2_video"
    assert verdict.retriable is True
    assert verdict.details["gate_closed_by"] == "infra"
    assert "frame_extract_failed:infra" in verdict.reason


def test_video_identity_gate_successful_extract_delegates_to_inner_verdict(
    monkeypatch, tmp_path
):
    calls: list[tuple[Path, dict]] = []
    inner_verdict = GateVerdict(
        passed=False,
        gate_name="gate_2",
        reason="content mismatch",
        details={"total_score": 3},
        cost=0.039,
        retriable=True,
    )
    _patch_inner_gate(monkeypatch, inner_verdict, calls)

    run_calls = []

    def fake_run(*args, **kwargs):
        run_calls.append((args, kwargs))
        return SimpleNamespace(returncode=0)

    monkeypatch.setattr(step_runner.subprocess, "run", fake_run)

    video_path = tmp_path / "take.mp4"
    video_path.write_bytes(b"video")
    shot_data = {"shot_id": "SH01"}
    gate = step_runner.make_video_identity_gate(ref_paths=[tmp_path / "ref.png"])
    verdict = gate(video_path, shot_data)

    assert len(run_calls) == 1
    assert calls == [(Path(tempfile.gettempdir()) / "take_idgate.jpg", shot_data)]
    assert verdict.passed is False
    assert verdict.gate_name == "gate_2_video"
    assert verdict.reason == "content mismatch"
    assert verdict.details == {"total_score": 3}
    assert verdict.cost == 0.039
    assert verdict.retriable is True


def test_video_identity_gate_image_suffix_scores_directly_without_ffmpeg(
    monkeypatch, tmp_path
):
    calls: list[tuple[Path, dict]] = []
    inner_verdict = GateVerdict(
        passed=True,
        gate_name="gate_2",
        reason="content pass",
        details={"total_score": 0},
        cost=0.039,
        retriable=False,
    )
    _patch_inner_gate(monkeypatch, inner_verdict, calls)

    def fake_run(*_args, **_kwargs):
        raise AssertionError("ffmpeg should not run for image inputs")

    monkeypatch.setattr(step_runner.subprocess, "run", fake_run)

    frame_path = tmp_path / "boundary.jpg"
    frame_path.write_bytes(b"image")
    shot_data = {"shot_id": "SH01"}
    gate = step_runner.make_video_identity_gate(ref_paths=[tmp_path / "ref.png"])
    verdict = gate(frame_path, shot_data)

    assert calls == [(frame_path, shot_data)]
    assert verdict.passed is True
    assert verdict.gate_name == "gate_2_video"
    assert verdict.reason == "content pass"


def test_ensure_ffmpeg_available_success_caches(monkeypatch):
    monkeypatch.setattr(step_runner, "_FFMPEG_AVAILABLE", None)
    calls = []

    def fake_run(args, *, capture_output, timeout):
        calls.append((args, capture_output, timeout))
        return SimpleNamespace(returncode=0)

    monkeypatch.setattr(step_runner.subprocess, "run", fake_run)

    step_runner._ensure_ffmpeg_available()
    step_runner._ensure_ffmpeg_available()

    assert calls == [(["ffmpeg", "-version"], True, 10)]


def test_ensure_ffmpeg_available_failure_caches_and_reraises(monkeypatch):
    monkeypatch.setattr(step_runner, "_FFMPEG_AVAILABLE", None)
    calls = []

    def fake_run(args, *, capture_output, timeout):
        calls.append((args, capture_output, timeout))
        raise FileNotFoundError("ffmpeg")

    monkeypatch.setattr(step_runner.subprocess, "run", fake_run)

    with pytest.raises(RuntimeError, match="ffmpeg unavailable"):
        step_runner._ensure_ffmpeg_available()
    with pytest.raises(RuntimeError, match="ffmpeg unavailable"):
        step_runner._ensure_ffmpeg_available()

    assert calls == [(["ffmpeg", "-version"], True, 10)]
