"""Tests for REC-180 ingest wiring: assembly materializes axis_plans; final-degrade sanitizes."""
from __future__ import annotations

from types import SimpleNamespace

from recoil.pipeline._lib.render_schema import (
    CameraTestedEpisode,
    CreativeEpisodeOutput,
    GlobalBible,
    ScreenDirection,
)
from recoil.pipeline.orchestrator import ingest_pipeline as ip
from recoil.pipeline.orchestrator.axis_validation import sanitize_axis_plans


def _pipeline() -> ip.IngestPipeline:
    pipe = object.__new__(ip.IngestPipeline)
    pipe.project = "demo"
    pipe.dry_run = False
    pipe.extraction_model = "opus-4.6"
    return pipe


def _camera_tested() -> CameraTestedEpisode:
    return CameraTestedEpisode.model_validate({
        "episode_id": "EP001", "project": "demo", "total_shots": 2,
        "shots": [
            {"shot_index": 1, "scene_index": 1, "source_text": "JANE studies the bridge.",
             "has_dialogue": True, "characters_mentioned": ["JANE"], "location_hint": "bridge"},
            {"shot_index": 2, "scene_index": 2, "source_text": "The bridge sits silent.",
             "has_dialogue": False, "characters_mentioned": [], "location_hint": "bridge"},
        ],
    })


def _bible() -> GlobalBible:
    return GlobalBible.model_validate({
        "project": "demo", "total_episodes": 1,
        "characters": {"JANE": {"char_id": "JANE", "display_name": "Jane",
                                "visual_description": "mid 30s", "episodes": [1],
                                "phases": [{"phase_id": "jane_ep1", "start_ep": 1, "end_ep": 1,
                                            "wardrobe_description": "navy jacket"}]}},
        "locations": {"bridge": {"location_id": "bridge", "aliases": ["INT. BRIDGE"],
                                 "description": "dim bridge",
                                 "lighting_profile": {"primary_source": "console glow", "direction": "below",
                                                      "quality": "soft", "color_temp": "cool"}}},
        "props": {},
    })


def _creative(axis_plans) -> CreativeEpisodeOutput:
    shots = [
        {"shot_index": i,
         "prompt_skeleton": {"subject_line": "a", "environment_line": "b", "action_line": "c",
                             "motion_line": "d e", "emotion_line": "f"},
         "shot_type": "CU", "target_editorial_duration_s": 4}
        for i in (1, 2)
    ]
    return CreativeEpisodeOutput.model_validate(
        {"episode_id": "EP001", "total_shots": 2, "shots": shots, "axis_plans": axis_plans})


def _motion(direction):
    return {"initial_anchor": {"kind": "motion", "reference_direction": direction}}


def test_assembly_materializes_varied():
    creative = _creative({1: _motion("left-to-right"), 2: _motion("right-to-left")})
    plan = _pipeline()._assemble_plan_from_creative(creative, _camera_tested(), _bible(), 1)
    # axis_plans copied as provenance
    assert set(plan.axis_plans) == {1, 2}
    # per-shot direction materialized (replaces the old SpatialData() stub) and varies
    dirs = {s.spatial_data.screen_direction for s in plan.shots}
    assert dirs == {ScreenDirection.LEFT_TO_RIGHT, ScreenDirection.RIGHT_TO_LEFT}


def test_degrade_path_drops_invalid():
    # scene 1 valid; scene 2 inert (non-neutral + center) -> invalid
    creative = _creative({1: _motion("left-to-right"),
                          2: {"initial_anchor": {"kind": "motion", "reference_direction": "center"}}})
    ct = _camera_tested()
    dropped = sanitize_axis_plans(creative, ct)        # the final-degrade step
    assert dropped == [2]
    plan = _pipeline()._assemble_plan_from_creative(creative, ct, _bible(), 1)
    # invalid scene 2 NOT persisted as provenance; its shot neutral-fell-back
    assert set(plan.axis_plans) == {1}
    s1 = next(s for s in plan.shots if s.scene_index == 1)
    s2 = next(s for s in plan.shots if s.scene_index == 2)
    assert s1.spatial_data.screen_direction == ScreenDirection.LEFT_TO_RIGHT
    assert s2.spatial_data.screen_direction == ScreenDirection.CENTER
    assert s2.spatial_data.camera_side == "A"


def test_input_block_has_scene():
    block = ip._creative_input_block(SimpleNamespace(
        shot_index=2, scene_index=5, has_dialogue=False, characters_mentioned=["X"], source_text="t"))
    assert "scene=5" in block and "[SHOT 2]" in block


def test_prompt_mentions_axis_plans():
    assert "axis_plans" in ip.STORYBOARD_SYSTEM_PROMPT


def test_bad_axis_enum_degrade_building_blocks():
    # The final-attempt degrade drops axis_plans and re-parses. Verify the mechanism: a bad axis
    # enum makes strict parsing raise, but stripping axis_plans yields a valid CreativeEpisodeOutput.
    import json
    import pytest
    pipe = _pipeline()
    shot = {"shot_index": 1,
            "prompt_skeleton": {"subject_line": "a", "environment_line": "b", "action_line": "c",
                                "motion_line": "d e", "emotion_line": "f"},
            "shot_type": "CU", "target_editorial_duration_s": 4}
    bad = {"episode_id": "EP001", "total_shots": 1, "shots": [shot],
           "axis_plans": {"1": {"initial_anchor": {"kind": "dialog",  # typo -> invalid enum
                                                   "reference_direction": "left-to-right"}}}}
    with pytest.raises(Exception):
        pipe._validate_creative_response(json.dumps(bad), 1)
    bad["axis_plans"] = {}  # the recovery step
    recovered = pipe._validate_creative_response(json.dumps(bad), 1)
    assert recovered.axis_plans == {}
