"""R4 Phase 3 — tests for the video sidecar populator.

Distinct from `recoil/pipeline/tests/test_sidecar.py` which covers the
asset-manifest read protocol. These tests cover `populate_sidecar` and
`write_sidecar_dict` — the .mp4.json writer used by both dispatch_cli
single-shot and step_runner.execute_pass.
"""

import json
from pathlib import Path
from types import SimpleNamespace

import pytest

from recoil.pipeline._lib.sidecar import (
    SIDECAR_SCHEMA_VERSION,
    _normalize_refs,
    populate_sidecar,
    write_sidecar_dict,
)


def test_populate_sidecar_basic():
    receipt = SimpleNamespace(
        cost_usd=1.21,
        run_result=SimpleNamespace(metadata={"seed": 2010003547}),
    )
    payload = {
        "model": "seeddance-2.0",
        "modality": "video_i2v",
        "duration": 5.0,
        "prompt": "Medium close-up. The subject in @Image1 turns.",
        "reference_images": ["/abs/path/jade_hero.png"],
        "video_path": "/abs/out/EP001_SH10_take16.mp4",
    }
    sc = populate_sidecar(
        receipt=receipt,
        payload=payload,
        tag="SOLO_JADE",
        project="tartarus",
        gate_results={"hero_frame_ok": True},
        prompt_layers={"action": "turns", "subject": "the protagonist"},
    )
    assert sc["schema_version"] == SIDECAR_SCHEMA_VERSION
    assert sc["model"] == "seeddance-2.0"
    assert sc["tag"] == "SOLO_JADE"
    assert sc["project"] == "tartarus"
    assert sc["cost_usd"] == 1.21
    assert sc["provenance"]["seed"] == 2010003547
    assert sc["provenance"]["refs_used"] == [{"path": "/abs/path/jade_hero.png"}]
    assert sc["provenance"]["gate_results"] == {"hero_frame_ok": True}
    # prompt_engine_version is the 12-char SHA256 hash exported by prompt_engine.py
    assert len(sc["provenance"]["prompt_engine_version"]) == 12


def test_populate_sidecar_explicit_refs_override_payload():
    payload = {"reference_images": ["from_payload.png"]}
    sc = populate_sidecar(
        receipt=None,
        payload=payload,
        refs_used=["explicit.png"],
    )
    assert sc["provenance"]["refs_used"] == [{"path": "explicit.png"}]


def test_populate_sidecar_no_receipt():
    sc = populate_sidecar(
        receipt=None,
        payload={"model": "kling-v3", "modality": "video_i2v"},
    )
    assert sc["cost_usd"] == 0.0
    assert sc["provenance"]["seed"] is None


def test_populate_sidecar_model_filename_id_derived():
    sc = populate_sidecar(
        receipt=None,
        payload={"model": "seeddance-2.0", "modality": "video_i2v"},
    )
    # filename id stable across naming module: dots become dashes
    assert sc["model_filename_id"] is not None
    assert "." not in sc["model_filename_id"]


def test_write_sidecar_dict(tmp_path):
    sc_path = tmp_path / "EP001_SH10_take16.mp4.json"
    sc = populate_sidecar(
        receipt=None,
        payload={"model": "kling-v3", "modality": "video_i2v"},
    )
    write_sidecar_dict(sc_path, sc)
    loaded = json.loads(sc_path.read_text())
    assert loaded["model"] == "kling-v3"
    assert loaded["schema_version"] == SIDECAR_SCHEMA_VERSION


# ───────────────────────────────────────────────────────────────
# R6 Phase 3 — extended populate_sidecar tests
# ───────────────────────────────────────────────────────────────


# ── _normalize_refs (Phase 2 helper) ───────────────────────────


def test_normalize_refs_none():
    assert _normalize_refs(None) == []


def test_normalize_refs_str_path_dict_mixed():
    out = _normalize_refs([
        "a.png",
        Path("/abs/b.png"),
        {"path": "c.png", "role": "hero"},
        None,
    ])
    assert out == [
        {"path": "a.png"},
        {"path": "/abs/b.png"},
        {"path": "c.png", "role": "hero"},
    ]


