from __future__ import annotations

from pathlib import Path

import pytest

from recoil.pipeline.cli import generate


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.mkdir()
    monkeypatch.setenv("RECOIL_PROJECTS_ROOT", str(root))
    return project_root


def _rederive_args(*extra: str) -> list[str]:
    return [
        "--project",
        PROJECT,
        "--episode",
        "1",
        *extra,
    ]


def test_rederive_board_only_requires_batch(
    capsys: pytest.CaptureFixture[str],
) -> None:
    with pytest.raises(SystemExit) as exc:
        generate._run_rederive_cli(_rederive_args("--board-only"))

    captured = capsys.readouterr()
    assert exc.value.code == 2
    assert "--board-only requires --batch" in captured.err


def test_rederive_board_only_rejects_from_script(
    capsys: pytest.CaptureFixture[str],
) -> None:
    with pytest.raises(SystemExit) as exc:
        generate._run_rederive_cli(
            _rederive_args(
                "--batch",
                "EP001_CONT_001",
                "--board-only",
                "--from-script",
            )
        )

    captured = capsys.readouterr()
    assert exc.value.code == 2
    assert (
        "--board-only and --from-script are separate operations — run them "
        "as two commands"
    ) in captured.err


def test_rederive_board_only_routes_to_storyboard_build(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    calls: list[dict] = []

    def fake_storyboard_build(**kwargs):
        calls.append(kwargs)
        return {"success": True, "status": "built"}

    def fail_camera_test(self, episode_num: int):  # noqa: ANN001
        raise AssertionError("run_camera_test must not run for --board-only")

    def fail_storyboard_pass(self, episode: int, bible):  # noqa: ANN001
        raise AssertionError("run_storyboard_pass must not run for --board-only")

    def fail_build_and_dispatch_board(*args, **kwargs):  # noqa: ANN001
        raise AssertionError("build_and_dispatch_board must be mocked")

    def fail_step_runner(*args, **kwargs):  # noqa: ANN001
        raise AssertionError("step runner must not be built in this test")

    monkeypatch.setattr(generate, "_run_storyboard_build", fake_storyboard_build)
    monkeypatch.setattr(generate.IngestPipeline, "run_camera_test", fail_camera_test)
    monkeypatch.setattr(
        generate.IngestPipeline,
        "run_storyboard_pass",
        fail_storyboard_pass,
    )
    monkeypatch.setattr(
        generate,
        "build_and_dispatch_board",
        fail_build_and_dispatch_board,
    )
    monkeypatch.setattr(generate, "_build_step_runner_for_episode", fail_step_runner)

    code = generate._run_rederive_cli(
        _rederive_args("--batch", "EP001_CONT_001", "--board-only")
    )

    assert code == generate.EXIT_OK
    assert calls == [
        {
            "project": PROJECT,
            "episode": 1,
            "batch": "EP001_CONT_001",
            "dry_run": False,
        }
    ]


def test_rederive_board_only_dry_run_passes_through(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    calls: list[dict] = []

    def fake_storyboard_build(**kwargs):
        calls.append(kwargs)
        return {
            "success": True,
            "dry_run": True,
            "note": "dry-run: zero image dispatch",
            "estimated_cost_usd": 0.41,
        }

    monkeypatch.setattr(generate, "_run_storyboard_build", fake_storyboard_build)

    code = generate._run_rederive_cli(
        _rederive_args("--batch", "EP001_CONT_001", "--board-only", "--dry-run")
    )

    assert code == generate.EXIT_OK
    assert calls == [
        {
            "project": PROJECT,
            "episode": 1,
            "batch": "EP001_CONT_001",
            "dry_run": True,
        }
    ]


@pytest.mark.parametrize(
    ("result", "expected_code"),
    [
        ({"success": True, "status": "built"}, generate.EXIT_OK),
        ({"success": False, "error": "stopped"}, generate.EXIT_PARTIAL),
    ],
)
def test_board_only_rederive_translates_storyboard_result_exit_code(
    result: dict,
    expected_code: int,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    def fake_storyboard_build(**kwargs):
        return result

    monkeypatch.setattr(generate, "_run_storyboard_build", fake_storyboard_build)

    code = generate._run_board_only_rederive(
        PROJECT,
        1,
        "EP001_CONT_001",
        dry_run=False,
    )

    assert code == expected_code


def test_rederive_board_only_rejects_malformed_batch_selector(
    capsys: pytest.CaptureFixture[str],
) -> None:
    with pytest.raises(SystemExit) as exc:
        generate._run_rederive_cli(
            _rederive_args("--batch", "BATCH_001", "--board-only")
        )

    captured = capsys.readouterr()
    assert exc.value.code == 2
    assert "invalid --batch selector 'BATCH_001'" in captured.err


def test_rederive_board_only_rejects_selector_episode_mismatch(
    capsys: pytest.CaptureFixture[str],
) -> None:
    with pytest.raises(SystemExit) as exc:
        generate._run_rederive_cli(
            _rederive_args("--batch", "EP002_CONT_001", "--board-only")
        )

    captured = capsys.readouterr()
    assert exc.value.code == 2
    assert "--batch episode EP002 != --episode 1" in captured.err


@pytest.mark.parametrize(
    "flag_args",
    [
        ("--skip-camera-test",),
        ("--skip-plan",),
        ("--skip-extract",),
        ("--reason", "retry note"),
        ("--strategy", "shot_spec"),
        ("--grouping", "continuity"),
    ],
)
def test_rederive_board_only_rejects_scene_rederive_flags(
    flag_args: tuple[str, ...],
    capsys: pytest.CaptureFixture[str],
) -> None:
    with pytest.raises(SystemExit) as exc:
        generate._run_rederive_cli(
            _rederive_args(
                "--batch",
                "EP001_CONT_001",
                "--board-only",
                *flag_args,
            )
        )

    captured = capsys.readouterr()
    assert exc.value.code == 2
    assert (
        "--board-only re-renders the board only; it takes no "
        "scene-rederive flags"
    ) in captured.err


def test_rederive_board_only_dry_run_prints_target_and_summary(
    monkeypatch: pytest.MonkeyPatch,
    capsys: pytest.CaptureFixture[str],
) -> None:
    def fake_storyboard_build(**kwargs):
        return {
            "success": True,
            "dry_run": True,
            "note": "dry-run: zero image dispatch",
            "estimated_cost_usd": 0.41,
        }

    monkeypatch.setattr(generate, "_run_storyboard_build", fake_storyboard_build)

    code = generate._run_rederive_cli(
        _rederive_args("--batch", "EP001_CONT_001", "--board-only", "--dry-run")
    )

    captured = capsys.readouterr()
    assert code == generate.EXIT_OK
    assert "EP001_CONT_001" in captured.out
    assert "dry-run" in captured.out


def test_flat_storyboard_help_mentions_board_only_canonical_surface(
    monkeypatch: pytest.MonkeyPatch,
    capsys: pytest.CaptureFixture[str],
) -> None:
    monkeypatch.setattr(generate.sys, "argv", ["generate.py", "--help"])

    with pytest.raises(SystemExit) as exc:
        generate.main()

    captured = capsys.readouterr()
    normalized = " ".join(captured.out.split())
    assert exc.value.code == 0
    assert "--storyboard" in captured.out
    assert "canonical surface is `rederive --batch X --board-only`" in normalized
