"""Tests for API settings validation and error handling."""

import pytest
from unittest.mock import patch, MagicMock

from src.local_deep_research.api.settings_utils import (
    InMemorySettingsManager,
    create_settings_snapshot,
)
from src.local_deep_research.api import quick_summary


class TestSettingsValidation:
    """Test validation of settings values."""

    def test_temperature_validation(self):
        """Test temperature value validation."""
        # Valid temperature values
        valid_temps = [0.0, 0.5, 1.0, 0.7, 0.999]
        for temp in valid_temps:
            snapshot = create_settings_snapshot(temperature=temp)
            assert snapshot["llm.temperature"]["value"] == temp

        # Invalid temperatures should be handled gracefully
        # (actual validation would happen in the LLM layer)
        invalid_temps = [-0.1, 1.1, 2.0, "not_a_number"]
        for temp in invalid_temps:
            snapshot = create_settings_snapshot(temperature=temp)
            # Should accept the value (validation happens later)
            assert snapshot["llm.temperature"]["value"] == temp

    def test_api_key_validation(self):
        """Test API key handling and validation."""
        # Test empty/None API keys
        snapshot1 = create_settings_snapshot(provider="openai", api_key="")
        assert snapshot1["llm.openai.api_key"]["value"] == ""

        snapshot2 = create_settings_snapshot(provider="anthropic", api_key=None)
        assert snapshot2["llm.anthropic.api_key"]["value"] is None

        # Test special characters in API keys
        special_key = "sk-proj-$p3c!@l_ch@rs_123"
        snapshot3 = create_settings_snapshot(
            provider="openai", api_key=special_key
        )
        assert snapshot3["llm.openai.api_key"]["value"] == special_key

    def test_provider_name_normalization(self):
        """Test that provider names are handled correctly."""
        # Test various provider name formats
        providers = [
            ("openai", "openai"),
            ("OpenAI", "OpenAI"),  # Case preserved
            ("OPENAI", "OPENAI"),
            ("anthropic", "anthropic"),
            ("Anthropic", "Anthropic"),
            ("ollama", "ollama"),
            ("custom_provider", "custom_provider"),
        ]

        for input_provider, expected in providers:
            snapshot = create_settings_snapshot(provider=input_provider)
            assert snapshot["llm.provider"]["value"] == expected

    def test_search_results_validation(self):
        """Test search results count validation."""
        # Valid counts
        valid_counts = [1, 5, 10, 20, 50, 100]
        for count in valid_counts:
            snapshot = create_settings_snapshot(max_search_results=count)
            assert snapshot["search.max_results"]["value"] == count

        # Edge cases (actual validation in search layer)
        edge_counts = [0, -1, 1000, "ten"]
        for count in edge_counts:
            snapshot = create_settings_snapshot(max_search_results=count)
            assert snapshot["search.max_results"]["value"] == count


class TestAPIErrorHandling:
    """Test error handling in API functions with settings."""

    @patch("src.local_deep_research.api.research_functions._init_search_system")
    def test_quick_summary_with_invalid_provider(self, mock_init):
        """Test quick_summary with invalid provider."""
        mock_init.side_effect = ValueError("Invalid provider: invalid_provider")

        with pytest.raises(ValueError) as exc_info:
            quick_summary(
                "test query", provider="invalid_provider", api_key="some-key"
            )

        assert "Invalid provider" in str(exc_info.value)

    @patch("src.local_deep_research.api.research_functions._init_search_system")
    def test_quick_summary_with_missing_api_key(self, mock_init):
        """Test behavior when API key is missing."""
        # Mock the search system to simulate missing API key error
        mock_system = MagicMock()
        mock_system.analyze_topic.side_effect = Exception(
            "API key required for provider"
        )
        mock_init.return_value = mock_system

        # Should raise an error about missing API key
        with pytest.raises(Exception) as exc_info:
            result = quick_summary(
                "test query",
                provider="openai",
                # No API key provided
            )
            # Force evaluation
            if hasattr(result, "__next__"):
                next(result)

        assert "API key required" in str(exc_info.value)

    def test_settings_with_circular_references(self):
        """Test handling of circular references in settings."""
        # Create settings with potential circular structure
        circular_dict = {"a": {}}
        circular_dict["a"]["b"] = circular_dict  # Circular reference

        # Should handle gracefully (no infinite loop)
        # Note: actual deep copy in create_settings_snapshot prevents this
        try:
            _ = create_settings_snapshot(
                base_settings={"custom": circular_dict}
            )
            # If we get here, the circular reference was handled
            assert True
        except Exception as e:
            # Some exception is acceptable as long as it's not a RecursionError
            assert not isinstance(e, RecursionError)


