"""Tests for settings validation."""

from __future__ import annotations

from pathlib import Path

import pytest

from cleave.core.settings import (
    VALID_BACKENDS,
    VALID_REPORT_NAMING,
    BackendSettings,
    CleaveSettings,
    SettingsValidationError,
    _validate_settings,
    load_settings,
    save_settings,
    set_setting,
)


class TestSettingsValidation:
    """Tests for _validate_settings()."""

    def test_valid_settings(self) -> None:
        """Should not raise for valid settings."""
        settings = CleaveSettings(
            cloven_report_naming="descriptor",
            backend="claude",
        )
        # Should not raise
        _validate_settings(settings)

    def test_invalid_report_naming(self) -> None:
        """Should raise for invalid cloven_report_naming."""
        settings = CleaveSettings(
            cloven_report_naming="invalid-naming",
            backend="claude",
        )
        with pytest.raises(SettingsValidationError) as exc_info:
            _validate_settings(settings)
        assert "cloven_report_naming" in str(exc_info.value)
        assert "invalid-naming" in str(exc_info.value)
        # Should suggest valid values
        for valid in VALID_REPORT_NAMING:
            assert valid in str(exc_info.value)

    def test_invalid_backend(self) -> None:
        """Should raise for invalid backend."""
        settings = CleaveSettings(
            cloven_report_naming="descriptor",
            backend="invalid-backend",
        )
        with pytest.raises(SettingsValidationError) as exc_info:
            _validate_settings(settings)
        assert "backend" in str(exc_info.value)
        assert "invalid-backend" in str(exc_info.value)
        # Should suggest valid values
        for valid in VALID_BACKENDS:
            assert valid in str(exc_info.value)

    def test_invalid_report_path_parent_missing(self, tmp_path: Path) -> None:
        """Should raise if cloven_report_path parent doesn't exist."""
        nonexistent = tmp_path / "nonexistent" / "subdir" / "reports"
        settings = CleaveSettings(
            cloven_report_naming="descriptor",
            backend="claude",
            cloven_report_path=str(nonexistent),
        )
        with pytest.raises(SettingsValidationError) as exc_info:
            _validate_settings(settings)
        assert "parent directory" in str(exc_info.value).lower()

    def test_valid_report_path(self, tmp_path: Path) -> None:
        """Should not raise if cloven_report_path is valid."""
        report_dir = tmp_path / "reports"
        settings = CleaveSettings(
            cloven_report_naming="descriptor",
            backend="claude",
            cloven_report_path=str(report_dir),
        )
        # Should not raise
        _validate_settings(settings)

    def test_none_report_path(self) -> None:
        """Should allow None for cloven_report_path."""
        settings = CleaveSettings(
            cloven_report_naming="descriptor",
            backend="claude",
            cloven_report_path=None,
        )
        # Should not raise
        _validate_settings(settings)

    def test_backends_not_dict(self) -> None:
        """Should raise if backends is not a dict."""
        settings = CleaveSettings(
            cloven_report_naming="descriptor",
            backend="claude",
        )
        # Bypass type checking to test validation
        settings.backends = "not a dict"  # type: ignore
        with pytest.raises(SettingsValidationError) as exc_info:
            _validate_settings(settings)
        assert "backends must be a dict" in str(exc_info.value)

    def test_backend_config_not_backend_settings(self) -> None:
        """Should raise if backend config is not BackendSettings."""
        settings = CleaveSettings(
            cloven_report_naming="descriptor",
            backend="claude",
        )
        # Bypass type checking to test validation
        settings.backends["claude"] = {"invalid": "dict"}  # type: ignore
        with pytest.raises(SettingsValidationError) as exc_info:
            _validate_settings(settings)
        assert "BackendSettings" in str(exc_info.value)

    def test_backend_config_invalid_base_url_type(self) -> None:
        """Should raise if backend base_url is not a string."""
        backend_config = BackendSettings(base_url=12345)  # type: ignore
        settings = CleaveSettings(
            cloven_report_naming="descriptor",
            backend="ollama",
            backends={"ollama": backend_config},
        )
        with pytest.raises(SettingsValidationError) as exc_info:
            _validate_settings(settings)
        assert "base_url" in str(exc_info.value)
        assert "must be a string" in str(exc_info.value)


