"""
Tests for LLM provider auto-discovery system.
"""

import pytest
from unittest.mock import Mock, patch

from local_deep_research.llm.providers.auto_discovery import (
    ProviderInfo,
    ProviderDiscovery,
    discover_providers,
    get_discovered_provider_options,
    get_provider_class,
)
from local_deep_research.llm.providers.base import BaseLLMProvider


class TestProviderInfo:
    """Tests for ProviderInfo class."""

    def test_provider_info_from_class(self, mock_provider_class):
        """Creates ProviderInfo from provider class."""
        info = ProviderInfo(mock_provider_class)
        assert info.provider_key == "MOCK"
        assert info.provider_name == "Mock Provider"
        assert info.company_name == "Mock Company"
        assert info.is_cloud is True

    def test_provider_info_default_provider_key(self):
        """Generates default provider key from class name."""

        class TestProvider(BaseLLMProvider):
            provider_name = "Test Provider"
            # No explicit provider_key

        info = ProviderInfo(TestProvider)
        assert info.provider_key == "TEST"

    def test_provider_info_to_dict(self, mock_provider_class):
        """Converts to dictionary for API responses."""
        info = ProviderInfo(mock_provider_class)
        result = info.to_dict()

        assert "value" in result
        assert "label" in result
        assert "is_cloud" in result

        assert result["value"] == "MOCK"
        assert result["is_cloud"] is True

    def test_display_name_generation_cloud(self, mock_provider_class):
        """Generates display name for cloud provider."""
        info = ProviderInfo(mock_provider_class)
        # Should contain provider name and cloud indicator
        assert "Mock Provider" in info.display_name
        assert "☁️" in info.display_name or "Cloud" in info.display_name

    def test_display_name_generation_local(self, mock_local_provider_class):
        """Generates display name for local provider."""
        info = ProviderInfo(mock_local_provider_class)
        # Should contain provider name and local indicator
        assert "Mock Local" in info.display_name
        assert "💻" in info.display_name or "Local" in info.display_name

    def test_requires_auth_for_models_with_method(self, mock_provider_class):
        """Uses requires_auth_for_models method if available."""
        info = ProviderInfo(mock_provider_class)
        assert info.requires_auth_for_models is True

    def test_requires_auth_for_models_inherited(self):
        """Inherits default True from BaseLLMProvider."""

        class SimpleProvider(BaseLLMProvider):
            provider_name = "Simple"

        info = ProviderInfo(SimpleProvider)
        assert info.requires_auth_for_models is True


