# recoil/pipeline/_lib/tests/test_dispatch_payload_audio.py
"""Unit tests for audio flag resolution in build_dispatch_payload.

Phase 2 (Production CLI Hygiene) — TESTS ONLY, no production file changes.
Phase 1 shipped NARRATIVE_DEFAULT_GENERATE_AUDIO and the resolution logic;
these tests prove the contract.

Resolution order (from dispatch_payload.py):
  1. shot.raw["generate_audio"] wins if present (any value).
  2. generate_audio kwarg if not None.
  3. Otherwise: NARRATIVE_DEFAULT_GENERATE_AUDIO (True).
"""

from __future__ import annotations

import struct
import sys
import zlib
from pathlib import Path

_REPO_ROOT = Path(__file__).resolve().parents[4]
if str(_REPO_ROOT) not in sys.path:
    sys.path.insert(0, str(_REPO_ROOT))

from recoil.pipeline._lib.dispatch_payload import (  # noqa: E402
    NARRATIVE_DEFAULT_GENERATE_AUDIO,
    build_dispatch_payload,
)
from recoil.pipeline._lib.plan_loader import CanonicalShot  # noqa: E402

# ---------------------------------------------------------------------------
# Minimal valid PNG bytes (1×1 white pixel) for start_frame_override.
# Avoids any real filesystem dependency.
# ---------------------------------------------------------------------------


def _make_minimal_png(path: Path) -> Path:
    """Write a minimal valid 1x1 PNG to path and return it."""

    def _chunk(name: bytes, data: bytes) -> bytes:
        length = struct.pack(">I", len(data))
        crc = struct.pack(">I", zlib.crc32(name + data) & 0xFFFFFFFF)
        return length + name + data + crc

    signature = b"\x89PNG\r\n\x1a\n"
    ihdr_data = struct.pack(">IIBBBBB", 1, 1, 8, 2, 0, 0, 0)
    ihdr = _chunk(b"IHDR", ihdr_data)
    raw_row = b"\x00\xFF\xFF\xFF"
    compressed = zlib.compress(raw_row, 9)
    idat = _chunk(b"IDAT", compressed)
    iend = _chunk(b"IEND", b"")

    path.write_bytes(signature + ihdr + idat + iend)
    return path


# ---------------------------------------------------------------------------
# Test fixture helpers
# ---------------------------------------------------------------------------


def _minimal_shot(shot_id: str = "EP001_SH01", **raw_extra) -> CanonicalShot:
    """Build a minimal CanonicalShot suitable for audio-flag tests."""
    raw: dict = {
        "shot_id": shot_id,
        "routing_data": {"target_editorial_duration_s": 3, "is_env_only": True},
        "asset_data": {"location_id": "int_lower_decks_corridor", "characters": []},
        "compiled_prompts": {"seeddance_t2v": "test prompt"},
        "prompt_data": {"shot_type": "WS"},
    }
    raw.update(raw_extra)
    return CanonicalShot(
        shot_id=shot_id,
        scene_index=1,
        sequence_id=None,
        pipeline="still",
        previs_model="gemini-3-pro-image-preview",
        video_model=None,
        location_id="int_lower_decks_corridor",
        characters=[],
        shot_type="WS",
        duration_s=3.0,
        is_env_only=True,
        has_dialogue=False,
        aspect_ratio=None,
        raw=raw,
    )


def _build(tmp_path: Path, shot: CanonicalShot, **kwargs) -> dict:
    """Call build_dispatch_payload with a real PNG start_frame_override."""
    png = _make_minimal_png(tmp_path / f"{shot.shot_id}_frame.png")
    return build_dispatch_payload(
        shot=shot,
        project="test_project",
        modality="video_i2v",
        model_override="seeddance-2.0",
        start_frame_override=png,
        **kwargs,
    )


# ---------------------------------------------------------------------------
# Tests
# ---------------------------------------------------------------------------


def test_narrative_default_generate_audio_constant_is_true():
    """NARRATIVE_DEFAULT_GENERATE_AUDIO must be True — this is the contract."""
    assert NARRATIVE_DEFAULT_GENERATE_AUDIO is True


def test_default_audio_is_true(tmp_path, monkeypatch):
    """When generate_audio kwarg is None and shot.raw has no generate_audio key,
    the payload["generate_audio"] must be True (the narrative default).
    """
    monkeypatch.setattr(
        "recoil.pipeline._lib.dispatch_payload.get_builder",
        lambda model_id, modality: lambda shot_raw, bible, pc=None: "test prompt",
    )
    shot = _minimal_shot()
    assert "generate_audio" not in shot.raw

    payload = _build(tmp_path, shot, generate_audio=None)

    assert payload["generate_audio"] is True


def test_per_shot_raw_false_overrides_default(tmp_path, monkeypatch):
    """shot.raw['generate_audio'] = False must win over the True default."""
    monkeypatch.setattr(
        "recoil.pipeline._lib.dispatch_payload.get_builder",
        lambda model_id, modality: lambda shot_raw, bible, pc=None: "test prompt",
    )
    shot = _minimal_shot(generate_audio=False)

    payload = _build(tmp_path, shot)

    assert payload["generate_audio"] is False


def test_kwarg_false_overrides_default_when_no_raw_key(tmp_path, monkeypatch):
    """generate_audio=False kwarg must produce False when raw has no key."""
    monkeypatch.setattr(
        "recoil.pipeline._lib.dispatch_payload.get_builder",
        lambda model_id, modality: lambda shot_raw, bible, pc=None: "test prompt",
    )
    shot = _minimal_shot()
    assert "generate_audio" not in shot.raw

    payload = _build(tmp_path, shot, generate_audio=False)

    assert payload["generate_audio"] is False


def test_raw_key_wins_over_kwarg(tmp_path, monkeypatch):
    """shot.raw['generate_audio'] = True must win even when kwarg=False.

    The spec: per-shot raw always takes highest priority.
    """
    monkeypatch.setattr(
        "recoil.pipeline._lib.dispatch_payload.get_builder",
        lambda model_id, modality: lambda shot_raw, bible, pc=None: "test prompt",
    )
    shot = _minimal_shot(generate_audio=True)

    payload = _build(tmp_path, shot, generate_audio=False)

    assert payload["generate_audio"] is True

