"""StepRunner unit tests for the Phase 3 naming-reset.

These are pragmatic, narrow tests:
- ``_save_video`` requires `model` (no default, no "unknown" sentinel)
- the r2v_multi write path produces a canonical FILENAME_PATTERN-matching name
- Phase 6 (M+K): ``_write_sidecar`` writes a .mp4.json with refs_used populated

Per BUILD_SPEC Phase 3 § Tests, when full StepRunner mocking is heavyweight
the second test is allowed to fall through to a direct ``build_filename``
unit assertion using realistic beat-shaped inputs. We do both: assert the
signature contract by introspection, and assert build_filename with the
inputs an r2v_multi beat would produce.

Phase 6 sidecar tests call ``StepRunner._write_sidecar`` directly with a
tmp_path video stub and a minimal payload — pragmatic over full integration.
"""

from __future__ import annotations

import inspect
import json
from types import SimpleNamespace

from recoil.core.naming import FILENAME_PATTERN, build_filename
from recoil.execution import step_runner as step_runner_mod


def test_save_video_requires_model():
    """``StepRunner._save_video`` must require `model` (no default).

    Phase 3 naming-reset eliminates the "unknown" sentinel default — any
    accidental re-introduction must fail this contract assertion.
    """
    sig = inspect.signature(step_runner_mod.StepRunner._save_video)
    assert "model" in sig.parameters, "_save_video must accept `model` kwarg"

    model_param = sig.parameters["model"]
    # `model` must be keyword-only and have NO default.
    assert model_param.default is inspect.Parameter.empty, (
        f"_save_video.model must have no default, got default="
        f"{model_param.default!r}"
    )
    assert model_param.kind in (
        inspect.Parameter.KEYWORD_ONLY,
        inspect.Parameter.POSITIONAL_OR_KEYWORD,
    ), f"unexpected parameter kind for `model`: {model_param.kind}"


def test_r2v_multi_canonical_name():
    """An r2v_multi beat's filename must match FILENAME_PATTERN.

    Realistic beat-shaped inputs: a 3-shot SeedDance coverage pass anchored
    on character JADE. Under the R4 SHORT grammar, project/tag/model have
    migrated to the sidecar (NAMING_DIGEST §"Field migration")—the filename
    carries episode, optional pass counter, shot list, take, and extension only.
    """
    name = build_filename(
        episode=1,
        pass_counter=14,
        shot_ids=["EP001_SH30", "EP001_SH31", "EP001_SH32"],
        take=1,
    )
    assert FILENAME_PATTERN.match(name), (
        f"r2v_multi build did not produce a canonical name: {name!r}"
    )
    assert name == "EP001_PASS_014_SH30_31_32_take1.mp4"


# ──────────────────────────────────────────────────────────────────────
# Phase 6 (M + K): _write_sidecar + refs_used population
# ──────────────────────────────────────────────────────────────────────


def _make_bare_step_runner():
    """Build a StepRunner instance bypassing __init__ for unit-testing helpers.

    The helper under test (_write_sidecar) doesn't touch self.* — only its
    kwargs — so we don't need a fully-wired StepRunner.
    """
    return step_runner_mod.StepRunner.__new__(step_runner_mod.StepRunner)


def _make_run_result_stub(*, cost_usd: float = 1.23, seed: int | None = 42, metadata=None):
    """Build a RunResult-shaped stub with cost_usd and metadata."""
    meta = metadata if metadata is not None else ({"seed": seed} if seed is not None else {})
    return SimpleNamespace(cost_usd=cost_usd, metadata=meta)


