"""Integration tests — cinema mode wired into the t2v/r2v/r2v_multi builders.

Covers:
  7f. Legacy plan fallback (no cinematography → functionally identical
      to pre-build; `Shot on {film_stock}` baseline preserved via the
      conditional `elif film_stock` branch, which is the original code
      unchanged)
  7g. Cinema mode active → tokens injected, film_stock baseline replaced
  7h. I2V builders DO NOT receive cinema tokens (regression guard)
  7i. Shot override merges correctly through the full builder pipeline
  7j. CanonicalShot.cinematography typed field + load-time validation
"""
import json
import pathlib

import pytest

from recoil.pipeline._lib.cinema_loader import CinemaConfigError
from recoil.pipeline._lib.plan_loader import load_plan
from recoil.pipeline._lib.prompt_engine import (
    build_kling_i2v_prompt,
    build_kling_t2v_prompt,
    build_seeddance_i2v_prompt,
    build_seeddance_r2v_prompt,
    build_seeddance_t2v_prompt,
    build_veo_prompt,
    build_wan_r2v_prompt,
)


def _make_shot(cinema_mode=None, cinema_overrides=None, with_start_frame=False):
    shot = {
        "shot_id": "TEST_001",
        "scene_index": 0,
        "prompt_data": {
            "prompt_skeleton": {"subject_line": "A figure stands.",
                                "action_line": "The figure turns slowly.",
                                "environment_line": "An empty corridor."},
            "shot_type": "MS",
            "camera_movement": "static",
            "kinetic_action": "turns",
        },
        "routing_data": {"target_editorial_duration_s": 5},
        "asset_data": {"characters": []},
    }
    # Phase 5 typed field — builders read `shot["cinematography"]` from
    # the dict-form shot (which mirrors CanonicalShot.cinematography after
    # plan_loader normalization).
    if cinema_mode is not None or cinema_overrides is not None:
        block = {}
        if cinema_mode is not None:
            block["mode"] = cinema_mode
        if cinema_overrides is not None:
            block["overrides"] = cinema_overrides
        shot["cinematography"] = block
    if with_start_frame:
        shot["routing_data"]["start_frame_path"] = "/tmp/x.png"
    return shot


BIBLE = {"characters": {}, "locations": {}}


# ============================================================
# 7f. Legacy plan fallback — functionally identical output for no-cinema plans
#     (conditional `elif film_stock` branch executes original code unchanged)
# ============================================================

def test_legacy_plan_seeddance_r2v_uses_film_stock_baseline():
    """No cinema_mode anywhere → legacy `shot on kodak vision3 500t` path.

    The r2v/t2v builders lowercase cinema/film-stock tokens uniformly
    (intentional `.lower()` in prompt_engine); assertions are case-exact
    against the emitted lowercase form.
    """
    shot = _make_shot()
    out = build_seeddance_r2v_prompt([shot], BIBLE, {}, 1)
    assert "kodak vision3 500t" in out.lower()
    # No cinema tokens
    assert "cooke s4/i" not in out.lower()
    assert "arri alexa" not in out.lower()


def test_legacy_plan_kling_t2v_uses_film_stock_baseline():
    shot = _make_shot()
    out = build_kling_t2v_prompt(shot, BIBLE, {}, 1)
    assert "Kodak Vision3 500T" in out
    assert "Cooke S4/i" not in out


def test_legacy_plan_veo_uses_film_stock_baseline():
    shot = _make_shot()
    out = build_veo_prompt(shot, BIBLE, {}, 1)
    assert "kodak vision3 500t" in out.lower()
    assert "cooke s4" not in out.lower()


# ============================================================
# 7g. Cinema mode active → tokens injected, baseline replaced
# ============================================================

def test_seeddance_r2v_cinema_active():
    shot = _make_shot(cinema_mode="narrative_cinematic")
    out = build_seeddance_r2v_prompt([shot], BIBLE, {}, 1).lower()
    assert "cooke s4/i" in out
    assert "arri alexa 35" in out
    # Legacy `shot on {film_stock}` baseline must be REPLACED by cinema tokens,
    # not stacked (the standalone baseline line no longer appears).
    assert "shot on kodak vision3 500t" not in out


def test_seeddance_t2v_cinema_active():
    shot = _make_shot(cinema_mode="kinetic_action")
    out = build_seeddance_t2v_prompt(shot, BIBLE, {}, 1).lower()
    assert "sony venice 2" in out  # kinetic_action body
    assert "skinny shutter" in out


def test_kling_t2v_cinema_compressed():
    shot = _make_shot(cinema_mode="narrative_cinematic")
    out = build_kling_t2v_prompt(shot, BIBLE, {}, 1)
    # Compressed render: head clauses only, no body
    assert "Cooke S4/i spherical prime lenses" in out
    assert "ARRI Alexa" not in out
    assert "classic Cooke Look" not in out  # trailing clauses dropped


def test_wan_r2v_cinema_active():
    shot = _make_shot(cinema_mode="noir_tension")
    out = build_wan_r2v_prompt([shot], BIBLE, {}, 1)
    assert "Zeiss Super Speed" in out
    assert "bleach bypass" in out


def test_project_default_applies_when_shot_has_none():
    """Shot has no cinema_mode but project_config does → project default applies."""
    shot = _make_shot()  # no cinema_mode on shot
    project_config = {"cinema_mode": "golden_hour"}
    out = build_seeddance_r2v_prompt([shot], BIBLE, project_config, 1)
    assert "golden hour" in out.lower()