class TestEnvironmentVariableIntegration:
    """Test environment variable integration with settings."""

    def test_environment_variable_type_conversion(self, monkeypatch):
        """Test that env vars are converted to correct types."""
        # Set various types via env vars
        monkeypatch.setenv("LDR_LLM_TEMPERATURE", "0.7")
        monkeypatch.setenv("LDR_SEARCH_MAX_RESULTS", "15")
        monkeypatch.setenv("LDR_SEARCH_SAFE_SEARCH", "true")
        monkeypatch.setenv("LDR_LLM_PROVIDER", "anthropic")

        manager = InMemorySettingsManager()

        # Check type conversions
        temp = manager.get_setting("llm.temperature")
        assert isinstance(temp, float)
        assert temp == 0.7

        # Note: The current implementation may not convert all types
        # This test documents expected behavior

    def test_invalid_environment_variable_values(self, monkeypatch):
        """Test handling of invalid env var values."""
        # Set invalid values
        monkeypatch.setenv("LDR_LLM_TEMPERATURE", "not_a_number")
        monkeypatch.setenv("LDR_SEARCH_MAX_RESULTS", "abc")

        manager = InMemorySettingsManager()

        # Should handle gracefully - either use default or store as string
        temp = manager.get_setting("llm.temperature")
        assert temp is not None  # Should have some value

    def test_environment_variable_precedence(self, monkeypatch):
        """Test precedence of env vars vs explicit settings."""
        monkeypatch.setenv("LDR_LLM_PROVIDER", "env_provider")

        # Env var should affect default snapshot
        default_snapshot = create_settings_snapshot()
        assert default_snapshot["llm.provider"]["value"] == "env_provider"

        # But explicit setting should override
        explicit_snapshot = create_settings_snapshot(
            provider="explicit_provider"
        )
        assert explicit_snapshot["llm.provider"]["value"] == "explicit_provider"

        # And overrides should also take precedence
        override_snapshot = create_settings_snapshot(
            overrides={"llm.provider": "override_provider"}
        )
        assert override_snapshot["llm.provider"]["value"] == "override_provider"


class TestSettingsUsabilityPatterns:
    """Test common usage patterns for settings."""

    def test_minimal_configuration(self):
        """Test that minimal configuration works correctly."""
        # Just provider and API key
        snapshot = create_settings_snapshot(
            provider="openai", api_key="sk-test"
        )

        # Should have sensible defaults for everything else
        assert snapshot["llm.provider"]["value"] == "openai"
        assert snapshot["llm.openai.api_key"]["value"] == "sk-test"
        assert "llm.temperature" in snapshot  # Should have default
        assert "search.max_results" in snapshot  # Should have default

    def test_common_research_presets(self):
        """Test common research configuration presets."""
        # Fast research preset
        fast_preset = create_settings_snapshot(
            provider="anthropic",
            api_key="test-key",
            temperature=0.7,
            overrides={
                "research.max_iterations": 1,
                "search.max_results": 5,
                "llm.max_tokens": 1000,
            },
        )

        # Thorough research preset
        thorough_preset = create_settings_snapshot(
            provider="openai",
            api_key="test-key",
            temperature=0.3,
            overrides={
                "research.max_iterations": 5,
                "search.max_results": 20,
                "llm.max_tokens": 4000,
                "search.engines.arxiv.enabled": True,
                "search.engines.wikipedia.enabled": True,
            },
        )

        # Verify presets have expected values
        assert fast_preset["research.max_iterations"]["value"] == 1
        assert fast_preset["search.max_results"]["value"] == 5

        assert thorough_preset["research.max_iterations"]["value"] == 5
        assert thorough_preset["search.engines.arxiv.enabled"]["value"] is True

    def test_provider_switching(self):
        """Test switching between providers easily."""
        base_config = {
            "research.max_iterations": 3,
            "search.max_results": 10,
        }

        # Create snapshots for different providers
        openai_snapshot = create_settings_snapshot(
            provider="openai", api_key="openai-key", overrides=base_config
        )

        anthropic_snapshot = create_settings_snapshot(
            provider="anthropic", api_key="anthropic-key", overrides=base_config
        )

        ollama_snapshot = create_settings_snapshot(
            provider="ollama",
            overrides={
                **base_config,
                "llm.ollama.base_url": "http://localhost:11434",
            },
        )

        # Verify each has correct provider config
        assert openai_snapshot["llm.provider"]["value"] == "openai"
        assert openai_snapshot["llm.openai.api_key"]["value"] == "openai-key"

        assert anthropic_snapshot["llm.provider"]["value"] == "anthropic"
        assert (
            anthropic_snapshot["llm.anthropic.api_key"]["value"]
            == "anthropic-key"
        )

        assert ollama_snapshot["llm.provider"]["value"] == "ollama"
        assert (
            ollama_snapshot["llm.ollama.base_url"]["value"]
            == "http://localhost:11434"
        )

        # All should have same base config
        for snapshot in [openai_snapshot, anthropic_snapshot, ollama_snapshot]:
            assert snapshot["research.max_iterations"]["value"] == 3
            assert snapshot["search.max_results"]["value"] == 10


class TestSettingsBackwardCompatibility:
    """Test backward compatibility of settings."""

    def test_legacy_setting_names(self):
        """Test that legacy setting names still work."""
        # If we rename settings in the future, we should handle old names
        # This is a placeholder for future compatibility tests
        snapshot = create_settings_snapshot()

        # Current settings should exist
        assert "llm.provider" in snapshot
        assert "search.max_results" in snapshot

    def test_settings_migration(self):
        """Test migration of old settings format to new format."""
        # Example: if we change setting structure in the future
        # This is a placeholder for future compatibility tests

        # Future: converter function would migrate to new format
        # For now, just verify current format works
        snapshot = create_settings_snapshot(
            provider="openai", temperature=0.7, max_search_results=10
        )

        assert snapshot["llm.provider"]["value"] == "openai"
        assert snapshot["llm.temperature"]["value"] == 0.7
        assert snapshot["search.max_results"]["value"] == 10