class TestProviderDiscovery:
    """Tests for ProviderDiscovery class."""

    @pytest.fixture
    def discovery(self):
        """Create fresh ProviderDiscovery instance."""
        # Reset singleton for testing
        ProviderDiscovery._instance = None
        ProviderDiscovery._providers = {}
        return ProviderDiscovery()

    def test_singleton_pattern(self, discovery):
        """ProviderDiscovery is a singleton."""
        discovery2 = ProviderDiscovery()
        assert discovery is discovery2

    def test_discover_providers_returns_dict(self, discovery):
        """discover_providers returns a dictionary."""
        # Mock the implementations directory
        with patch("pathlib.Path.glob") as mock_glob:
            mock_glob.return_value = []
            result = discovery.discover_providers()
            assert isinstance(result, dict)

    def test_discover_providers_caches_result(self, discovery):
        """discover_providers caches results."""
        with patch("pathlib.Path.glob") as mock_glob:
            mock_glob.return_value = []
            discovery.discover_providers()

            # Second call shouldn't re-scan
            discovery.discover_providers()
            assert mock_glob.call_count == 1

    def test_discover_providers_force_refresh(self, discovery):
        """discover_providers with force_refresh re-scans."""
        with patch("pathlib.Path.glob") as mock_glob:
            mock_glob.return_value = []
            discovery.discover_providers()
            discovery.discover_providers(force_refresh=True)
            assert mock_glob.call_count == 2

    def test_get_provider_info_not_found(self, discovery):
        """get_provider_info returns None for unknown provider."""
        with patch("pathlib.Path.glob") as mock_glob:
            mock_glob.return_value = []
            discovery.discover_providers()
            result = discovery.get_provider_info("NONEXISTENT")
            assert result is None

    def test_get_provider_info_case_insensitive(self, discovery):
        """get_provider_info is case-insensitive."""
        with patch("pathlib.Path.glob") as mock_glob:
            mock_glob.return_value = []
            discovery.discover_providers()

            # Add a mock provider
            mock_info = Mock()
            discovery._providers["TEST"] = mock_info

            # Should find it regardless of case
            assert discovery.get_provider_info("test") is mock_info
            assert discovery.get_provider_info("TEST") is mock_info
            assert discovery.get_provider_info("Test") is mock_info

    def test_get_provider_options_returns_list(self, discovery):
        """get_provider_options returns a sorted list."""
        with patch("pathlib.Path.glob") as mock_glob:
            mock_glob.return_value = []
            discovery.discover_providers()
            result = discovery.get_provider_options()
            assert isinstance(result, list)

    def test_get_provider_options_sorted(self, discovery):
        """get_provider_options returns sorted by label."""
        # Add mock providers with specific labels
        mock_info_a = Mock()
        mock_info_a.to_dict.return_value = {
            "value": "A",
            "label": "Alpha Provider",
        }

        mock_info_z = Mock()
        mock_info_z.to_dict.return_value = {
            "value": "Z",
            "label": "Zeta Provider",
        }

        discovery._discovered = True
        discovery._providers = {"A": mock_info_a, "Z": mock_info_z}

        result = discovery.get_provider_options()
        assert result[0]["label"] == "Alpha Provider"
        assert result[1]["label"] == "Zeta Provider"

    def test_get_provider_class_found(self, discovery, mock_provider_class):
        """get_provider_class returns class for known provider."""
        mock_info = Mock()
        mock_info.provider_class = mock_provider_class

        discovery._discovered = True
        discovery._providers = {"MOCK": mock_info}

        result = discovery.get_provider_class("MOCK")
        assert result is mock_provider_class

    def test_get_provider_class_not_found(self, discovery):
        """get_provider_class returns None for unknown provider."""
        discovery._discovered = True
        discovery._providers = {}

        result = discovery.get_provider_class("NONEXISTENT")
        assert result is None


class TestModuleFunctions:
    """Tests for module-level convenience functions.

    Note: Singleton reset is handled by the global reset_all_singletons fixture
    in tests/conftest.py.
    """

    def test_discover_providers_function(self):
        """discover_providers() function works."""
        with patch("pathlib.Path.glob") as mock_glob:
            mock_glob.return_value = []
            result = discover_providers()
            assert isinstance(result, dict)

    def test_discover_providers_force_refresh(self):
        """discover_providers() supports force_refresh.

        This test was previously skipped due to singleton caching issues.
        The global reset_all_singletons fixture now properly resets the
        ProviderDiscovery singleton between tests.
        """
        with patch("pathlib.Path.glob") as mock_glob:
            mock_glob.return_value = []
            # First call - may or may not trigger glob depending on singleton state
            discover_providers()
            initial_count = mock_glob.call_count

            # Force refresh should always trigger a new scan
            discover_providers(force_refresh=True)

            # The force_refresh call should have incremented the count
            assert mock_glob.call_count >= initial_count + 1

    def test_get_discovered_provider_options_function(self):
        """get_discovered_provider_options() function works."""
        with patch("pathlib.Path.glob") as mock_glob:
            mock_glob.return_value = []
            result = get_discovered_provider_options()
            assert isinstance(result, list)

    def test_get_provider_class_function(self):
        """get_provider_class() function works."""
        with patch("pathlib.Path.glob") as mock_glob:
            mock_glob.return_value = []
            result = get_provider_class("NONEXISTENT")
            assert result is None