def test_shot_override_beats_project_default():
    shot = _make_shot(cinema_mode="noir_tension")
    project_config = {"cinema_mode": "narrative_cinematic"}
    out = build_seeddance_r2v_prompt([shot], BIBLE, project_config, 1).lower()
    assert "zeiss super speed" in out  # noir_tension lens
    assert "cooke s4/i" not in out  # would be narrative_cinematic


# ============================================================
# 7h. I2V builders DO NOT receive cinema tokens (regression guard)
# ============================================================

def test_seeddance_i2v_ignores_cinema_mode():
    """Even with cinema_mode set, the i2v builder must not emit cinema tokens.

    (Start frame already carries the visual look — adding camera/lens
    tokens pollutes the motion directive.)
    """
    shot = _make_shot(cinema_mode="narrative_cinematic", with_start_frame=True)
    out = build_seeddance_i2v_prompt(shot, BIBLE, {}, 1)
    assert "Cooke S4/i" not in out
    assert "ARRI Alexa" not in out


def test_kling_i2v_ignores_cinema_mode():
    shot = _make_shot(cinema_mode="narrative_cinematic", with_start_frame=True)
    out = build_kling_i2v_prompt(shot, BIBLE, {})
    assert "Cooke S4/i" not in out
    assert "ARRI Alexa" not in out


def test_veo_i2v_ignores_cinema_mode():
    """build_veo_prompt is dispatched for BOTH veo-3.1 t2v AND i2v (one
    function serves both modalities via the BUILDERS table). The
    start_frame_path guard added in Phase 6f is what enforces the i2v
    skip rule for Veo — assert no camera/lens/aperture/shutter tokens
    leak when a start frame is present, even if cinematography is set.
    """
    shot = _make_shot(cinema_mode="narrative_cinematic", with_start_frame=True)
    out = build_veo_prompt(shot, BIBLE, {}, 1)
    lower = out.lower()
    # Camera/lens tokens must NOT appear — i2v image carries the look.
    assert "cooke" not in lower
    assert "arri alexa" not in lower
    assert "panavision" not in lower
    # Color-science tokens from Veo's cinema_token_map also must not
    # appear (the guard skips the entire injection, not just camera/lens).
    assert "bleach bypass" not in lower


# ============================================================
# 7i. Shot override merges through full builder pipeline
# ============================================================

def test_seeddance_r2v_shot_override_lens():
    shot = _make_shot(
        cinema_mode="narrative_cinematic",
        cinema_overrides={"lens": "panavision_ultra_vintage"},
    )
    out = build_seeddance_r2v_prompt([shot], BIBLE, {}, 1).lower()
    assert "panavision ultra vintage" in out
    assert "cooke s4/i" not in out
    # Other fields stay at mode default
    assert "arri alexa 35" in out


# ============================================================
# 7j. CanonicalShot.cinematography typed field + load-time validation
# ============================================================

def _write_plan(tmp_path, plan: dict) -> pathlib.Path:
    p = tmp_path / "plan.json"
    p.write_text(json.dumps(plan), encoding="utf-8")
    return p


def _base_plan_with_shot(shot_extra: dict) -> dict:
    base_shot = {
        "shot_id": "SH001", "scene_index": 0,
        "prompt_data": {"shot_type": "MS"},
        "routing_data": {"target_editorial_duration_s": 5},
        "asset_data": {"characters": []},
    }
    base_shot.update(shot_extra)
    return {"episode_id": "ep_test", "project": "test_project",
            "shots": [base_shot]}


def test_canonical_shot_cinematography_typed_field_populates(tmp_path):
    plan = _base_plan_with_shot({
        "cinematography": {"mode": "narrative_cinematic",
                           "overrides": {"lens": "panavision_ultra_vintage"}},
    })
    cp = load_plan(_write_plan(tmp_path, plan))
    assert cp.shots[0].cinematography == {
        "mode": "narrative_cinematic",
        "overrides": {"lens": "panavision_ultra_vintage"},
    }


def test_canonical_shot_cinematography_defaults_to_none_when_absent(tmp_path):
    plan = _base_plan_with_shot({})
    cp = load_plan(_write_plan(tmp_path, plan))
    assert cp.shots[0].cinematography is None


def test_canonical_shot_cinematography_invalid_mode_raises_at_load(tmp_path):
    plan = _base_plan_with_shot({
        "cinematography": {"mode": "definitely_not_a_real_mode"},
    })
    with pytest.raises(CinemaConfigError, match="definitely_not_a_real_mode"):
        load_plan(_write_plan(tmp_path, plan))


def test_canonical_shot_cinematography_invalid_override_id_raises_at_load(tmp_path):
    plan = _base_plan_with_shot({
        "cinematography": {"mode": "narrative_cinematic",
                           "overrides": {"lens": "bogus_lens_id"}},
    })
    with pytest.raises(CinemaConfigError, match="bogus_lens_id"):
        load_plan(_write_plan(tmp_path, plan))


def test_canonical_shot_cinematography_invalid_override_field_raises_at_load(tmp_path):
    plan = _base_plan_with_shot({
        "cinematography": {"mode": "narrative_cinematic",
                           "overrides": {"not_a_real_field": "anything"}},
    })
    with pytest.raises(CinemaConfigError, match="not_a_real_field"):
        load_plan(_write_plan(tmp_path, plan))


def test_canonical_shot_cinematography_roundtrip_via_raw(tmp_path):
    """raw["cinematography"] preserves the original block byte-for-byte."""
    block = {"mode": "noir_tension", "overrides": {"grade": "bleach_bypass"}}
    plan = _base_plan_with_shot({"cinematography": block})
    cp = load_plan(_write_plan(tmp_path, plan))
    assert cp.shots[0].cinematography == block
    assert cp.shots[0].raw["cinematography"] == block
