"""Unit tests for prompt_engine builders."""

from __future__ import annotations

import re
import sys
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 import prompt_engine  # noqa: E402


def _minimal_core_semantics() -> dict:
    return {
        "wardrobe": "",
        "film_stock": "",
        "lighting_data": {},
        "scene_visual_locks": "",
        "environment_line": "",
        "allow_music": False,
        "_project_config": {},
        "arc_preamble": "",
    }


def _motion_bible() -> dict:
    return {
        "characters": {
            "JADE": {
                "display_name": "Jade",
                "role": "protagonist",
                "visual_description": "lean salvage pilot",
            },
            "WREN": {
                "display_name": "Wren",
                "role": "support",
                "visual_description": "armored tactical partner",
            },
        },
        "locations": {
            "corridor": {
                "display_name": "Corridor",
                "spatial_description": "a lower-deck corridor",
            }
        },
    }


_MISSING = object()


def _storyboard_segments() -> list[dict]:
    return [
        {
            "setting": "Airlock",
            "intent": "Jade checks the pressure gauge",
        }
    ]


def test_storyboard_strip_prompt_fix_notes_none_byte_identical():
    expected = (
        "Create a storyboard as ONE single horizontal image: 1 panels in a "
        "single row, left to right, panels numbered 1-1. Each panel is a "
        "9:16 vertical film frame.\n\n"
        "STYLE:\n"
        f"{prompt_engine.STORYBOARD_STYLE_LOCK}\n\n"
        "REFERENCE MAPPING:\n"
        "No attached references.\n\n"
        "AUTHORING:\n"
        f"{prompt_engine.STORYBOARD_DIRECTOR_PREAMBLE}\n"
        "Draw EXACTLY 1 panels, one per numbered beat below, in order. "
        "No invented panels, no filler.\n\n"
        "STORY BEATS:\n"
        "1.\n"
        "Setting: Airlock\n"
        "Jade checks the pressure gauge"
    )

    omitted = prompt_engine.build_storyboard_strip_prompt(
        _storyboard_segments(),
        1,
        {},
        {},
        False,
    )
    explicit_none = prompt_engine.build_storyboard_strip_prompt(
        _storyboard_segments(),
        1,
        {},
        {},
        False,
        fix_notes=None,
    )

    assert omitted == expected
    assert explicit_none == expected


def test_storyboard_strip_prompt_renders_indexed_fix_notes():
    prompt = prompt_engine.build_storyboard_strip_prompt(
        _storyboard_segments(),
        1,
        {},
        {},
        False,
        fix_notes=[
            "Panel 1: Move Jade closer to the visible pressure gauge.",
            "Transition 1->2: Preserve the left-to-right screen direction.",
        ],
    )

    section = (
        "FIX NOTES (from the previous attempt's review - obey these)\n"
        "Panel 1: Move Jade closer to the visible pressure gauge.\n"
        "Transition 1->2: Preserve the left-to-right screen direction."
    )
    assert prompt.endswith("\n\n" + section)
    assert (
        prompt.count("FIX NOTES (from the previous attempt's review - obey these)")
        == 1
    )


def _motion_shot(
    *,
    action_line: str = "Jade presses the latch",
    motion_line: object = _MISSING,
    characters: list[dict] | None = None,
) -> dict:
    skeleton = {
        "subject_line": "Jade braces at the hatch",
        "action_line": action_line,
        "emotion_line": "Jade stays focused",
    }
    if motion_line is not _MISSING:
        skeleton["motion_line"] = str(motion_line)
    return {
        "shot_id": "SH01",
        "routing_data": {"target_editorial_duration_s": 4},
        "asset_data": {
            "location_id": "corridor",
            "characters": characters or [{"char_id": "JADE"}],
        },
        "prompt_data": {
            "shot_type": "MS",
            "focal_length": "50mm",
            "camera_movement": "static",
            "prompt_skeleton": skeleton,
        },
    }


def _patch_motion_builder_deps(monkeypatch):
    monkeypatch.setattr(prompt_engine, "load_cinema_modes", lambda: {"modes": {}})
    monkeypatch.setattr(prompt_engine, "render_camera_line", lambda *args, **kwargs: "")
    monkeypatch.setattr(prompt_engine, "_get_include_focal_length", lambda _model: False)


def test_seeddance_r2v_prompt_appends_motion_line_after_action(monkeypatch):
    _patch_motion_builder_deps(monkeypatch)
    shot = _motion_shot(motion_line="Jade shoves harder and Wren's signal light flickers")

    prompt = prompt_engine.build_seeddance_r2v_prompt(
        [shot],
        _motion_bible(),
        project_config={},
        ref_manifest={"identity_1": 1, "identity_2": 3, "scene_1": 2},
        _core_semantics=_minimal_core_semantics(),
    )

    assert (
        "Jade presses the latch. "
        "Jade shoves harder and Wren's signal light flickers. "
        "Jade stays focused."
    ) in prompt