class TestProviderDiscoveryIntegration:
    """Integration tests for provider discovery.

    Note: Singleton reset is handled by the global reset_all_singletons fixture
    in tests/conftest.py.
    """

    def test_discovers_real_providers(self):
        """Discovers actual provider implementations."""
        # This test uses the real implementations directory
        result = discover_providers(force_refresh=True)

        # Should find at least some providers
        assert len(result) > 0

        # Check that providers have expected structure
        for key, info in result.items():
            assert isinstance(key, str)
            assert hasattr(info, "provider_key")
            assert hasattr(info, "provider_name")
            assert hasattr(info, "to_dict")

    def test_discovered_providers_have_required_attributes(self):
        """Discovered providers have all required attributes."""
        result = discover_providers(force_refresh=True)

        for key, info in result.items():
            # All providers should have these
            assert info.provider_key is not None
            assert info.provider_name is not None
            assert info.display_name is not None
            assert info.is_cloud in [True, False, None]

            # to_dict should return valid structure
            dict_result = info.to_dict()
            assert "value" in dict_result
            assert "label" in dict_result

    def test_provider_options_for_dropdown(self):
        """Provider options are suitable for UI dropdowns."""
        discover_providers(force_refresh=True)
        options = get_discovered_provider_options()

        assert isinstance(options, list)

        for option in options:
            # Each option should have value and label
            assert "value" in option
            assert "label" in option

            # Value should be a string key
            assert isinstance(option["value"], str)
            assert len(option["value"]) > 0

            # Label should be human-readable
            assert isinstance(option["label"], str)
            assert len(option["label"]) > 0


class TestGetAvailableProviderOptions:
    """Tests for get_available_provider_options filtering."""

    @pytest.fixture
    def discovery(self):
        """Create fresh ProviderDiscovery instance."""
        ProviderDiscovery._instance = None
        ProviderDiscovery._providers = {}
        return ProviderDiscovery()

    def test_filters_unavailable_providers(self, discovery):
        """Unavailable providers are excluded from results."""
        available = Mock()
        available.provider_class.is_available.return_value = True
        available.provider_key = "AVAIL"
        available.to_dict.return_value = {
            "value": "AVAIL",
            "label": "Available",
        }

        unavailable = Mock()
        unavailable.provider_class.is_available.return_value = False
        unavailable.provider_key = "UNAVAIL"

        discovery._discovered = True
        discovery._providers = {"AVAIL": available, "UNAVAIL": unavailable}

        result = discovery.get_available_provider_options()
        assert len(result) == 1
        assert result[0]["value"] == "AVAIL"

    def test_keeps_available_providers(self, discovery):
        """Available providers are included in results."""
        p1 = Mock()
        p1.provider_class.is_available.return_value = True
        p1.provider_key = "P1"
        p1.to_dict.return_value = {"value": "P1", "label": "Provider 1"}

        p2 = Mock()
        p2.provider_class.is_available.return_value = True
        p2.provider_key = "P2"
        p2.to_dict.return_value = {"value": "P2", "label": "Provider 2"}

        discovery._discovered = True
        discovery._providers = {"P1": p1, "P2": p2}

        result = discovery.get_available_provider_options()
        assert len(result) == 2

    def test_forwards_settings_snapshot(self, discovery):
        """settings_snapshot is passed to is_available()."""
        provider = Mock()
        provider.provider_class.is_available.return_value = True
        provider.provider_key = "TEST"
        provider.to_dict.return_value = {"value": "TEST", "label": "Test"}

        discovery._discovered = True
        discovery._providers = {"TEST": provider}

        snapshot = {"some_key": "some_value"}
        discovery.get_available_provider_options(settings_snapshot=snapshot)
        provider.provider_class.is_available.assert_called_once_with(
            settings_snapshot=snapshot
        )

    def test_empty_result_does_not_crash(self, discovery):
        """Empty provider list returns empty list without error."""
        discovery._discovered = True
        discovery._providers = {}

        result = discovery.get_available_provider_options()
        assert result == []


class TestBaseLLMProviderDefaults:
    """Tests for BaseLLMProvider default method implementations."""

    def test_is_available_defaults_to_false(self):
        """BaseLLMProvider.is_available() returns False (fail-closed).

        Subclasses must override this to indicate actual availability.
        """
        assert BaseLLMProvider.is_available() is False

    def test_requires_auth_for_models_defaults_to_true(self):
        """BaseLLMProvider.requires_auth_for_models() returns True."""
        assert BaseLLMProvider.requires_auth_for_models() is True

    def test_create_llm_raises_not_implemented(self):
        """BaseLLMProvider.create_llm() raises NotImplementedError."""
        with pytest.raises(NotImplementedError, match="BaseLLMProvider"):
            BaseLLMProvider.create_llm()
