"""Tests for CLI engine switch and workers enforcement."""

from __future__ import annotations

from types import SimpleNamespace
from unittest.mock import patch, MagicMock

import pytest
from click.testing import CliRunner

from clawbench.cli import cli


@pytest.fixture
def runner():
    return CliRunner()


def _make_mock_result():
    """Return a GEPA-style result object."""
    return SimpleNamespace(
        best_idx=0,
        total_metric_calls=2,
        val_aggregate_scores=[0.7],
    )


@pytest.fixture
def _mock_deps():
    """Mock heavy imports so optimize doesn't need real infra."""
    fake_tasks = [{"task_name": "t1", "task_path": "/fake/t1"}]
    with patch("clawbench.task_utils.discover_tasks", return_value=fake_tasks), \
         patch("clawbench.optimize.run_optimization") as mock_run:
        mock_run.return_value = _make_mock_result()
        yield mock_run


class TestEngineDefault:
    def test_help_shows_engine_option(self, runner):
        result = runner.invoke(cli, ["optimize", "--help"])
        assert result.exit_code == 0
        assert "--engine" in result.output

    def test_default_engine_is_gepa(self, runner):
        """--engine defaults to 'gepa'."""
        result = runner.invoke(cli, ["optimize", "--help"])
        assert "gepa" in result.output


class TestEngineAcceptReject:
    def test_accepts_gepa(self, runner, tmp_path, _mock_deps):
        skill = tmp_path / "SKILL.md"
        skill.write_text("# Skill\n")
        tasks_dir = tmp_path / "tasks"
        tasks_dir.mkdir()

        result = runner.invoke(cli, [
            "optimize",
            "--skill", str(skill),
            "--tasks-dir", str(tasks_dir),
            "--engine", "gepa",
        ])
        assert result.exit_code == 0
        _mock_deps.assert_called_once()
        assert _mock_deps.call_args.kwargs["engine"] == "gepa"

    def test_rejects_unsupported_engine(self, runner, tmp_path):
        skill = tmp_path / "SKILL.md"
        skill.write_text("# Skill\n")
        tasks_dir = tmp_path / "tasks"
        tasks_dir.mkdir()

        result = runner.invoke(cli, [
            "optimize",
            "--skill", str(skill),
            "--tasks-dir", str(tasks_dir),
            "--engine", "bogus",
        ])
        assert result.exit_code != 0
        assert "Invalid value" in result.output or "invalid choice" in result.output.lower()

    def test_rejects_custom_engine(self, runner, tmp_path):
        """Custom engine is no longer supported."""
        skill = tmp_path / "SKILL.md"
        skill.write_text("# Skill\n")
        tasks_dir = tmp_path / "tasks"
        tasks_dir.mkdir()

        result = runner.invoke(cli, [
            "optimize",
            "--skill", str(skill),
            "--tasks-dir", str(tasks_dir),
            "--engine", "custom",
        ])
        assert result.exit_code != 0


class TestWorkersEnforcement:
    def test_workers_1_accepted(self, runner, tmp_path, _mock_deps):
        skill = tmp_path / "SKILL.md"
        skill.write_text("# Skill\n")
        tasks_dir = tmp_path / "tasks"
        tasks_dir.mkdir()

        result = runner.invoke(cli, [
            "optimize",
            "--skill", str(skill),
            "--tasks-dir", str(tasks_dir),
            "--workers", "1",
        ])
        assert result.exit_code == 0

    def test_workers_2_rejected_docker(self, runner, tmp_path):
        skill = tmp_path / "SKILL.md"
        skill.write_text("# Skill\n")
        tasks_dir = tmp_path / "tasks"
        tasks_dir.mkdir()

        result = runner.invoke(cli, [
            "optimize",
            "--skill", str(skill),
            "--tasks-dir", str(tasks_dir),
            "--workers", "2",
        ])
        assert result.exit_code != 0
        assert "cloud environment" in result.output

    def test_help_documents_workers(self, runner):
        result = runner.invoke(cli, ["optimize", "--help"])
        assert "--workers" in result.output


