"""CP-4 Phase 4 — VideoRunner unit tests."""

import sys
import pathlib
import json
from types import SimpleNamespace
from unittest.mock import patch

sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent.parent.parent))
from recoil.core.paths import ensure_pipeline_importable

ensure_pipeline_importable()


from recoil.pipeline.core.registry import (  # noqa: E402
    MODALITY_VIDEO_I2V,
    register_runner,
    get_runner,
)
from recoil.pipeline.core.runners.video_runner import VideoRunner  # noqa: E402
from recoil.core.naming import build_filename  # noqa: E402
from recoil.execution.step_runner import StepRunner  # noqa: E402
from recoil.execution.types import GenerationResult  # noqa: E402


class _FakeStepRunner:
    """Captures execute_video calls. Returns a SimpleNamespace shaped
    like the REAL StepResult contract from recoil/execution/step_types.py
    (verified 2026-04-27).
    """

    def __init__(
        self,
        simulate_success=True,
        simulate_state="video_complete",
        simulate_path="/tmp/fake.mp4",
    ):
        self.calls = []
        self._success = simulate_success
        self._state = simulate_state
        self._path = simulate_path

    def execute_video(self, **kwargs):
        self.calls.append(kwargs)
        return SimpleNamespace(
            shot_id=kwargs.get("shot_id", "TEST"),
            success=self._success,
            final_state=self._state,
            output_path=self._path,
            cost_usd=0.20,
            error=None,
            take_index=0,
            gate_verdict=None,
            model="seeddance-2.0",
            pipeline="i2v",
        )


def test_video_runner_modality():
    r = VideoRunner(_FakeStepRunner())
    assert r.modality == MODALITY_VIDEO_I2V == "video_i2v"


def test_video_runner_i2v_path():
    sr = _FakeStepRunner()
    r = VideoRunner(sr)
    out = r.run(
        {
            "shot_id": "EP001_SH02",
            "prompt": "a hand reaches",
            "model": "kling-o3",
            "start_frame": "/tmp/start.png",
            "duration": 5,
            "aspect_ratio": "9_16",
        }
    )
    assert out.success is True
    assert out.output_path == "/tmp/fake.mp4"
    assert out.modality == "video_i2v"
    from pathlib import Path

    assert sr.calls[0]["start_frame"] == Path("/tmp/start.png")


def test_video_runner_t2v_path():
    """start_frame absent → t2v branch in execute_video."""
    sr = _FakeStepRunner()
    r = VideoRunner(sr)
    out = r.run(
        {
            "shot_id": "EP001_SH02",
            "prompt": "establishing shot",
            "model": "seeddance-2.0",
            "aspect_ratio": "9_16",
        }
    )
    assert out.success is True
    assert sr.calls[0]["start_frame"] is None


def test_video_runner_threads_optional_kwargs():
    sr = _FakeStepRunner()
    r = VideoRunner(sr)

    def on_status(s):
        return None

    r.run(
        {
            "shot_id": "x",
            "prompt": "y",
            "model": "kling-v3",
            "duration": 8,
            "aspect_ratio": "16:9",
            "elements_payload": {"foo": "bar"},
            "generate_audio": False,
            "on_status": on_status,
            "reference_images": ["url1", "url2"],
            "negative_prompt": "blurry",
            "inputs_snapshot": {"snap": True},
        }
    )
    call = sr.calls[0]
    assert call["duration"] == 8
    assert call["aspect_ratio"] == "16:9"
    assert call["elements_payload"] == {"foo": "bar"}
    assert call["generate_audio"] is False
    assert call["on_status"] is on_status
    assert call["reference_images"] == ["url1", "url2"]
    assert call["negative_prompt"] == "blurry"
    assert call["inputs_snapshot"] == {"snap": True}


def test_video_runner_threads_grouping_kwargs():
    sr = _FakeStepRunner()
    r = VideoRunner(sr)
    grouping = {
        "strategy": "continuity",
        "ordinal": 7,
        "shot_ids": ["EP001_SH01"],
        "source_pass_id": None,
    }

    r.run(
        {
            "shot_id": "EP001_SH01",
            "prompt": "a hand reaches",
            "model": "kling-v3",
            "aspect_ratio": "16:9",
            "grouping": grouping,
        }
    )

    assert sr.calls[0]["grouping"] == grouping


