from __future__ import annotations

import json
import sys
from pathlib import Path

import pytest

from recoil.core.paths import ProjectPaths
from recoil.pipeline._lib.board_builder import compute_source_sha256
from recoil.pipeline._lib.derivation_sha import shotset_hash
from recoil.pipeline.cli import generate
from recoil.pipeline.core.persistence import (
    load_manifest,
    load_scene,
    load_scene_active,
    save_scene,
    scene_path,
)
from recoil.pipeline.core.receipts import utc_now_iso8601
from recoil.pipeline.core.scene_version_store import SceneVersionStore
from recoil.pipeline.core.take import Beat, Scene


PROJECT = "fixture"
BATCH_ID = "BATCH_004"
BOARD_SELECTOR = "EP001_CONT_004"


@pytest.fixture(autouse=True)
def _projects_root(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
    root = tmp_path / "projects"
    root.mkdir()
    (root / ".recoil-data-root").touch()
    project_root = root / PROJECT
    project_root.mkdir()
    monkeypatch.setenv("RECOIL_PROJECTS_ROOT", str(root))
    monkeypatch.setenv("RECOIL_LOCKED_BY", "JT")
    return project_root


def _shot(intent: str = "Beat 10 action.") -> dict:
    return {
        "shot_id": "EP001_SH10",
        "scene_index": 1,
        "duration_s": 1.0,
        "intent": intent,
        "asset_data": {"characters": [], "location_id": None},
        "spatial_data": {},
    }


def _source_sha(intent: str = "Beat 10 action.") -> str:
    return compute_source_sha256(
        [
            {
                "shot_id": "EP001_SH10",
                "start_s": 0.0,
                "end_s": 1.0,
                "duration_s": 1.0,
                "intent": intent,
                "sublocation": None,
            }
        ]
    )


def _save_scene(*, locked: bool = False) -> Path:
    beat = Beat(
        beat_id=BATCH_ID,
        beat_metadata={"scene_id": BATCH_ID, "modality": "r2v_multi"},
    )
    scene = Scene(
        scene_id=BATCH_ID,
        beats=[beat],
        locked=locked,
        lock_reason="staged" if locked else None,
        locked_by="JT" if locked else None,
        locked_at="2026-06-13T00:00:00Z" if locked else None,
    )
    path = scene_path(PROJECT, "ep_001", BATCH_ID)
    save_scene(scene, path)
    return path


def _save_board_scene() -> Path:
    paths = ProjectPaths.for_project(PROJECT)
    artifact_rel = "prep/ep_001/storyboards/EP001_CONT_004_v01.png"
    artifact_path = paths.project_root / artifact_rel
    artifact_path.parent.mkdir(parents=True, exist_ok=True)
    artifact_path.write_bytes(b"\x89PNG\r\n" + (b"0" * 2048))
    shot = _shot()
    beat = Beat(
        beat_id=BATCH_ID,
        beat_metadata={
            "scene_id": BATCH_ID,
            "modality": "r2v_multi",
            "shot": shot,
            "batch_shots": [shot],
            "grouping": {
                "modality": "r2v_multi",
                "shot_ids": ["EP001_SH10"],
                "shotset_hash": shotset_hash(["EP001_SH10"]),
            },
        },
        board={
            "status": "proposed",
            "artifact": artifact_rel,
            "source_sha256": _source_sha(),
            "approved_by": None,
            "updated_at": utc_now_iso8601(),
        },
    )
    scene = Scene(scene_id=BATCH_ID, beats=[beat])
    path = scene_path(PROJECT, "ep_001", BATCH_ID)
    save_scene(scene, path)
    return path


def test_lock_scene_handler_sets_lock_fields() -> None:
    scene_file = _save_scene()

    code, result = generate._run_scene_lock(
        project=PROJECT,
        episode=1,
        batch=BATCH_ID,
        reason="staged",
    )

    assert code == 0
    assert result["path"] == str(scene_file)
    assert result["lock_reason"] == "staged"
    restored = load_scene(scene_file)
    assert restored.locked is True
    assert restored.lock_reason == "staged"
    assert restored.locked_by == "JT"
    assert restored.locked_at is not None


def test_unlock_scene_handler_clears_lock_fields() -> None:
    scene_file = _save_scene(locked=True)

    code, result = generate._run_scene_lock(
        project=PROJECT,
        episode=1,
        batch=BATCH_ID,
        reason=None,
        unlock=True,
    )

    assert code == 0
    assert result["locked"] is False
    restored = load_scene(scene_file)
    assert restored.locked is False
    assert restored.lock_reason is None
    assert restored.locked_by is None
    assert restored.locked_at is None


def test_approve_board_locks_parent_scene(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    scene_file = _save_board_scene()
    monkeypatch.setattr(
        generate,
        "render_board_finish",
        lambda *a, **k: {
            "success": True,
            "artifact": "prep/ep_001/storyboards/EP001_CONT_004_v01_photoreal.png",
        },
    )
    monkeypatch.setattr(
        generate,
        "_build_step_runner_for_episode",
        lambda *a, **k: "runner",
    )

    code, result = generate._run_board_decision(
        project=PROJECT,
        episode=1,
        batch=BOARD_SELECTOR,
        decision="approve",
    )

    assert code == 0
    assert result["status"] == "approved"
    restored = load_scene(scene_file)
    assert restored.locked is True
    assert restored.lock_reason.startswith("approved:")
    assert restored.locked_by == "JT"
    assert restored.locked_at is not None


def test_lock_scene_real_argv_parse_routes_to_lock_handler(
    monkeypatch: pytest.MonkeyPatch,
    capsys: pytest.CaptureFixture[str],
) -> None:
    routed = {}

    def fake_lock_handler(**kwargs):
        routed.update(kwargs)
        return 0, {
            "success": True,
            "action": "lock",
            "path": "/tmp/ep_001_BATCH_004.json",
            "lock_reason": kwargs["reason"],
        }

    monkeypatch.setattr(generate, "_run_scene_lock", fake_lock_handler)
    monkeypatch.setattr(
        sys,
        "argv",
        [
            "generate.py",
            "--project",
            PROJECT,
            "--episode",
            "1",
            "--batch",
            BATCH_ID,
            "--lock-scene",
            "staged",
        ],
    )

    code = generate.main()
    captured = capsys.readouterr()
    payload = json.loads(captured.out.strip())

    assert code == 0
    assert routed == {
        "project": PROJECT,
        "episode": 1,
        "batch": BATCH_ID,
        "reason": "staged",
        "unlock": False,
    }
    assert payload["lock_reason"] == "staged"


def test_no_force_overwrite_flag(
    capsys: pytest.CaptureFixture[str],
) -> None:
    """REC-231 Phase 7: ``--force-scene-overwrite`` is DELETED — the rederive CLI
    rejects it as an unknown argument (the force/clobber overwrite path no longer
    exists; no silent overwrite path remains)."""
    with pytest.raises(SystemExit) as excinfo:
        generate._run_rederive_cli(
            [
                "--project",
                PROJECT,
                "--episode",
                "1",
                "--from-script",
                "--batch",
                "EP001_CONT_004",
                "--force-scene-overwrite",
            ]
        )

    assert excinfo.value.code == 2  # argparse unknown-argument error
    assert "force-scene-overwrite" in capsys.readouterr().err


def test_locked_preserved_via_append() -> None:
    """REC-231 Phase 7: re-deriving a LOCKED scene APPENDS a candidate version and
    leaves the active (locked) body byte-untouched — ``scene.locked`` no longer gates
    the write (no clobber gate, no force). ``load_scene_active`` still returns the
    preserved locked body because the pointer never moves on an append."""
    flat_path = _save_scene(locked=True)
    original_bytes = flat_path.read_bytes()
    original = load_scene(flat_path)
    assert original.locked is True

    candidate = Scene(
        scene_id=BATCH_ID,
        beats=[
            Beat(
                beat_id="NEW_BEAT_001",
                beat_metadata={"scene_id": BATCH_ID, "modality": "r2v_multi"},
            )
        ],
    )
    version = SceneVersionStore(PROJECT, "ep_001").write_scene_candidate(
        BATCH_ID, candidate
    )

    assert version == 2
    # The active (locked v1) body is byte-untouched by the append.
    assert flat_path.read_bytes() == original_bytes
    # Pointer unmoved → load_scene_active returns the preserved locked body.
    active = load_scene_active(PROJECT, "ep_001", BATCH_ID)
    assert active.to_dict() == original.to_dict()
    assert active.locked is True
    # Manifest: v1 = materialized legacy_flat (active/approved), v2 = candidate.
    manifest = load_manifest(PROJECT, "ep_001", BATCH_ID)
    assert manifest["active_version"] == 1
    by_version = {v["version"]: v for v in manifest["versions"]}
    assert by_version[1]["source"] == "legacy_flat"
    assert by_version[1]["state"] == "approved"
    assert by_version[2]["state"] == "candidate"