class TestValTestTaskArgs:
    """Verify --val-tasks and --test-tasks CLI options."""

    @pytest.fixture
    def _mock_multi_tasks(self):
        """Mock with 3 discovered tasks."""
        fake_tasks = [
            {"task_name": "task-a", "task_path": "/fake/a"},
            {"task_name": "task-b", "task_path": "/fake/b"},
            {"task_name": "task-c", "task_path": "/fake/c"},
        ]
        with patch("clawbench.task_utils.discover_tasks", return_value=fake_tasks), \
             patch("clawbench.optimize.run_optimization") as mock_run:
            mock_run.return_value = _make_mock_result()
            yield mock_run

    def test_val_tasks_partitions_correctly(self, runner, tmp_path, _mock_multi_tasks):
        skill = tmp_path / "SKILL.md"
        skill.write_text("# Skill\n")
        tasks_dir = tmp_path / "tasks"
        tasks_dir.mkdir()

        result = runner.invoke(cli, [
            "optimize",
            "--skill", str(skill),
            "--tasks-dir", str(tasks_dir),
            "--val-tasks", "task-b",
        ])
        assert result.exit_code == 0
        kw = _mock_multi_tasks.call_args.kwargs
        train_names = [t["task_name"] for t in kw["train_tasks"]]
        val_names = [t["task_name"] for t in kw["val_tasks"]]
        assert "task-b" not in train_names
        assert "task-b" in val_names
        assert len(train_names) == 2

    def test_test_tasks_partitions_correctly(self, runner, tmp_path, _mock_multi_tasks):
        skill = tmp_path / "SKILL.md"
        skill.write_text("# Skill\n")
        tasks_dir = tmp_path / "tasks"
        tasks_dir.mkdir()

        result = runner.invoke(cli, [
            "optimize",
            "--skill", str(skill),
            "--tasks-dir", str(tasks_dir),
            "--test-tasks", "task-c",
        ])
        assert result.exit_code == 0
        kw = _mock_multi_tasks.call_args.kwargs
        train_names = [t["task_name"] for t in kw["train_tasks"]]
        test_names = [t["task_name"] for t in kw["test_tasks"]]
        assert "task-c" not in train_names
        assert "task-c" in test_names

    def test_unknown_val_task_rejected(self, runner, tmp_path, _mock_multi_tasks):
        skill = tmp_path / "SKILL.md"
        skill.write_text("# Skill\n")
        tasks_dir = tmp_path / "tasks"
        tasks_dir.mkdir()

        result = runner.invoke(cli, [
            "optimize",
            "--skill", str(skill),
            "--tasks-dir", str(tasks_dir),
            "--val-tasks", "nonexistent",
        ])
        assert result.exit_code != 0
        assert "unknown" in result.output.lower() or "nonexistent" in result.output

    def test_val_test_overlap_rejected(self, runner, tmp_path, _mock_multi_tasks):
        skill = tmp_path / "SKILL.md"
        skill.write_text("# Skill\n")
        tasks_dir = tmp_path / "tasks"
        tasks_dir.mkdir()

        result = runner.invoke(cli, [
            "optimize",
            "--skill", str(skill),
            "--tasks-dir", str(tasks_dir),
            "--val-tasks", "task-b",
            "--test-tasks", "task-b",
        ])
        assert result.exit_code != 0
        assert "both" in result.output.lower()

    def test_no_train_remaining_rejected(self, runner, tmp_path, _mock_multi_tasks):
        skill = tmp_path / "SKILL.md"
        skill.write_text("# Skill\n")
        tasks_dir = tmp_path / "tasks"
        tasks_dir.mkdir()

        result = runner.invoke(cli, [
            "optimize",
            "--skill", str(skill),
            "--tasks-dir", str(tasks_dir),
            "--val-tasks", "task-a,task-b,task-c",
        ])
        assert result.exit_code != 0
        assert "no train" in result.output.lower()

    def test_help_shows_val_test_options(self, runner):
        result = runner.invoke(cli, ["optimize", "--help"])
        assert "--val-tasks" in result.output
        assert "--test-tasks" in result.output