def test_video_i2v_live_writer_scan_preserves_take1_and_creates_take2(tmp_path):
    video_dir = tmp_path / "renders"
    frames_dir = tmp_path / "frames"
    for path in (video_dir, frames_dir):
        path.mkdir()
    start_frame = frames_dir / "start.png"
    start_frame.write_bytes(b"png")
    paths = SimpleNamespace(
        project="fixture",
        project_root=tmp_path,
        video_dir=video_dir,
    )
    grouping = {
        "strategy": "oner",
        "ordinal": 3,
        "shot_ids": ["EP001_SH05"],
        "source_pass_id": None,
    }
    take1 = video_dir / build_filename(
        episode=1,
        strategy="oner",
        ordinal=3,
        shot_ids=["EP001_SH05"],
        take=1,
    )
    take1.write_bytes(b"original take1")

    class Store:
        def __init__(self):
            self.takes = []

        def update_shot(self, *args, **kwargs):
            return None

        def get_shot(self, shot_id):
            return {}

        def append_take(self, shot_id, take_record):
            self.takes.append(take_record)
            return len(self.takes)

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

        def submit(self, payload):
            return SimpleNamespace(result=None)

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

    step_runner = StepRunner(store=Store(), paths=paths, validate_frames=False)
    runner = VideoRunner(step_runner)

    with patch(
        "recoil.execution.video_model_client.VideoModelClient",
        FakeVideoModelClient,
    ):
        result = runner.run(
            {
                "shot_id": "EP001_SH05",
                "prompt": "one shot oner",
                "model": "seeddance-2.0",
                "start_frame": str(start_frame),
                "duration": 4,
                "aspect_ratio": "9:16",
                "generate_audio": False,
                "grouping": grouping,
            }
        )

    take2 = video_dir / build_filename(
        episode=1,
        strategy="oner",
        ordinal=3,
        shot_ids=["EP001_SH05"],
        take=2,
    )
    sidecar = take2.with_suffix(take2.suffix + ".json")
    sidecar_data = json.loads(sidecar.read_text())

    assert result.success is True
    assert take1.read_bytes() == b"original take1"
    assert take2.read_bytes() == b"new take2"
    assert "_ONER_003_" in result.output_path
    assert sidecar_data["provenance"]["grouping"] == grouping


def test_video_runner_missing_required():
    r = VideoRunner(_FakeStepRunner())
    out = r.run({"shot_id": "x"})
    assert out.success is False
    assert "missing required keys" in out.error
    # Bug 6.1 — failure-path metadata: canonical 6 keys, final_state="failed", cost_usd=0.0
    for key in (
        "final_state",
        "cost_usd",
        "gate_verdict",
        "take_index",
        "model",
        "pipeline",
    ):
        assert key in out.metadata
    assert out.metadata["final_state"] == "failed"
    assert out.metadata["cost_usd"] == 0.0
    assert out.metadata["gate_verdict"] is None
    assert out.metadata["take_index"] is None
    assert out.metadata["model"] is None
    assert out.metadata["pipeline"] is None


def test_video_runner_exception_caught():
    class _Boom:
        def execute_video(self, **k):
            raise ValueError("model unavailable")

    out = VideoRunner(_Boom()).run({"shot_id": "x", "prompt": "y", "model": "z", "aspect_ratio": "9_16"})
    assert out.success is False
    assert "ValueError" in out.error
    assert "model unavailable" in out.error
    for key in (
        "final_state",
        "cost_usd",
        "gate_verdict",
        "take_index",
        "model",
        "pipeline",
    ):
        assert key in out.metadata
    assert out.metadata["final_state"] == "failed"
    assert out.metadata["cost_usd"] == 0.0
    assert out.metadata["gate_verdict"] is None
    assert out.metadata["take_index"] is None
    assert out.metadata["model"] is None
    assert out.metadata["pipeline"] is None


def test_video_runner_failure_status():
    sr = _FakeStepRunner(simulate_success=False, simulate_state="video_failed")
    out = VideoRunner(sr).run({"shot_id": "x", "prompt": "y", "model": "z"})
    assert out.success is False


def test_video_runner_register_and_resolve():
    sr = _FakeStepRunner()
    runner = VideoRunner(sr)
    register_runner(MODALITY_VIDEO_I2V, runner)
    assert get_runner("video_i2v") is runner