def test_normalize_refs_rejects_bare_string():
    with pytest.raises(TypeError):
        _normalize_refs("not-a-list.png")


def test_normalize_refs_rejects_non_path_element():
    with pytest.raises(TypeError):
        _normalize_refs([42])


# ── housekeeping defaults ──────────────────────────────────────


def test_populate_sidecar_housekeeping_defaults_stamped():
    sc = populate_sidecar(
        receipt=None,
        payload={"model": "kling-v3", "modality": "video_i2v"},
    )
    assert sc["source"] == "pipeline"
    assert sc["status"] == "candidate"
    assert sc["lineage"] == {}
    assert sc["notes"] == ""
    assert sc["tags"] == []
    assert isinstance(sc["created_at"], str)
    assert isinstance(sc["updated_at"], str)
    assert sc["created_at"].endswith("Z")


def test_populate_sidecar_housekeeping_overrides():
    sc = populate_sidecar(
        receipt=None,
        payload={"model": "kling-v3", "modality": "video_i2v"},
        source="manual_drop",
        status="approved",
    )
    assert sc["source"] == "manual_drop"
    assert sc["status"] == "approved"


# ── dual-emit (Opus R3 Q4) ─────────────────────────────────────


def test_populate_sidecar_dual_emit_model_prompt_cost():
    sc = populate_sidecar(
        receipt=None,
        payload={
            "model": "seeddance-2.0",
            "modality": "video_i2v",
            "prompt": "shot prompt",
            "cost_usd": 1.517,
        },
    )
    assert sc["model"] == "seeddance-2.0"
    assert sc["provenance"]["model"] == "seeddance-2.0"
    assert sc["prompt"] == "shot prompt"
    assert sc["provenance"]["prompt"] == "shot prompt"
    assert sc["cost_usd"] == 1.517
    # NOTE: provenance uses LEGACY KEY "cost", top-level uses MODERN "cost_usd"
    assert sc["provenance"]["cost"] == 1.517


# ── receipt-None payload fallback (Opus R3 Q3) ─────────────────


def test_populate_sidecar_with_receipt_none_payload_fallback():
    sc = populate_sidecar(
        receipt=None,
        payload={
            "model": "seeddance-2.0",
            "modality": "video_i2v",
            "cost_usd": 0.42,
            "seed": 12345,
        },
    )
    assert sc["cost_usd"] == 0.42
    assert sc["provenance"]["seed"] == 12345
    assert sc["provenance"]["cost"] == 0.42


# ── refs shape coverage ────────────────────────────────────────


def test_populate_sidecar_refs_string_input_normalized():
    sc = populate_sidecar(
        receipt=None,
        payload={"model": "x", "modality": "video_i2v"},
        refs_used=["a.png", "b.png"],
    )
    assert sc["provenance"]["refs_used"] == [
        {"path": "a.png"},
        {"path": "b.png"},
    ]


def test_populate_sidecar_refs_dict_input_passthrough():
    sc = populate_sidecar(
        receipt=None,
        payload={"model": "x", "modality": "video_i2v"},
        refs_used=[{"path": "a.png", "role": "hero"}],
    )
    assert sc["provenance"]["refs_used"] == [{"path": "a.png", "role": "hero"}]


def test_populate_sidecar_refs_path_input_normalized():
    sc = populate_sidecar(
        receipt=None,
        payload={"model": "x", "modality": "video_i2v"},
        refs_used=[Path("/abs/p.png")],
    )
    assert sc["provenance"]["refs_used"] == [{"path": "/abs/p.png"}]


# ── new explicit kwargs land at the right level ────────────────


def test_populate_sidecar_explicit_kwargs_land_in_provenance():
    sc = populate_sidecar(
        receipt=None,
        payload={"model": "x", "modality": "video_i2v"},
        dispatch_path="video_i2v_dispatch",
        provider_adapter="fal_seeddance",
        pipeline="video_i2v",
        shot_id="EP001_SH10",
        generation_params={"duration": 5.0, "aspect_ratio": "9:16"},
        inputs_snapshot_hash="sha:abc123",
        location_id="int_apartment",
    )
    p = sc["provenance"]
    assert p["dispatch_path"] == "video_i2v_dispatch"
    assert p["provider_adapter"] == "fal_seeddance"
    assert p["pipeline"] == "video_i2v"
    assert p["shot_id"] == "EP001_SH10"
    assert p["generation_params"] == {"duration": 5.0, "aspect_ratio": "9:16"}
    assert p["inputs_snapshot_hash"] == "sha:abc123"
    assert p["location_id"] == "int_apartment"