def test_seeddance_r2v_prompt_legacy_without_motion_line_byte_identical(monkeypatch):
    _patch_motion_builder_deps(monkeypatch)
    shot = _motion_shot()

    prompt = prompt_engine.build_seeddance_r2v_prompt(
        [shot],
        _motion_bible(),
        project_config={},
        ref_manifest={"identity_1": 1, "scene_1": 2},
        _core_semantics=_minimal_core_semantics(),
    )

    assert prompt == (
        "@Image1 is Jade \u2014 lean salvage pilot. "
        "@Image2 is Corridor \u2014 a lower-deck corridor. "
        "Shot 1: Static Medium shot. Jade from @Image1 in @Image2. "
        "Jade braces at the hatch. Jade presses the latch. Jade stays focused. 1s. "
        "Cinematic, photorealistic. "
        "4k, hd, rich details, sharp clarity, cinematic texture, natural colors, "
        "stable picture. Diegetic audio only. No music, no score."
    )


def test_seeddance_r2v_prompt_warns_once_for_missing_motion_lines(
    monkeypatch, caplog
):
    _patch_motion_builder_deps(monkeypatch)
    caplog.set_level("WARNING", logger=prompt_engine.__name__)
    shots = [
        _motion_shot(motion_line="Jade leans into the latch"),
        _motion_shot(),
        _motion_shot(motion_line=""),
    ]

    prompt_engine.build_seeddance_r2v_prompt(
        shots,
        _motion_bible(),
        project_config={},
        ref_manifest={"identity_1": 1, "scene_1": 2},
        _core_semantics=_minimal_core_semantics(),
    )

    messages = [
        record.getMessage()
        for record in caplog.records
        if "missing motion_line" in record.getMessage()
    ]
    assert messages == ["r2v: 2/3 shots missing motion_line (shot numbers: 2, 3)"]


def test_seeddance_r2v_multi_appends_motion_line_after_action_and_binds_names(
    monkeypatch,
):
    _patch_motion_builder_deps(monkeypatch)
    shot = _motion_shot(
        motion_line="Jade shoves Wren aside",
        characters=[{"char_id": "JADE"}, {"char_id": "WREN"}],
    )

    prompt = prompt_engine.build_seeddance_r2v_prompt_multi(
        [shot],
        _motion_bible(),
        project_config={},
        coverage_pass_dict={
            "focus_character": "JADE",
            "location_id": "corridor",
            "segments": [shot],
        },
        ref_manifest={"identity_1": 1, "identity_2": 3, "scene_1": 2},
        _core_semantics=_minimal_core_semantics(),
    )

    shot_line = re.search(r"Shot 1:.*? 4s\.", prompt).group(0)
    assert (
        "@Image1 presses the latch. @Image1 shoves @Image3 aside. "
        "the subject stays focused."
    ) in shot_line
    assert "Jade shoves Wren" not in shot_line


def test_seeddance_r2v_multi_legacy_without_motion_line_byte_identical(monkeypatch):
    _patch_motion_builder_deps(monkeypatch)
    shot = _motion_shot()

    prompt = prompt_engine.build_seeddance_r2v_prompt_multi(
        [shot],
        _motion_bible(),
        project_config={},
        coverage_pass_dict={
            "focus_character": "JADE",
            "location_id": "corridor",
            "segments": [shot],
        },
        ref_manifest={"identity_1": 1, "scene_1": 2},
        _core_semantics=_minimal_core_semantics(),
    )

    assert prompt == (
        "@Image1 is the protagonist character \u2014 lean salvage pilot. "
        "@Image2 is the location reference. "
        "Shot 1: Static Medium shot. @Image1 in @Image2. "
        "@Image1 braces at the hatch. @Image1 presses the latch. "
        "the subject stays focused. 4s. Cinematic, photorealistic. "
        "4k, hd, rich details, sharp clarity, cinematic texture, natural colors, "
        "stable picture. Diegetic audio only. No music, no score."
    )


def test_seeddance_r2v_multi_warns_once_for_missing_motion_lines(
    monkeypatch, caplog
):
    _patch_motion_builder_deps(monkeypatch)
    caplog.set_level("WARNING", logger=prompt_engine.__name__)
    shots = [
        _motion_shot(motion_line="Jade leans into the latch"),
        _motion_shot(),
        _motion_shot(motion_line=""),
    ]

    prompt_engine.build_seeddance_r2v_prompt_multi(
        shots,
        _motion_bible(),
        project_config={},
        coverage_pass_dict={
            "focus_character": "JADE",
            "location_id": "corridor",
            "segments": shots,
        },
        ref_manifest={"identity_1": 1, "scene_1": 2},
        _core_semantics=_minimal_core_semantics(),
    )

    messages = [
        record.getMessage()
        for record in caplog.records
        if "missing motion_line" in record.getMessage()
    ]
    assert messages == [
        "r2v_multi: 2/3 shots missing motion_line (shot numbers: 2, 3)"
    ]


