"""
Tests for OpenAI-compatible base provider.
"""

import pytest
from unittest.mock import Mock, patch

from local_deep_research.llm.providers.openai_base import (
    OpenAICompatibleProvider,
)


class TestOpenAICompatibleProviderMetadata:
    """Tests for OpenAICompatibleProvider class metadata."""

    def test_provider_name(self):
        """Default provider name is set."""
        assert OpenAICompatibleProvider.provider_name == "openai_endpoint"

    def test_default_base_url(self):
        """Default base URL is OpenAI."""
        assert (
            OpenAICompatibleProvider.default_base_url
            == "https://api.openai.com/v1"
        )

    def test_default_model(self):
        """Default model is empty by design — users must explicitly pick one."""
        assert OpenAICompatibleProvider.default_model == ""

    def test_api_key_setting(self):
        """API key setting is defined."""
        assert OpenAICompatibleProvider.api_key_setting is not None


class TestOpenAICompatibleCreateLLM:
    """Tests for create_llm method."""

    def test_create_llm_raises_without_api_key(self):
        """Raises ValueError when API key not configured."""
        with patch(
            "local_deep_research.config.thread_settings.get_setting_from_snapshot"
        ) as mock_get_setting:
            mock_get_setting.return_value = None

            with pytest.raises(ValueError) as exc_info:
                OpenAICompatibleProvider.create_llm()

            assert "api key" in str(exc_info.value).lower()

    def test_create_llm_success(self):
        """Successfully creates ChatOpenAI instance."""

        def mock_get_setting_side_effect(key, default=None, *args, **kwargs):
            settings_map = {
                "llm.openai_endpoint.api_key": "test-api-key",
                "llm.max_tokens": 4096,
                "llm.streaming": True,
                "llm.max_retries": 3,
                "llm.request_timeout": 60,
            }
            return settings_map.get(key, default)

        with patch(
            "local_deep_research.config.thread_settings.get_setting_from_snapshot"
        ) as mock_get_setting:
            mock_get_setting.side_effect = mock_get_setting_side_effect

            with patch(
                "local_deep_research.llm.providers.openai_base.ChatOpenAI"
            ) as mock_chat_openai:
                mock_llm = Mock()
                mock_chat_openai.return_value = mock_llm

                result = OpenAICompatibleProvider.create_llm(
                    model_name="test-model"
                )

                assert result is mock_llm
                mock_chat_openai.assert_called_once()

    def test_create_llm_uses_default_model(self):
        """Raises ValueError when no model name is provided (no silent default)."""

        def mock_get_setting_side_effect(key, default=None, *args, **kwargs):
            settings_map = {
                "llm.openai_endpoint.api_key": "test-api-key",
                "llm.max_tokens": 4096,
                "llm.streaming": True,
                "llm.max_retries": 3,
                "llm.request_timeout": 60,
            }
            return settings_map.get(key, default)

        with patch(
            "local_deep_research.config.thread_settings.get_setting_from_snapshot"
        ) as mock_get_setting:
            mock_get_setting.side_effect = mock_get_setting_side_effect

            with pytest.raises(ValueError, match="model not configured"):
                OpenAICompatibleProvider.create_llm()

    def test_create_llm_with_custom_model(self):
        """Uses custom model when specified."""

        def mock_get_setting_side_effect(key, default=None, *args, **kwargs):
            settings_map = {
                "llm.openai_endpoint.api_key": "test-api-key",
                "llm.max_tokens": 4096,
                "llm.streaming": True,
                "llm.max_retries": 3,
                "llm.request_timeout": 60,
            }
            return settings_map.get(key, default)

        with patch(
            "local_deep_research.config.thread_settings.get_setting_from_snapshot"
        ) as mock_get_setting:
            mock_get_setting.side_effect = mock_get_setting_side_effect

            with patch(
                "local_deep_research.llm.providers.openai_base.ChatOpenAI"
            ) as mock_chat_openai:
                OpenAICompatibleProvider.create_llm(model_name="gpt-4")

                call_kwargs = mock_chat_openai.call_args[1]
                assert call_kwargs["model"] == "gpt-4"

    def test_create_llm_with_custom_temperature(self):
        """Uses custom temperature."""

        def mock_get_setting_side_effect(key, default=None, *args, **kwargs):
            settings_map = {
                "llm.openai_endpoint.api_key": "test-api-key",
                "llm.max_tokens": 4096,
                "llm.streaming": True,
                "llm.max_retries": 3,
                "llm.request_timeout": 60,
            }
            return settings_map.get(key, default)

        with patch(
            "local_deep_research.config.thread_settings.get_setting_from_snapshot"
        ) as mock_get_setting:
            mock_get_setting.side_effect = mock_get_setting_side_effect

            with patch(
                "local_deep_research.llm.providers.openai_base.ChatOpenAI"
            ) as mock_chat_openai:
                OpenAICompatibleProvider.create_llm(
                    model_name="test-model", temperature=0.2
                )

                call_kwargs = mock_chat_openai.call_args[1]
                assert call_kwargs["temperature"] == 0.2

    def test_create_llm_with_custom_base_url(self):
        """Uses custom base URL from kwargs."""

        def mock_get_setting_side_effect(key, default=None, *args, **kwargs):
            settings_map = {
                "llm.openai_endpoint.api_key": "test-api-key",
                "llm.max_tokens": 4096,
                "llm.streaming": True,
                "llm.max_retries": 3,
                "llm.request_timeout": 60,
            }
            return settings_map.get(key, default)

        with patch(
            "local_deep_research.config.thread_settings.get_setting_from_snapshot"
        ) as mock_get_setting:
            mock_get_setting.side_effect = mock_get_setting_side_effect

            with patch(
                "local_deep_research.llm.providers.openai_base.ChatOpenAI"
            ) as mock_chat_openai:
                OpenAICompatibleProvider.create_llm(
                    model_name="test-model",
                    base_url="https://custom.api.com/v1",
                )

                call_kwargs = mock_chat_openai.call_args[1]
                assert "custom.api.com" in call_kwargs["base_url"]


