from __future__ import annotations

import json
from pathlib import Path

import pytest

from recoil.core.paths import ProjectPaths
from recoil.core.ref_types import RefAsset, ReferenceBundle
from recoil.pipeline._lib import board_builder as bb
from recoil.pipeline._lib.prompt_engine import STORYBOARD_FINISH_NO_TEXT_FOOTER
from recoil.pipeline.core.persistence import save_scene, scene_path
from recoil.pipeline.core.receipts import GenerationReceipt
from recoil.pipeline.core.registry import MODALITY_STORYBOARD, RunResult
from recoil.pipeline.core.take import Beat, Scene


PROJECT = "fixture_project"
BATCH_ID = "EP001_CONT_004"
SCENE_ID = "BATCH_004"


@pytest.fixture()
def project_paths(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> ProjectPaths:
    paths = ProjectPaths(project_root=tmp_path / PROJECT)
    paths.project_root.mkdir(parents=True)
    monkeypatch.setattr(
        ProjectPaths,
        "for_project",
        classmethod(lambda cls, project=None: paths),
    )
    return paths


def _settings_passthrough(segments, **_kwargs):
    return [
        dict(segment, setting=f"Setting {i}")
        for i, segment in enumerate(segments, start=1)
    ]


def _receipt(success: bool, *, error: str | None = None) -> GenerationReceipt:
    return GenerationReceipt(
        receipt_id="rcpt_carrier",
        modality=MODALITY_STORYBOARD,
        caller_id="board_builder",
        project=PROJECT,
        episode=1,
        shot_id=BATCH_ID,
        timestamp_utc="2026-06-21T00:00:00Z",
        run_result=RunResult(
            id="run_carrier",
            modality=MODALITY_STORYBOARD,
            output_path="/tmp/board.png" if success else None,
            metadata={},
            success=success,
            error=error,
        ),
    )


def _write_bible(paths: ProjectPaths) -> None:
    paths.global_bible_path.parent.mkdir(parents=True, exist_ok=True)
    paths.global_bible_path.write_text(
        json.dumps(
            {
                "project": PROJECT,
                "total_episodes": 1,
                "characters": {"JADE": {"visual_description": "Lean salvager."}},
                "props": {
                    "debt_counter": {
                        "attached_to": "JADE",
                        "is_permanent_attachment": True,
                        "description": "amber readout worn on the left wrist",
                    },
                    "loose_meter": {
                        "description": "portable handheld meter",
                    },
                },
            }
        ),
        encoding="utf-8",
    )


def _raw_wrapped_shot(
    *,
    props: list[dict] | None = None,
    characters: list[dict] | None = None,
    intent: str = "Jade notices an amber readout pulsing near the hatch.",
) -> dict:
    return {
        "raw": {
            "shot_id": "EP001_SH10",
            "scene_index": 1,
            "duration_s": 1.0,
            "intent": intent,
            "asset_data": {
                "props": props or [],
                "characters": characters or [],
                "location_id": None,
            },
            "spatial_data": {},
        }
    }


def _write_batch_scene(
    shots: list[dict],
    *,
    board: dict | None = None,
    shared_characters: list[str] | None = None,
) -> Path:
    beat = Beat(
        beat_id=SCENE_ID,
        beat_metadata={
            "scene_id": SCENE_ID,
            "modality": "r2v_multi",
            "shot": shots[0],
            "batch_shots": shots,
            "batch_summary": {
                "shared_characters": shared_characters or [],
                "shared_location_id": None,
            },
        },
        board=board,
    )
    scene = Scene(
        scene_id=SCENE_ID,
        beats=[beat],
        scene_metadata={"episode": "ep_001", "project": PROJECT},
    )
    path = scene_path(PROJECT, "ep_001", SCENE_ID)
    save_scene(scene, path)
    return path


def _assert_full_invariant(prompt: str) -> None:
    lower = prompt.lower()
    assert "debt_counter" in lower
    assert "jade" in lower
    assert "permanent attachment" in lower
    assert "free-floating" in lower


def _assert_between(prompt: str, upper: str) -> None:
    sb = prompt.find("STORY BEATS:")
    pi = prompt.find("PROP INVARIANT")
    hi = prompt.find(upper)
    assert sb != -1 and pi != -1 and hi != -1 and sb < pi < hi


def _write_jade_refs(paths: ProjectPaths, monkeypatch: pytest.MonkeyPatch) -> None:
    front = paths.project_root / "assets" / "char" / "jade" / "base" / "jade_front.png"
    profile = paths.project_root / "assets" / "char" / "jade" / "base" / "jade_profile.png"
    front.parent.mkdir(parents=True, exist_ok=True)
    front.write_bytes(b"front")
    profile.write_bytes(b"profile")

    def fake_bundle(_paths, char_id, phase=None):
        return ReferenceBundle(
            (
                RefAsset(
                    path=front,
                    role="identity",
                    subject=char_id,
                    kind="turn",
                    view="front",
                ),
                RefAsset(
                    path=profile,
                    role="identity",
                    subject=char_id,
                    kind="turn",
                    view="profile",
                ),
            )
        )

    monkeypatch.setattr(bb, "resolve_character_bundle", fake_bundle)


def test_pencil_live_path_injects_structured_raw_wrapped_carrier(
    project_paths: ProjectPaths,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    _write_bible(project_paths)
    _write_batch_scene(
        [
            _raw_wrapped_shot(
                props=[{"prop_id": "debt_counter"}, {"prop_id": "loose_meter"}],
                characters=[],
            )
        ],
    )
    monkeypatch.setattr(bb, "derive_settings", _settings_passthrough)

    result = bb.build_and_dispatch_board(
        PROJECT,
        1,
        BATCH_ID,
        step_runner=object(),
        dry_run=True,
        fix_notes=["placement boundary"],
    )

    prompt = result["prompt"]
    _assert_full_invariant(prompt)
    _assert_between(prompt, "FIX NOTES")
    assert "PROP INVARIANT: the loose_meter" not in prompt


def test_pencil_live_path_injects_when_carrier_only_shared_character(
    project_paths: ProjectPaths,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    _write_bible(project_paths)
    _write_batch_scene(
        [_raw_wrapped_shot(props=[], characters=[])],
        shared_characters=["JADE"],
    )
    _write_jade_refs(project_paths, monkeypatch)
    monkeypatch.setattr(bb, "derive_settings", _settings_passthrough)

    result = bb.build_and_dispatch_board(
        PROJECT,
        1,
        BATCH_ID,
        step_runner=object(),
        dry_run=True,
    )

    _assert_full_invariant(result["prompt"])


def test_pencil_refusal_fallback_rebuild_keeps_carrier_facts(
    project_paths: ProjectPaths,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    _write_bible(project_paths)
    _write_batch_scene(
        [_raw_wrapped_shot(props=[{"prop_id": "debt_counter"}], characters=[])],
    )
    monkeypatch.setattr(bb, "derive_settings", _settings_passthrough)
    prompts: list[str] = []

    def fake_dispatch(modality, payload, *, context):
        prompts.append(payload["prompt"])
        if len(prompts) == 1:
            return _receipt(False, error="image content policy refusal")
        return _receipt(True)

    monkeypatch.setattr(bb, "dispatch", fake_dispatch)

    result = bb.build_and_dispatch_board(
        PROJECT,
        1,
        BATCH_ID,
        step_runner=object(),
        board_model="gpt-image-2",
        fix_notes=["placement boundary"],
    )

    assert result["success"] is True
    assert len(prompts) == 2
    _assert_full_invariant(prompts[1])
    _assert_between(prompts[1], "FIX NOTES")


def _write_approved_finish_scene(paths: ProjectPaths) -> None:
    shot = _raw_wrapped_shot(
        props=[],
        characters=[{"char_id": "JADE"}],
        intent="Jade checks the amber readout on her wrist.",
    )
    segments = [
        {
            "shot_id": "EP001_SH10",
            "start_s": 0.0,
            "end_s": 1.0,
            "duration_s": 1.0,
            "intent": "Jade checks the amber readout on her wrist.",
            "sublocation": None,
        }
    ]
    source_sha256 = bb.compute_source_sha256(segments, version=2)
    artifact_rel = "prep/ep_001/storyboards/EP001_CONT_004_v03.png"
    artifact = paths.project_root / artifact_rel
    artifact.parent.mkdir(parents=True, exist_ok=True)
    artifact.write_bytes(b"\x89PNG\r\n")
    Path(f"{artifact}.json").write_text(
        json.dumps(
            {
                "kind": "storyboard",
                "batch_id": BATCH_ID,
                "version": 3,
                "source_sha256": source_sha256,
                "fingerprint_version": 2,
                "identity_refs": [],
                "sublocation_refs": [],
                "prop_refs": [],
            }
        ),
        encoding="utf-8",
    )
    board = {
        "status": "approved",
        "artifact": artifact_rel,
        "source_sha256": source_sha256,
        "fingerprint_version": 2,
        "approved_by": "director",
        "updated_at": "2026-06-21T00:00:00Z",
    }
    _write_batch_scene([shot], board=board)


def test_finish_live_path_injects_raw_wrapped_carrier_presence(
    project_paths: ProjectPaths,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    _write_bible(project_paths)
    _write_approved_finish_scene(project_paths)
    monkeypatch.setattr(bb, "derive_settings", _settings_passthrough)
    prompts: list[str] = []

    def fake_dispatch(modality, payload, *, context):
        prompts.append(payload["prompt"])
        return _receipt(True)

    monkeypatch.setattr(bb, "dispatch", fake_dispatch)

    result = bb.render_board_finish(
        PROJECT, 1, BATCH_ID, step_runner=object(), expected_version=1
    )

    assert result["success"] is True
    assert len(prompts) == 1
    _assert_full_invariant(prompts[0])
    _assert_between(prompts[0], STORYBOARD_FINISH_NO_TEXT_FOOTER)


def test_finish_refusal_fallback_rebuild_keeps_carrier_facts(
    project_paths: ProjectPaths,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    _write_bible(project_paths)
    _write_approved_finish_scene(project_paths)
    monkeypatch.setattr(bb, "derive_settings", _settings_passthrough)
    prompts: list[str] = []

    def fake_dispatch(modality, payload, *, context):
        prompts.append(payload["prompt"])
        if len(prompts) == 1:
            return _receipt(False, error="image content policy refusal")
        return _receipt(True)

    monkeypatch.setattr(bb, "dispatch", fake_dispatch)

    result = bb.render_board_finish(
        PROJECT,
        1,
        BATCH_ID,
        step_runner=object(),
        expected_version=1,
        board_model="gpt-image-2",
    )

    assert result["success"] is True
    assert len(prompts) == 2
    _assert_full_invariant(prompts[1])
    _assert_between(prompts[1], STORYBOARD_FINISH_NO_TEXT_FOOTER)