class TestSetSetting:
    """Tests for set_setting() validation."""

    def test_set_valid_report_naming(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
        """Should allow setting valid cloven_report_naming."""
        settings_file = tmp_path / "settings.yaml"
        monkeypatch.setattr("cleave.core.settings.SETTINGS_FILE", settings_file)
        monkeypatch.setattr("cleave.core.settings.SETTINGS_DIR", tmp_path)

        set_setting("cloven_report_naming", "timestamp")
        # Should not raise
        settings = load_settings()
        assert settings.cloven_report_naming == "timestamp"

    def test_set_invalid_report_naming(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
        """Should raise for invalid cloven_report_naming."""
        settings_file = tmp_path / "settings.yaml"
        monkeypatch.setattr("cleave.core.settings.SETTINGS_FILE", settings_file)
        monkeypatch.setattr("cleave.core.settings.SETTINGS_DIR", tmp_path)

        with pytest.raises(SettingsValidationError) as exc_info:
            set_setting("cloven_report_naming", "invalid")
        assert "Invalid cloven_report_naming" in str(exc_info.value)
        # Should suggest valid values
        for valid in VALID_REPORT_NAMING:
            assert valid in str(exc_info.value)

    def test_set_invalid_report_naming_type(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
        """Should raise if cloven_report_naming is not a string."""
        settings_file = tmp_path / "settings.yaml"
        monkeypatch.setattr("cleave.core.settings.SETTINGS_FILE", settings_file)
        monkeypatch.setattr("cleave.core.settings.SETTINGS_DIR", tmp_path)

        with pytest.raises(SettingsValidationError) as exc_info:
            set_setting("cloven_report_naming", 123)
        assert "must be a string" in str(exc_info.value)

    def test_set_valid_backend(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
        """Should allow setting valid backend."""
        settings_file = tmp_path / "settings.yaml"
        monkeypatch.setattr("cleave.core.settings.SETTINGS_FILE", settings_file)
        monkeypatch.setattr("cleave.core.settings.SETTINGS_DIR", tmp_path)

        set_setting("backend", "ollama")
        settings = load_settings()
        assert settings.backend == "ollama"

    def test_set_invalid_backend(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
        """Should raise for invalid backend."""
        settings_file = tmp_path / "settings.yaml"
        monkeypatch.setattr("cleave.core.settings.SETTINGS_FILE", settings_file)
        monkeypatch.setattr("cleave.core.settings.SETTINGS_DIR", tmp_path)

        with pytest.raises(SettingsValidationError) as exc_info:
            set_setting("backend", "invalid-backend")
        assert "Invalid backend" in str(exc_info.value)
        # Should suggest valid values
        for valid in VALID_BACKENDS:
            assert valid in str(exc_info.value)

    def test_set_invalid_backend_type(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
        """Should raise if backend is not a string."""
        settings_file = tmp_path / "settings.yaml"
        monkeypatch.setattr("cleave.core.settings.SETTINGS_FILE", settings_file)
        monkeypatch.setattr("cleave.core.settings.SETTINGS_DIR", tmp_path)

        with pytest.raises(SettingsValidationError) as exc_info:
            set_setting("backend", ["list", "not", "string"])
        assert "must be a string" in str(exc_info.value)

    def test_set_valid_report_path(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
        """Should allow setting valid cloven_report_path."""
        settings_file = tmp_path / "settings.yaml"
        monkeypatch.setattr("cleave.core.settings.SETTINGS_FILE", settings_file)
        monkeypatch.setattr("cleave.core.settings.SETTINGS_DIR", tmp_path)

        report_dir = tmp_path / "reports"
        set_setting("cloven_report_path", str(report_dir))
        settings = load_settings()
        assert settings.cloven_report_path == str(report_dir)

    def test_set_invalid_report_path(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
        """Should raise if cloven_report_path parent doesn't exist."""
        settings_file = tmp_path / "settings.yaml"
        monkeypatch.setattr("cleave.core.settings.SETTINGS_FILE", settings_file)
        monkeypatch.setattr("cleave.core.settings.SETTINGS_DIR", tmp_path)

        nonexistent = tmp_path / "nonexistent" / "reports"
        with pytest.raises(SettingsValidationError) as exc_info:
            set_setting("cloven_report_path", str(nonexistent))
        assert "parent directory" in str(exc_info.value).lower()

    def test_set_invalid_report_path_type(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
        """Should raise if cloven_report_path is not a string."""
        settings_file = tmp_path / "settings.yaml"
        monkeypatch.setattr("cleave.core.settings.SETTINGS_FILE", settings_file)
        monkeypatch.setattr("cleave.core.settings.SETTINGS_DIR", tmp_path)

        with pytest.raises(SettingsValidationError) as exc_info:
            set_setting("cloven_report_path", 123)
        assert "must be a string or None" in str(exc_info.value)

    def test_set_backends_invalid_type(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
        """Should raise if backends is not a dict."""
        settings_file = tmp_path / "settings.yaml"
        monkeypatch.setattr("cleave.core.settings.SETTINGS_FILE", settings_file)
        monkeypatch.setattr("cleave.core.settings.SETTINGS_DIR", tmp_path)

        with pytest.raises(SettingsValidationError) as exc_info:
            set_setting("backends", "not a dict")
        assert "must be a dict" in str(exc_info.value)


class TestLoadSettings:
    """Tests for load_settings() validation."""

    def test_load_invalid_settings_raises(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
        """Should raise SettingsValidationError for invalid settings."""
        settings_file = tmp_path / "settings.yaml"
        monkeypatch.setattr("cleave.core.settings.SETTINGS_FILE", settings_file)
        monkeypatch.setattr("cleave.core.settings.SETTINGS_DIR", tmp_path)

        # Write invalid settings
        settings_file.write_text("""
cloven_report_naming: invalid-value
backend: claude
""")

        with pytest.raises(SettingsValidationError) as exc_info:
            load_settings()
        assert "cloven_report_naming" in str(exc_info.value)


class TestSaveSettings:
    """Tests for save_settings() validation."""

    def test_save_invalid_settings_raises(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
        """Should raise SettingsValidationError for invalid settings."""
        settings_file = tmp_path / "settings.yaml"
        monkeypatch.setattr("cleave.core.settings.SETTINGS_FILE", settings_file)
        monkeypatch.setattr("cleave.core.settings.SETTINGS_DIR", tmp_path)

        settings = CleaveSettings(
            cloven_report_naming="invalid-naming",
            backend="claude",
        )

        with pytest.raises(SettingsValidationError) as exc_info:
            save_settings(settings)
        assert "cloven_report_naming" in str(exc_info.value)

    def test_save_valid_settings(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
        """Should save valid settings without error."""
        settings_file = tmp_path / "settings.yaml"
        monkeypatch.setattr("cleave.core.settings.SETTINGS_FILE", settings_file)
        monkeypatch.setattr("cleave.core.settings.SETTINGS_DIR", tmp_path)

        settings = CleaveSettings(
            cloven_report_naming="descriptor",
            backend="claude",
        )

        save_settings(settings)
        assert settings_file.exists()

        # Verify it can be loaded back
        loaded = load_settings()
        assert loaded.cloven_report_naming == "descriptor"
        assert loaded.backend == "claude"