def test_seeddance_r2v_multi_binds_generic_subject_to_focus_token(monkeypatch):
    monkeypatch.setattr(prompt_engine, "load_cinema_modes", lambda: {"modes": {}})
    monkeypatch.setattr(prompt_engine, "render_camera_line", lambda *args, **kwargs: "")

    bible = {
        "characters": {
            "HERO": {
                "display_name": "Hero",
                "role": "protagonist",
                "visual_description": "focused lead",
            },
            "ALLY": {
                "display_name": "Ally",
                "role": "support",
                "visual_description": "watchful second",
            },
        },
        "locations": {
            "bridge": {
                "display_name": "Bridge",
                "spatial_description": "a narrow command bridge",
            }
        },
    }
    shot = {
        "shot_id": "SH01",
        "routing_data": {
            "target_editorial_duration_s": 3,
            "has_dialogue": True,
        },
        "asset_data": {
            "location_id": "bridge",
            "characters": [{"char_id": "HERO"}, {"char_id": "ALLY"}],
        },
        "prompt_data": {
            "shot_type": "MS",
            "focal_length": "50mm",
            "camera_movement": "static",
            "prompt_skeleton": {
                "subject_line": "the subject squares up beside the other character",
                "action_line": "the subject steps closer and speaks to the subject",
                "emotion_line": "",
            },
        },
        "audio_data": {"dialogue": [{"text": "Stay with me."}]},
    }

    prompt = prompt_engine.build_seeddance_r2v_prompt_multi(
        [shot],
        bible,
        project_config={},
        coverage_pass_dict={
            "focus_character": "HERO",
            "location_id": "bridge",
            "segments": [shot],
        },
        ref_manifest={"identity_1": 1, "identity_2": 2, "scene_1": 3},
        _core_semantics=_minimal_core_semantics(),
    )

    shot_line = re.search(r"Shot 1:.*? 3s\.", prompt).group(0)
    assert "the subject" not in shot_line.lower()
    assert "@Image1 steps closer and speaks to @Image2." in shot_line
    assert '@Image1 speaks: "Stay with me."' in shot_line


def test_seeddance_r2v_multi_binds_cross_shot_character_mentions(monkeypatch):
    monkeypatch.setattr(prompt_engine, "load_cinema_modes", lambda: {"modes": {}})
    monkeypatch.setattr(prompt_engine, "render_camera_line", lambda *args, **kwargs: "")

    bible = {
        "characters": {
            "JADE": {
                "display_name": "Jade",
                "role": "protagonist",
                "visual_description": "lean salvage pilot",
            },
            "WREN": {
                "display_name": "Wren",
                "role": "support",
                "visual_description": "armored tactical partner",
            },
        },
        "locations": {
            "corridor": {
                "display_name": "Corridor",
                "spatial_description": "a lower-deck corridor",
            }
        },
    }
    jade_shot = {
        "shot_id": "SH18",
        "routing_data": {"target_editorial_duration_s": 4},
        "asset_data": {
            "location_id": "corridor",
            "characters": [{"char_id": "JADE"}],
        },
        "prompt_data": {
            "shot_type": "MS",
            "focal_length": "50mm",
            "camera_movement": "static",
            "prompt_skeleton": {
                "subject_line": "",
                "action_line": "Jade holds up two fingers and speaks to Wren",
                "emotion_line": "",
            },
        },
    }
    wren_shot = {
        "shot_id": "SH19",
        "routing_data": {"target_editorial_duration_s": 3},
        "asset_data": {
            "location_id": "corridor",
            "characters": [{"char_id": "WREN"}],
        },
        "prompt_data": {
            "shot_type": "CU",
            "focal_length": "50mm",
            "camera_movement": "static",
            "prompt_skeleton": {
                "subject_line": "",
                "action_line": "Wren watches",
                "emotion_line": "",
            },
        },
    }

    prompt = prompt_engine.build_seeddance_r2v_prompt_multi(
        [jade_shot, wren_shot],
        bible,
        project_config={},
        coverage_pass_dict={
            "focus_character": "JADE",
            "location_id": "corridor",
            "segments": [jade_shot, wren_shot],
        },
        ref_manifest={"identity_1": 1, "identity_2": 2, "scene_1": 3},
        _core_semantics=_minimal_core_semantics(),
    )

    shot_line = re.search(r"Shot 1:.*? 4s\.", prompt).group(0)
    assert "the subject" not in shot_line.lower()
    assert "@Image1 holds up two fingers and speaks to @Image2." in shot_line