class TestOpenAICompatibleIsAvailable:
    """Tests for is_available method."""

    def test_available_with_api_key(self):
        """Returns True when API key is configured."""
        with patch(
            "local_deep_research.config.thread_settings.get_setting_from_snapshot"
        ) as mock_get_setting:
            mock_get_setting.return_value = "test-api-key"

            result = OpenAICompatibleProvider.is_available()
            assert result is True

    def test_not_available_without_api_key(self):
        """Returns False when API key is not configured."""
        with patch(
            "local_deep_research.config.thread_settings.get_setting_from_snapshot"
        ) as mock_get_setting:
            mock_get_setting.return_value = None

            result = OpenAICompatibleProvider.is_available()
            assert result is False

    def test_not_available_with_empty_api_key(self):
        """Returns False when API key is empty string."""
        with patch(
            "local_deep_research.config.thread_settings.get_setting_from_snapshot"
        ) as mock_get_setting:
            mock_get_setting.return_value = ""

            result = OpenAICompatibleProvider.is_available()
            assert result is False


class TestOpenAICompatibleRequiresAuth:
    """Tests for requires_auth_for_models method."""

    def test_requires_auth_by_default(self):
        """Returns True by default."""
        result = OpenAICompatibleProvider.requires_auth_for_models()
        assert result is True