def test_write_sidecar_basic_shape(tmp_path):
    """``_write_sidecar`` writes a .mp4.json sidecar with the M+K schema."""
    sr = _make_bare_step_runner()
    video_path = tmp_path / "TARTARUS_EP001_PASS_014_SH30_A_JADE_seeddance-2-0_take1.mp4"
    video_path.write_bytes(b"stub-video-bytes")

    payload = {
        "prompt": "wide shot of jade entering the corridor",
        "reference_images": ["/refs/jade.png", "/refs/corridor.png"],
        "duration_s": 10,
        "model": "seeddance-2.0",
        "modality": "r2v_multi",
    }
    sr._write_sidecar(
        video_path=video_path,
        run_result=_make_run_result_stub(),
        unified_payload=payload,
    )

    sidecar_path = video_path.with_suffix(video_path.suffix + ".json")
    assert sidecar_path.exists(), "sidecar .mp4.json must be created"

    data = json.loads(sidecar_path.read_text())
    assert data["schema_version"] == "1.0"
    assert data["video_path"] == str(video_path)
    assert data["model"] == "seeddance-2.0"
    assert data["modality"] == "r2v_multi"
    assert data["cost_usd"] == 1.23
    assert data["duration_s"] == 10
    assert data["prompt"] == "wide shot of jade entering the corridor"
    assert "provenance" in data
    assert data["provenance"]["seed"] == 42
    # Phase 7 fields stubbed.
    assert data["provenance"]["gate_results"] == {}
    assert data["provenance"]["prompt_layers"] == {}


def test_sidecar_refs_used_populated(tmp_path):
    """A payload with 4 reference_images → sidecar's refs_used has length 4 (Bug K)."""
    sr = _make_bare_step_runner()
    video_path = tmp_path / "out.mp4"
    video_path.write_bytes(b"x")

    refs = [
        "/refs/r1.png",
        "/refs/r2.png",
        "/refs/r3.png",
        "/refs/r4.png",
    ]
    payload = {
        "prompt": "p",
        "reference_images": refs,
        "duration_s": 5,
        "model": "seeddance-2.0",
        "modality": "r2v_multi",
    }
    sr._write_sidecar(
        video_path=video_path,
        run_result=_make_run_result_stub(),
        unified_payload=payload,
    )

    data = json.loads((video_path.parent / (video_path.name + ".json")).read_text())
    # R6 Phase 2: refs_used canonicalized to list[dict] via _normalize_refs.
    assert data["provenance"]["refs_used"] == [{"path": r} for r in refs]
    assert len(data["provenance"]["refs_used"]) == 4


def test_single_shot_sidecar_refs_used(tmp_path):
    """Single-shot path (mirrors _save_video) also populates refs_used.

    Spec calls this the _save_video single-shot scenario — we exercise the
    helper directly with a single ref to confirm refs_used carries through.
    """
    sr = _make_bare_step_runner()
    video_path = tmp_path / "single.mp4"
    video_path.write_bytes(b"")

    payload = {
        "prompt": "single ref shot",
        "reference_images": ["/refs/hero.png"],
        "duration_s": 5,
        "model": "kling-o3",
        "modality": "i2v",
    }
    sr._write_sidecar(
        video_path=video_path,
        run_result=_make_run_result_stub(),
        unified_payload=payload,
    )

    data = json.loads((video_path.parent / (video_path.name + ".json")).read_text())
    # R6 Phase 2: refs_used canonicalized to list[dict] via _normalize_refs.
    assert data["provenance"]["refs_used"] == [{"path": "/refs/hero.png"}]
    assert data["model"] == "kling-o3"
    assert data["modality"] == "i2v"


def test_sidecar_empty_refs_when_none_provided(tmp_path):
    """No reference_images key → refs_used is an empty list, not missing."""
    sr = _make_bare_step_runner()
    video_path = tmp_path / "no_refs.mp4"
    video_path.write_bytes(b"")

    sr._write_sidecar(
        video_path=video_path,
        run_result=_make_run_result_stub(seed=None),
        unified_payload={
            "prompt": "no refs",
            "duration_s": 5,
            "model": "seeddance-2.0",
            "modality": "t2v",
        },
    )

    data = json.loads((video_path.parent / (video_path.name + ".json")).read_text())
    assert data["provenance"]["refs_used"] == []
    assert data["provenance"]["seed"] is None


def test_execute_pass_writes_sidecar_smoke():
    """Smoke test: _write_sidecar is called from execute_pass body.

    Full integration mock of execute_pass is heavyweight (VideoModelClient,
    fal storage, ffmpeg). Per spec § Tests "pragmatism over completeness",
    we assert the source-level invariant: _write_sidecar appears inside
    the execute_pass body. The shape correctness is covered by the
    direct-helper tests above.
    """
    src = inspect.getsource(step_runner_mod.StepRunner.execute_pass)
    assert "_write_sidecar" in src, (
        "execute_pass must call self._write_sidecar after writing the r2v_multi video"
    )
