"""Tests for orchestrator config."""

from __future__ import annotations

from pathlib import Path
from unittest.mock import patch

import pytest

from cleave.orchestrator.config import OrchestratorConfig, _parse_timeout
from cleave.orchestrator.errors import ClaudeCliNotFoundError, OrchestratorError


class TestParseTimeout:
    def test_integer_passthrough(self) -> None:
        assert _parse_timeout(3600) == 3600

    def test_hours(self) -> None:
        assert _parse_timeout("8h") == 28800

    def test_minutes(self) -> None:
        assert _parse_timeout("30m") == 1800

    def test_seconds_suffix(self) -> None:
        assert _parse_timeout("120s") == 120

    def test_bare_number_string(self) -> None:
        assert _parse_timeout("3600") == 3600

    def test_fractional_hours(self) -> None:
        assert _parse_timeout("0.5h") == 1800


class TestOrchestratorConfig:
    def _make_config(self, tmp_path: Path, **kwargs) -> OrchestratorConfig:
        """Helper to create a config with a valid repo path."""
        (tmp_path / ".git").mkdir()
        defaults = {
            "directive": "Test directive",
            "repo_path": tmp_path,
        }
        defaults.update(kwargs)
        return OrchestratorConfig(**defaults)

    def test_defaults(self, tmp_path: Path) -> None:
        cfg = self._make_config(tmp_path)
        assert cfg.model == "opus"
        assert cfg.planner_model == "sonnet"
        assert cfg.max_budget_usd == 50.0
        assert cfg.child_budget_usd == 15.0
        assert cfg.timeout_seconds == 28800
        assert cfg.max_depth == 3
        assert cfg.max_parallel_children == 4
        assert cfg.dry_run is False

    def test_validate_success(self, tmp_path: Path) -> None:
        cfg = self._make_config(tmp_path)
        cfg.validate()  # Should not raise

    def test_validate_empty_directive(self, tmp_path: Path) -> None:
        cfg = self._make_config(tmp_path, directive="   ")
        with pytest.raises(OrchestratorError, match="empty"):
            cfg.validate()

    def test_validate_no_git_repo(self, tmp_path: Path) -> None:
        cfg = OrchestratorConfig(directive="test", repo_path=tmp_path)
        with pytest.raises(OrchestratorError, match="not a git"):
            cfg.validate()

    def test_validate_bad_budget(self, tmp_path: Path) -> None:
        cfg = self._make_config(tmp_path, max_budget_usd=-1)
        with pytest.raises(OrchestratorError, match="positive"):
            cfg.validate()

    def test_validate_child_exceeds_total(self, tmp_path: Path) -> None:
        cfg = self._make_config(tmp_path, max_budget_usd=10, child_budget_usd=20)
        with pytest.raises(OrchestratorError, match="cannot exceed"):
            cfg.validate()

    def test_validate_bad_depth(self, tmp_path: Path) -> None:
        cfg = self._make_config(tmp_path, max_depth=0)
        with pytest.raises(OrchestratorError, match="between 1 and 10"):
            cfg.validate()

    def test_to_dict_roundtrip(self, tmp_path: Path) -> None:
        cfg = self._make_config(tmp_path, success_criteria=["tests pass"])
        d = cfg.to_dict()
        assert d["directive"] == "Test directive"
        assert d["success_criteria"] == ["tests pass"]
        assert d["repo_path"] == str(tmp_path)

        cfg2 = OrchestratorConfig.from_dict(d)
        assert cfg2.directive == cfg.directive
        assert cfg2.repo_path == cfg.repo_path
        assert cfg2.success_criteria == cfg.success_criteria

    def test_str_repo_path_coerced(self, tmp_path: Path) -> None:
        (tmp_path / ".git").mkdir()
        cfg = OrchestratorConfig(directive="test", repo_path=str(tmp_path))  # type: ignore[arg-type]
        assert isinstance(cfg.repo_path, Path)

    def test_find_claude_cli_found(self, tmp_path: Path) -> None:
        cfg = self._make_config(tmp_path)
        with patch("shutil.which", return_value="/usr/local/bin/claude"):
            assert cfg.find_claude_cli() == "/usr/local/bin/claude"

    def test_find_claude_cli_missing(self, tmp_path: Path) -> None:
        cfg = self._make_config(tmp_path)
        with patch("shutil.which", return_value=None):
            with pytest.raises(ClaudeCliNotFoundError):
                cfg.find_claude_cli()

    def test_from_dict_ignores_unknown_keys(self, tmp_path: Path) -> None:
        """from_dict should silently ignore keys from newer versions."""
        cfg = self._make_config(tmp_path)
        d = cfg.to_dict()
        d["future_field"] = "some_value"
        d["another_new_field"] = 42
        cfg2 = OrchestratorConfig.from_dict(d)
        assert cfg2.directive == cfg.directive
        assert not hasattr(cfg2, "future_field")

    def test_root_directive_defaults_to_directive(self, tmp_path: Path) -> None:
        cfg = self._make_config(tmp_path, directive="Fix the bug")
        assert cfg.root_directive == "Fix the bug"

    def test_root_directive_preserved_explicitly(self, tmp_path: Path) -> None:
        cfg = self._make_config(
            tmp_path,
            directive="child-a: Fix the bug",
            root_directive="Fix the bug",
        )
        assert cfg.root_directive == "Fix the bug"
        assert cfg.directive == "child-a: Fix the bug"

    def test_root_directive_roundtrip(self, tmp_path: Path) -> None:
        cfg = self._make_config(
            tmp_path,
            directive="child-a: Fix the bug",
            root_directive="Fix the bug",
        )
        d = cfg.to_dict()
        assert d["root_directive"] == "Fix the bug"
        cfg2 = OrchestratorConfig.from_dict(d)
        assert cfg2.root_directive == "Fix the bug"
        assert cfg2.directive == "child-a: Fix the bug"