class TestOpenAICompatibleListModels:
    """Tests for list_models methods."""

    def test_list_models_for_api_without_key(self):
        """Returns empty list when no API key."""
        result = OpenAICompatibleProvider.list_models_for_api()
        assert result == []

    def test_list_models_for_api_with_key(self, mock_openai_client):
        """Returns models when API key provided."""
        with patch("openai.OpenAI") as mock_openai:
            mock_openai.return_value = mock_openai_client

            result = OpenAICompatibleProvider.list_models_for_api(
                api_key="test-key"
            )

            assert isinstance(result, list)
            assert len(result) == 2
            assert result[0]["value"] == "gpt-4"
            assert result[1]["value"] == "gpt-3.5-turbo"

    def test_list_models_for_api_error_handling(self):
        """Returns empty list on error."""
        with patch("openai.OpenAI") as mock_openai:
            mock_openai.side_effect = Exception("API error")

            result = OpenAICompatibleProvider.list_models_for_api(
                api_key="test-key"
            )

            assert result == []

    def test_list_models_uses_settings(self):
        """list_models() gets API key from settings."""
        with patch(
            "local_deep_research.config.thread_settings.get_setting_from_snapshot"
        ) as mock_get_setting:
            mock_get_setting.return_value = "settings-key"

            with patch("openai.OpenAI") as mock_openai:
                mock_client = Mock()
                mock_model = Mock()
                mock_model.id = "gpt-4"
                mock_client.models.list.return_value = Mock(data=[mock_model])
                mock_openai.return_value = mock_client

                result = OpenAICompatibleProvider.list_models()

                assert len(result) == 1

    def test_list_models_for_api_with_custom_base_url(self, mock_openai_client):
        """Passes custom base_url to OpenAI client.

        REGRESSION TEST: This verifies the fix for issue where custom OpenAI
        endpoints with private IPs (172.x, 10.x, etc.) were not working because
        the base_url wasn't being passed correctly to the OpenAI client.
        """
        with patch("openai.OpenAI") as mock_openai:
            mock_openai.return_value = mock_openai_client

            custom_url = "http://172.19.0.5:8000/v1"
            OpenAICompatibleProvider.list_models_for_api(
                api_key="test-key", base_url=custom_url
            )

            # Verify OpenAI client was created with the custom URL
            mock_openai.assert_called_once()
            call_kwargs = mock_openai.call_args[1]
            assert call_kwargs["base_url"] == custom_url

    def test_list_models_for_api_uses_default_url_when_none(
        self, mock_openai_client
    ):
        """Uses default_base_url when no base_url provided."""
        with patch("openai.OpenAI") as mock_openai:
            mock_openai.return_value = mock_openai_client

            OpenAICompatibleProvider.list_models_for_api(api_key="test-key")

            call_kwargs = mock_openai.call_args[1]
            assert (
                call_kwargs["base_url"]
                == OpenAICompatibleProvider.default_base_url
            )

    def test_list_models_for_api_private_ip_addresses(self, mock_openai_client):
        """Works correctly with various private IP address formats.

        REGRESSION TEST: Ensures custom endpoints with private IPs work.
        These are common for self-hosted vLLM, Ollama, and other local servers.
        """
        private_urls = [
            "http://10.0.0.100:8000/v1",  # Class A private
            "http://172.16.0.50:5000/v1",  # Class B private
            "http://172.19.0.5:8000/v1",  # Class B private (Docker)
            "http://192.168.1.100:11434/v1",  # Class C private
            "http://localhost:8000/v1",  # localhost
            "http://127.0.0.1:8000/v1",  # loopback
        ]

        for url in private_urls:
            with patch("openai.OpenAI") as mock_openai:
                mock_openai.return_value = mock_openai_client

                result = OpenAICompatibleProvider.list_models_for_api(
                    api_key="test-key", base_url=url
                )

                # Verify URL was passed correctly
                call_kwargs = mock_openai.call_args[1]
                assert call_kwargs["base_url"] == url, (
                    f"URL {url} was not passed correctly"
                )
                assert len(result) == 2, f"Failed to get models for {url}"


class TestOpenAICompatibleCreateLLMInstance:
    """Tests for _create_llm_instance method."""

    def test_create_instance_bypasses_api_key_check(self):
        """_create_llm_instance doesn't require API key from settings."""
        with patch(
            "local_deep_research.llm.providers.openai_base.ChatOpenAI"
        ) as mock_chat_openai:
            mock_llm = Mock()
            mock_chat_openai.return_value = mock_llm

            result = OpenAICompatibleProvider._create_llm_instance(
                model_name="test-model",
                api_key="provided-key",
            )

            assert result is mock_llm
            call_kwargs = mock_chat_openai.call_args[1]
            assert call_kwargs["api_key"] == "provided-key"

    def test_create_instance_uses_dummy_key_by_default(self):
        """Uses dummy key when none provided."""
        with patch(
            "local_deep_research.llm.providers.openai_base.ChatOpenAI"
        ) as mock_chat_openai:
            mock_llm = Mock()
            mock_chat_openai.return_value = mock_llm

            OpenAICompatibleProvider._create_llm_instance(
                model_name="test-model"
            )

            call_kwargs = mock_chat_openai.call_args[1]
            assert call_kwargs["api_key"] == "not-required"


class TestOpenAICompatibleSubclass:
    """Tests for subclassing OpenAICompatibleProvider."""

    def test_subclass_overrides_work(self):
        """Subclass can override provider settings."""

        class CustomProvider(OpenAICompatibleProvider):
            provider_name = "Custom Provider"
            api_key_setting = "llm.custom.api_key"
            default_base_url = "https://custom.api.com/v1"
            default_model = "custom-model"

        assert CustomProvider.provider_name == "Custom Provider"
        assert CustomProvider.api_key_setting == "llm.custom.api_key"
        assert CustomProvider.default_base_url == "https://custom.api.com/v1"
        assert CustomProvider.default_model == "custom-model"

    def test_subclass_inherits_methods(self):
        """Subclass inherits provider methods."""

        class CustomProvider(OpenAICompatibleProvider):
            provider_name = "Custom"
            api_key_setting = None  # No API key required

        # Should inherit is_available
        assert hasattr(CustomProvider, "is_available")
        assert hasattr(CustomProvider, "create_llm")
        assert hasattr(CustomProvider, "list_models")

    def test_subclass_no_api_key_required(self):
        """Provider with no api_key_setting is always available."""

        class NoAuthProvider(OpenAICompatibleProvider):
            provider_name = "No Auth"
            api_key_setting = None  # No API key required

        result = NoAuthProvider.is_available()
        assert result is True
