import json
import os
from pathlib import Path

from recoil.execution.step_types import ProjectPaths
from recoil.pipeline.cli import generate
from recoil.pipeline.core.persistence import SceneVersionConflictError


def _write_plan(project: str = "fixture", episode: int = 1) -> None:
    root = Path(os.environ["RECOIL_PROJECTS_ROOT"])
    (root / project).mkdir(parents=True, exist_ok=True)
    paths = ProjectPaths.for_episode(project, episode)
    paths.project_root.mkdir(parents=True, exist_ok=True)
    paths.plans_dir.mkdir(parents=True, exist_ok=True)
    (paths.plans_dir / f"ep_{episode:03d}_plan.json").write_text(
        json.dumps(
            {
                "episode_id": f"ep_{episode:03d}",
                "project": project,
                "shots": [
                    {
                        "shot_id": "EP001_SH01",
                        "scene_index": 1,
                        "pipeline": "video",
                        "video_model": "seeddance-2.0",
                        "asset_data": {"location_id": "L1", "characters": []},
                        "prompt_data": {"shot_type": "MS"},
                        "routing_data": {"target_editorial_duration_s": 2},
                    },
                    {
                        "shot_id": "EP001_SH02",
                        "scene_index": 2,
                        "pipeline": "video",
                        "video_model": "seeddance-2.0",
                        "asset_data": {"location_id": "L1", "characters": []},
                        "prompt_data": {"shot_type": "MS"},
                        "routing_data": {"target_editorial_duration_s": 2},
                    },
                    {
                        "shot_id": "EP001_SH03",
                        "scene_index": 3,
                        "pipeline": "video",
                        "video_model": "seeddance-2.0",
                        "asset_data": {"location_id": "L1", "characters": []},
                        "prompt_data": {"shot_type": "MS"},
                        "routing_data": {"target_editorial_duration_s": 2},
                    },
                ],
            }
        ),
        encoding="utf-8",
    )


def _coverage_pass_dict(pass_id: str = "PASS_011", shot_ids=None) -> dict:
    shot_ids = shot_ids or ["EP001_SH01", "EP001_SH03"]
    return {
        "pass_id": pass_id,
        "episode_id": "ep_001",
        "shot_range": [shot_ids[0], shot_ids[-1]],
        "camera_side": "A",
        "label": "fixture",
        "focus_character": "",
        "pass_type": "env",
        "location_id": "L1",
        "generation_config": {"mode": "t2v"},
        "segments": [
            {
                "segment_index": index,
                "source_shot_id": shot_id,
                "shot_type": "MS",
                "duration_s": 2,
                "prompt": f"shot {shot_id}",
            }
            for index, shot_id in enumerate(shot_ids)
        ],
    }


def test_cli_scope_grouping_continuity_all_does_not_load_coverage_passes(
    tmp_path,
    monkeypatch,
):
    root = tmp_path / "projects"
    root.mkdir()
    (root / ".recoil-data-root").touch()
    monkeypatch.setenv("RECOIL_PROJECTS_ROOT", str(root))
    _write_plan()

    def _fail_load(*args, **kwargs):
        raise AssertionError("coverage passes should not load for continuity --all")

    monkeypatch.setattr(generate, "_load_coverage_pass_dicts", _fail_load)

    result = generate.run_generation(
        project="fixture",
        episode=1,
        pass_ids=None,
        grouping="continuity",
        dry_run=True,
    )

    assert result["success"] is True
    assert result["grouping"] == "continuity"


def test_cli_scope_grouping_coverage_all_loads_and_validates_passes(
    tmp_path,
    monkeypatch,
):
    root = tmp_path / "projects"
    root.mkdir()
    (root / ".recoil-data-root").touch()
    monkeypatch.setenv("RECOIL_PROJECTS_ROOT", str(root))
    (root / "fixture").mkdir(parents=True, exist_ok=True)
    loaded = []
    validated = []

    def _load(paths, episode):
        loaded.append((paths.project, episode))
        return [_coverage_pass_dict()]

    def _validate(passes):
        validated.append([p.pass_id for p in passes])
        return []

    monkeypatch.setattr(generate, "_load_coverage_pass_dicts", _load)
    monkeypatch.setattr(generate, "validate_all_passes", _validate)

    result = generate.run_generation(
        project="fixture",
        episode=1,
        pass_ids=None,
        grouping="coverage",
        dry_run=True,
    )

    assert result["success"] is True
    assert result["grouping"] == "coverage"
    assert loaded == [("fixture", 1)]
    assert validated == [["PASS_011"]]


