from __future__ import annotations

import json
from pathlib import Path

import pytest

from recoil.execution.step_types import ProjectPaths
from recoil.pipeline.cli import generate
from recoil.pipeline.orchestrator import ingest_pipeline


PROJECT = "fixture"


@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 / "episodes").mkdir(parents=True)
    (project_root / "episodes" / "ep_001.md").write_text(
        "INT. TEST ROOM - NIGHT\n\nA clean test scene plays out.",
        encoding="utf-8",
    )
    monkeypatch.setenv("RECOIL_PROJECTS_ROOT", str(root))
    return project_root


def _shot(batch_index: int, shot_index: int) -> dict:
    ordinal = (batch_index - 1) * 3 + shot_index
    return {
        "shot_id": f"EP001_SH{ordinal:02d}",
        "scene_index": batch_index,
        "pipeline": "video",
        "video_model": "seeddance-2.0",
        "asset_data": {
            "characters": [],
            "location_id": f"LOC_{batch_index}",
        },
        "prompt_data": {"shot_type": "MS"},
        "routing_data": {
            "target_editorial_duration_s": 2.0,
            "is_env_only": False,
            "has_dialogue": False,
        },
        "aspect_ratio": "9:16",
    }


def _plan_payload() -> dict:
    return {
        "episode_id": "ep_001",
        "project": PROJECT,
        "shots": [
            _shot(batch_index, shot_index)
            for batch_index in range(1, 5)
            for shot_index in range(1, 4)
        ],
    }


def _camera_test_payload() -> dict:
    return {
        "episode_id": "EP001",
        "project": PROJECT,
        "total_shots": 28,
        "shots": [
            {
                "shot_index": index,
                "scene_index": ((index - 1) // 7) + 1,
                "source_text": f"Shot {index} source text.",
                "has_dialogue": False,
                "characters_mentioned": [],
                "location_hint": "INT. TEST ROOM - NIGHT",
            }
            for index in range(1, 29)
        ],
    }


def _write_plan(path: Path) -> None:
    path.write_text(json.dumps(_plan_payload(), indent=2), encoding="utf-8")


def _set_rederive_argv(monkeypatch: pytest.MonkeyPatch, *extra: str) -> None:
    monkeypatch.setattr(
        generate.sys,
        "argv",
        [
            "generate.py",
            "rederive",
            "--project",
            PROJECT,
            "--episode",
            "1",
            *extra,
        ],
    )


def _stub_bible(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(generate.IngestPipeline, "_load_bible", lambda self: {"stub": "bible"})


def test_rederive_runs_camera_test_before_plan_pass(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    order: list[tuple[str, int]] = []

    def fake_run_camera_test(self, episode_num: int):  # noqa: ANN001
        order.append(("camera_test", episode_num))
        return {"sentinel": "camera"}

    def fake_run_storyboard_pass(self, episode: int, bible):  # noqa: ANN001
        order.append(("storyboard", episode))
        _write_plan(self.plans_dir / "ep_001_plan.json")
        return {"sentinel": "plan"}

    _stub_bible(monkeypatch)
    monkeypatch.setattr(generate.IngestPipeline, "run_camera_test", fake_run_camera_test)
    monkeypatch.setattr(
        generate.IngestPipeline,
        "run_storyboard_pass",
        fake_run_storyboard_pass,
    )
    _set_rederive_argv(monkeypatch, "--skip-extract", "--dry-run")

    code = generate.main()

    assert code == 0
    assert order == [("camera_test", 1), ("storyboard", 1)]


def test_rederive_skip_camera_test_bypasses_stage_zero(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    order: list[tuple[str, int]] = []

    def fake_run_camera_test(self, episode_num: int):  # noqa: ANN001
        order.append(("camera_test", episode_num))
        raise AssertionError("run_camera_test must not run with --skip-camera-test")

    def fake_run_storyboard_pass(self, episode: int, bible):  # noqa: ANN001
        order.append(("storyboard", episode))
        _write_plan(self.plans_dir / "ep_001_plan.json")
        return {"sentinel": "plan"}

    _stub_bible(monkeypatch)
    monkeypatch.setattr(generate.IngestPipeline, "run_camera_test", fake_run_camera_test)
    monkeypatch.setattr(
        generate.IngestPipeline,
        "run_storyboard_pass",
        fake_run_storyboard_pass,
    )
    _set_rederive_argv(
        monkeypatch,
        "--skip-camera-test",
        "--skip-extract",
        "--dry-run",
    )

    code = generate.main()

    assert code == 0
    assert order == [("storyboard", 1)]


def test_rederive_camera_test_claude_branch_does_not_require_gemini_key(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    assert ingest_pipeline.CAMERA_TEST_MODEL.startswith("claude-")

    monkeypatch.delenv("GEMINI_API_KEY", raising=False)
    monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False)
    oauth_calls: list[dict] = []
    storyboard_calls: list[int] = []

    def fake_call_opus_oauth(
        model: str,
        system_prompt: str,
        user_prompt: str,
        *,
        json_schema: dict,
        timeout: int,
    ) -> str:
        oauth_calls.append(
            {
                "model": model,
                "system_prompt": system_prompt,
                "user_prompt": user_prompt,
                "json_schema": json_schema,
                "timeout": timeout,
            }
        )
        return json.dumps(_camera_test_payload())

    def fake_run_storyboard_pass(self, episode: int, bible):  # noqa: ANN001
        storyboard_calls.append(episode)
        _write_plan(self.plans_dir / "ep_001_plan.json")
        return {"sentinel": "plan"}

    monkeypatch.setattr(ingest_pipeline, "call_opus_oauth", fake_call_opus_oauth)
    _stub_bible(monkeypatch)
    monkeypatch.setattr(
        generate.IngestPipeline,
        "run_storyboard_pass",
        fake_run_storyboard_pass,
    )
    _set_rederive_argv(monkeypatch, "--skip-extract", "--dry-run")

    code = generate.main()

    assert code == 0
    assert storyboard_calls == [1]
    assert len(oauth_calls) == 1
    assert oauth_calls[0]["model"].startswith("claude-")
    assert oauth_calls[0]["timeout"] == 2400
    assert "CameraTestedEpisode object" in oauth_calls[0]["user_prompt"]
    assert (
        ProjectPaths.for_episode(PROJECT, 1).project_root
        / "_pipeline/state/visual/camera_tested/ep_001.json"
    ).exists()