def test_populate_sidecar_pipeline_default_unknown():
    sc = populate_sidecar(
        receipt=None,
        payload={"model": "x", "modality": "video_i2v"},
    )
    assert sc["provenance"]["pipeline"] == "unknown"
    assert sc["provenance"]["dispatch_path"] == "unknown"


# ── snapshot isomorphism vs legacy fixture (Opus R3 Q6 / cond 8) ──


def test_populate_sidecar_field_isomorphism():
    """populate_sidecar output must be a key-superset of the legacy
    write_pipeline_sidecar_RETIRED fixture. R6 Phase 7 deletes
    write_pipeline_sidecar_RETIRED; this snapshot survives because the
    fixture is static.
    """
    fixture_path = Path(__file__).parent / "fixtures" / "legacy_sidecar_shape.json"
    legacy = json.loads(fixture_path.read_text())

    sc = populate_sidecar(
        receipt=None,
        payload={
            "model": "seeddance-2.0",
            "modality": "video_i2v",
            "prompt": "Medium close-up. The subject in @Image1 turns.",
            "cost_usd": 1.517,
            "seed": 2010003547,
            "duration": 5.0,
            "video_path": "/x/video.mp4",
        },
        refs_used=[{"path": "/abs/path/jade_hero.png"}],
        gate_results={"hero_frame_ok": True},
        prompt_layers={"action": "turns", "subject": "the protagonist"},
        dispatch_path="video_i2v_dispatch",
        provider_adapter="fal_seeddance",
        pipeline="video_i2v",
        shot_id="EP001_SH10",
        generation_params={"duration": 5.0, "aspect_ratio": "9:16"},
        inputs_snapshot_hash="sha:abc123",
        location_id="int_apartment",
    )

    # Every top-level key in the legacy fixture must exist in the new output.
    for k in legacy.keys():
        assert k in sc, f"R6 populate_sidecar missing legacy top-level key: {k}"
    # Every provenance key in the legacy fixture must exist in the new provenance.
    for k in legacy["provenance"].keys():
        assert k in sc["provenance"], (
            f"R6 populate_sidecar.provenance missing legacy key: {k}"
        )


# ── top-level model present (Risk #2 from SYNTHESIS) ───────────


def test_populate_sidecar_top_level_model_present():
    """Risk #2: tree.py:311 reads top-level `model`. Silent regression
    if we forget to emit it.
    """
    sc = populate_sidecar(
        receipt=None,
        payload={"model": "kling-v3", "modality": "video_i2v"},
    )
    assert sc["model"] == "kling-v3", "tree.py would show 'None' in model column"


# ── write_sidecar_dict integration (Phase 1 + Phase 2) ─────────


def test_write_sidecar_dict_atomic_round_trip(tmp_path):
    sc_path = tmp_path / "EP001_SH10_take18.mp4.json"
    sc = populate_sidecar(
        receipt=None,
        payload={
            "model": "seeddance-2.0",
            "modality": "video_i2v",
            "cost_usd": 1.517,
            "video_path": str(tmp_path / "EP001_SH10_take18.mp4"),
        },
        refs_used=["/abs/p.png"],
        shot_id="EP001_SH10",
        pipeline="video_i2v",
    )
    write_sidecar_dict(sc_path, sc)
    loaded = json.loads(sc_path.read_text())
    assert loaded["model"] == "seeddance-2.0"
    assert loaded["provenance"]["shot_id"] == "EP001_SH10"
    # Phase 1: lock file should persist (no auto-unlink).
    lock = tmp_path / ".EP001_SH10_take18.mp4.json.lock"
    assert lock.exists(), "Phase 1 lock file should not be auto-unlinked"