def test_cli_scope_selected_pass_noncoverage_reassembles_subset_without_pass_grouping(
    tmp_path,
    monkeypatch,
):
    root = tmp_path / "projects"
    root.mkdir()
    (root / ".recoil-data-root").touch()
    monkeypatch.setenv("RECOIL_PROJECTS_ROOT", str(root))
    _write_plan()
    captured = {}

    monkeypatch.setattr(
        generate,
        "_load_coverage_pass_dicts",
        lambda paths, episode: [_coverage_pass_dict()],
    )
    monkeypatch.setattr(generate, "validate_all_passes", lambda passes: [])
    monkeypatch.setattr(generate, "acquire_episode_lock", lambda project_root, episode: Path("/tmp/lock"))
    monkeypatch.setattr(generate, "release_episode_lock", lambda lock_path: None)
    monkeypatch.setattr(generate, "ExecutionStore", lambda *args, **kwargs: object())
    monkeypatch.setattr(generate, "StepRunner", lambda *args, **kwargs: object())

    class FakeRunner:
        def __init__(self, **kwargs):
            pass

        async def run_episode_batches(self, canonical_plan, **kwargs):
            captured["shot_ids"] = [shot.shot_id for shot in canonical_plan.shots]
            captured["grouping"] = kwargs["grouping"]
            captured["selected_coverage_passes"] = kwargs["selected_coverage_passes"]
            return []

    monkeypatch.setattr(generate, "EpisodeRunner", FakeRunner)

    result = generate.run_generation(
        project="fixture",
        episode=1,
        pass_ids=["PASS_011"],
        grouping="solo",
        dry_run=False,
    )

    assert result["success"] is True
    assert result["grouping"] == "solo"
    assert captured == {
        "shot_ids": ["EP001_SH01", "EP001_SH03"],
        "grouping": "solo",
        "selected_coverage_passes": [],
    }


def test_cli_scene_version_conflict_returns_structured_payload(tmp_path, monkeypatch):
    root = tmp_path / "projects"
    root.mkdir()
    (root / ".recoil-data-root").touch()
    monkeypatch.setenv("RECOIL_PROJECTS_ROOT", str(root))
    _write_plan()
    monkeypatch.setattr(generate, "acquire_episode_lock", lambda project_root, episode: Path("/tmp/lock"))
    monkeypatch.setattr(generate, "release_episode_lock", lambda lock_path: None)
    monkeypatch.setattr(generate, "ExecutionStore", lambda *args, **kwargs: object())
    monkeypatch.setattr(generate, "StepRunner", lambda *args, **kwargs: object())
    monkeypatch.setattr(generate, "LearningEngine", lambda project: object())
    monkeypatch.setattr(generate, "StrategyEngine", lambda **kwargs: object())

    class FakeRunner:
        def __init__(self, **kwargs):
            pass

        async def run_episode_batches(self, canonical_plan, **kwargs):
            raise SceneVersionConflictError("BATCH_004", 2, 3)

    monkeypatch.setattr(generate, "EpisodeRunner", FakeRunner)

    result = generate.run_generation(
        project="fixture",
        episode=1,
        grouping="continuity",
        dry_run=False,
    )

    assert result["success"] is False
    assert result["error"] == "scene_version_conflict"
    assert result["batch_id"] == "BATCH_004"
    assert result["expected_version"] == 2
    assert result["current_version"] == 3


def test_cli_scope_validate_rejects_explicit_noncoverage_grouping(tmp_path, monkeypatch):
    root = tmp_path / "projects"
    root.mkdir()
    (root / ".recoil-data-root").touch()
    monkeypatch.setenv("RECOIL_PROJECTS_ROOT", str(root))

    result = generate.run_generation(
        project="fixture",
        episode=1,
        grouping="solo",
        validate_only=True,
    )

    assert result["success"] is False
    assert result["error"] == "validate_requires_coverage_grouping"


def test_cli_scope_reroll_rejects_resolved_noncoverage_auto_all(tmp_path, monkeypatch):
    root = tmp_path / "projects"
    root.mkdir()
    (root / ".recoil-data-root").touch()
    monkeypatch.setenv("RECOIL_PROJECTS_ROOT", str(root))

    result = generate.run_generation(
        project="fixture",
        episode=1,
        pass_ids=None,
        grouping="auto",
        retry=True,
    )

    assert result["success"] is False
    assert result["error"] == "reroll_non_coverage_deferred"
    assert result["grouping"] == "continuity"


def test_cli_scope_new_take_rejects_explicit_noncoverage_selected_pass(
    tmp_path,
    monkeypatch,
):
    root = tmp_path / "projects"
    root.mkdir()
    (root / ".recoil-data-root").touch()
    monkeypatch.setenv("RECOIL_PROJECTS_ROOT", str(root))

    result = generate.run_generation(
        project="fixture",
        episode=1,
        pass_ids=["PASS_011"],
        grouping="continuity",
        force_new_take=True,
    )

    assert result["success"] is False
    assert result["error"] == "reroll_non_coverage_deferred"
    assert result["grouping"] == "continuity"
