"""Tests for Google/Gemini LLM provider."""

import pytest
from unittest.mock import Mock, patch

from local_deep_research.llm.providers.implementations.google import (
    GoogleProvider,
)


class TestGoogleProviderMetadata:
    """Tests for GoogleProvider class metadata."""

    def test_provider_name(self):
        """Provider name is correct."""
        assert GoogleProvider.provider_name == "Google Gemini"

    def test_provider_key(self):
        """Provider key is correct."""
        assert GoogleProvider.provider_key == "GOOGLE"

    def test_is_cloud(self):
        """Google is a cloud provider."""
        assert GoogleProvider.is_cloud is True

    def test_company_name(self):
        """Company name is Google."""
        assert GoogleProvider.company_name == "Google"

    def test_api_key_setting(self):
        """API key setting is correct."""
        assert GoogleProvider.api_key_setting == "llm.google.api_key"

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

    def test_default_base_url(self):
        """Default base URL is Google's OpenAI-compatible endpoint."""
        assert (
            "generativelanguage.googleapis.com"
            in GoogleProvider.default_base_url
        )
        assert "openai" in GoogleProvider.default_base_url


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

    def test_requires_auth_for_models_returns_true(self):
        """Google requires authentication for model listing."""
        assert GoogleProvider.requires_auth_for_models() is True


class TestGoogleListModelsForApi:
    """Tests for list_models_for_api method (custom implementation)."""

    def test_returns_empty_without_api_key(self):
        """Returns empty list when no API key provided."""
        result = GoogleProvider.list_models_for_api(api_key=None)
        assert result == []

    def test_returns_empty_with_empty_api_key(self):
        """Returns empty list when API key is empty string."""
        result = GoogleProvider.list_models_for_api(api_key="")
        assert result == []

    def test_lists_models_with_valid_key(self):
        """Returns models when valid API key provided."""
        mock_response = Mock()
        mock_response.status_code = 200
        mock_response.json.return_value = {
            "models": [
                {
                    "name": "models/gemini-1.5-flash",
                    "supportedGenerationMethods": ["generateContent"],
                },
                {
                    "name": "models/gemini-1.5-pro",
                    "supportedGenerationMethods": ["generateContent"],
                },
            ]
        }

        with patch("local_deep_research.security.safe_get") as mock_get:
            mock_get.return_value = mock_response

            result = GoogleProvider.list_models_for_api(
                api_key="test-google-key"
            )

            assert len(result) == 2
            assert {
                "value": "gemini-1.5-flash",
                "label": "gemini-1.5-flash",
            } in result
            assert {
                "value": "gemini-1.5-pro",
                "label": "gemini-1.5-pro",
            } in result

    def test_strips_models_prefix_from_name(self):
        """Strips 'models/' prefix from model names."""
        mock_response = Mock()
        mock_response.status_code = 200
        mock_response.json.return_value = {
            "models": [
                {
                    "name": "models/gemini-2.0-flash-exp",
                    "supportedGenerationMethods": ["generateContent"],
                },
            ]
        }

        with patch("local_deep_research.security.safe_get") as mock_get:
            mock_get.return_value = mock_response

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

            # Should strip "models/" prefix
            assert result[0]["value"] == "gemini-2.0-flash-exp"
            assert "models/" not in result[0]["value"]

    def test_handles_model_without_prefix(self):
        """Handles model names that don't have 'models/' prefix."""
        mock_response = Mock()
        mock_response.status_code = 200
        mock_response.json.return_value = {
            "models": [
                {
                    "name": "gemini-custom",
                    "supportedGenerationMethods": ["generateContent"],
                },
            ]
        }

        with patch("local_deep_research.security.safe_get") as mock_get:
            mock_get.return_value = mock_response

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

            assert result[0]["value"] == "gemini-custom"

    def test_filters_embedding_models(self):
        """Filters out models that don't support generateContent."""
        mock_response = Mock()
        mock_response.status_code = 200
        mock_response.json.return_value = {
            "models": [
                {
                    "name": "models/gemini-1.5-flash",
                    "supportedGenerationMethods": ["generateContent"],
                },
                {
                    "name": "models/embedding-001",
                    "supportedGenerationMethods": [
                        "embedContent"
                    ],  # Embedding model
                },
                {
                    "name": "models/text-embedding-004",
                    "supportedGenerationMethods": [
                        "embedContent",
                        "countTokens",
                    ],
                },
            ]
        }

        with patch("local_deep_research.security.safe_get") as mock_get:
            mock_get.return_value = mock_response

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

            # Only gemini-1.5-flash should be included
            assert len(result) == 1
            assert result[0]["value"] == "gemini-1.5-flash"

    def test_includes_multimethod_models_with_generate(self):
        """Includes models that support generateContent plus other methods."""
        mock_response = Mock()
        mock_response.status_code = 200
        mock_response.json.return_value = {
            "models": [
                {
                    "name": "models/gemini-1.5-pro",
                    "supportedGenerationMethods": [
                        "generateContent",
                        "countTokens",
                        "createCachedContent",
                    ],
                },
            ]
        }

        with patch("local_deep_research.security.safe_get") as mock_get:
            mock_get.return_value = mock_response

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

            assert len(result) == 1
            assert result[0]["value"] == "gemini-1.5-pro"

    def test_passes_api_key_via_header_not_url(self):
        """Passes API key via the ``x-goog-api-key`` header, not as a
        ``?key=...`` query parameter. See issue #4184: the URL is embedded
        in ``requests``/``urllib3`` exception messages, but headers are
        not — so keeping the key out of the URL is the primary
        prevention-by-construction control against credential leakage
        through logs.
        """
        mock_response = Mock()
        mock_response.status_code = 200
        mock_response.json.return_value = {"models": []}

        with patch("local_deep_research.security.safe_get") as mock_get:
            mock_get.return_value = mock_response

            GoogleProvider.list_models_for_api(api_key="my-test-key")

            url = mock_get.call_args[0][0]
            headers = mock_get.call_args[1].get("headers") or {}

            assert "my-test-key" not in url, (
                "API key must not appear in the URL — it would leak "
                "through HTTP exception messages. See issue #4184."
            )
            assert headers.get("x-goog-api-key") == "my-test-key", (
                f"API key must be passed via the x-goog-api-key header. "
                f"Got headers: {headers!r}"
            )

    def test_uses_native_gemini_api_endpoint(self):
        """Uses native Gemini API endpoint, not OpenAI-compatible."""
        mock_response = Mock()
        mock_response.status_code = 200
        mock_response.json.return_value = {"models": []}

        with patch("local_deep_research.security.safe_get") as mock_get:
            mock_get.return_value = mock_response

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

            call_args = mock_get.call_args[0]
            url = call_args[0]
            # Should use native API, not /openai endpoint
            assert "generativelanguage.googleapis.com" in url
            assert "/v1beta/models" in url
            assert "/openai" not in url

    def test_returns_empty_on_non_200_status(self):
        """Returns empty list when API returns non-200 status."""
        mock_response = Mock()
        mock_response.status_code = 401

        with patch("local_deep_research.security.safe_get") as mock_get:
            mock_get.return_value = mock_response

            result = GoogleProvider.list_models_for_api(api_key="invalid-key")

            assert result == []

    def test_returns_empty_on_400_status(self):
        """Returns empty list on 400 Bad Request."""
        mock_response = Mock()
        mock_response.status_code = 400

        with patch("local_deep_research.security.safe_get") as mock_get:
            mock_get.return_value = mock_response

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

            assert result == []

    def test_returns_empty_on_500_status(self):
        """Returns empty list on server error."""
        mock_response = Mock()
        mock_response.status_code = 500

        with patch("local_deep_research.security.safe_get") as mock_get:
            mock_get.return_value = mock_response

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

            assert result == []

    def test_returns_empty_on_exception(self):
        """Returns empty list when exception occurs."""
        with patch("local_deep_research.security.safe_get") as mock_get:
            mock_get.side_effect = Exception("Network error")

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

            assert result == []

    def test_returns_empty_on_connection_error(self):
        """Returns empty list on connection error."""
        import requests

        with patch("local_deep_research.security.safe_get") as mock_get:
            mock_get.side_effect = requests.exceptions.ConnectionError()

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

            assert result == []

    def test_returns_empty_on_timeout(self):
        """Returns empty list on timeout."""
        import requests

        with patch("local_deep_research.security.safe_get") as mock_get:
            mock_get.side_effect = requests.exceptions.Timeout()

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

            assert result == []

    def test_handles_empty_models_list(self):
        """Handles response with empty models list."""
        mock_response = Mock()
        mock_response.status_code = 200
        mock_response.json.return_value = {"models": []}

        with patch("local_deep_research.security.safe_get") as mock_get:
            mock_get.return_value = mock_response

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

            assert result == []

    def test_handles_missing_models_key(self):
        """Handles response missing 'models' key."""
        mock_response = Mock()
        mock_response.status_code = 200
        mock_response.json.return_value = {}  # No 'models' key

        with patch("local_deep_research.security.safe_get") as mock_get:
            mock_get.return_value = mock_response

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

            assert result == []

    def test_handles_model_missing_supported_methods(self):
        """Handles model without supportedGenerationMethods."""
        mock_response = Mock()
        mock_response.status_code = 200
        mock_response.json.return_value = {
            "models": [
                {
                    "name": "models/gemini-1.5-flash",
                    # Missing supportedGenerationMethods
                },
            ]
        }

        with patch("local_deep_research.security.safe_get") as mock_get:
            mock_get.return_value = mock_response

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

            # Model without supportedGenerationMethods should be skipped
            assert result == []

    def test_handles_model_missing_name(self):
        """Handles model without name field."""
        mock_response = Mock()
        mock_response.status_code = 200
        mock_response.json.return_value = {
            "models": [
                {
                    "supportedGenerationMethods": ["generateContent"],
                    # Missing 'name'
                },
            ]
        }

        with patch("local_deep_research.security.safe_get") as mock_get:
            mock_get.return_value = mock_response

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

            # Model without name should be skipped
            assert result == []

    def test_ignores_base_url_parameter(self):
        """Ignores base_url parameter (uses fixed Google endpoint)."""
        mock_response = Mock()
        mock_response.status_code = 200
        mock_response.json.return_value = {"models": []}

        with patch("local_deep_research.security.safe_get") as mock_get:
            mock_get.return_value = mock_response

            # Pass a custom base_url - should be ignored
            GoogleProvider.list_models_for_api(
                api_key="test-key", base_url="https://custom-endpoint.com"
            )

            call_args = mock_get.call_args[0]
            url = call_args[0]
            # Should still use Google's native API
            assert "generativelanguage.googleapis.com" in url

    def test_uses_10_second_timeout(self):
        """Uses 10 second timeout for API call."""
        mock_response = Mock()
        mock_response.status_code = 200
        mock_response.json.return_value = {"models": []}

        with patch("local_deep_research.security.safe_get") as mock_get:
            mock_get.return_value = mock_response

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

            call_kwargs = mock_get.call_args[1]
            assert call_kwargs["timeout"] == 10


class TestGoogleCreateLLM:
    """Tests for create_llm method (inherited from base with Google settings)."""

    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:
                GoogleProvider.create_llm()

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

    def test_create_llm_with_valid_api_key(self):
        """Successfully creates ChatOpenAI instance with valid API key."""

        def mock_get_setting_side_effect(key, default=None, *args, **kwargs):
            settings_map = {
                "llm.google.api_key": "test-google-key",
                "llm.max_tokens": None,
                "llm.streaming": None,
                "llm.max_retries": None,
                "llm.request_timeout": None,
            }
            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:
                mock_llm = Mock()
                mock_chat.return_value = mock_llm

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

                assert result is mock_llm
                mock_chat.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.google.api_key": "test-key",
                "llm.max_tokens": None,
                "llm.streaming": None,
                "llm.max_retries": None,
                "llm.request_timeout": None,
            }
            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"):
                GoogleProvider.create_llm()

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

        def mock_get_setting_side_effect(key, default=None, *args, **kwargs):
            settings_map = {
                "llm.google.api_key": "test-key",
                "llm.max_tokens": None,
                "llm.streaming": None,
                "llm.max_retries": None,
                "llm.request_timeout": None,
            }
            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:
                GoogleProvider.create_llm(model_name="gemini-1.5-pro")

                call_kwargs = mock_chat.call_args[1]
                assert call_kwargs["model"] == "gemini-1.5-pro"

    def test_create_llm_uses_google_base_url(self):
        """Uses Google's OpenAI-compatible endpoint as base URL."""

        def mock_get_setting_side_effect(key, default=None, *args, **kwargs):
            settings_map = {
                "llm.google.api_key": "test-key",
                "llm.max_tokens": None,
                "llm.streaming": None,
                "llm.max_retries": None,
                "llm.request_timeout": None,
            }
            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:
                with patch(
                    "local_deep_research.llm.providers.openai_base.normalize_url",
                    side_effect=lambda x: x,
                ):
                    GoogleProvider.create_llm(model_name="test-model")

                    call_kwargs = mock_chat.call_args[1]
                    assert (
                        "generativelanguage.googleapis.com"
                        in call_kwargs["base_url"]
                    )

    def test_create_llm_passes_temperature(self):
        """Passes temperature parameter."""

        def mock_get_setting_side_effect(key, default=None, *args, **kwargs):
            settings_map = {
                "llm.google.api_key": "test-key",
                "llm.max_tokens": None,
                "llm.streaming": None,
                "llm.max_retries": None,
                "llm.request_timeout": None,
            }
            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:
                GoogleProvider.create_llm(
                    model_name="test-model", temperature=0.3
                )

                call_kwargs = mock_chat.call_args[1]
                assert call_kwargs["temperature"] == 0.3


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

    def test_is_available_true_when_key_exists(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-google-key"

            result = GoogleProvider.is_available()
            assert result is True

    def test_is_available_false_when_no_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 = GoogleProvider.is_available()
            assert result is False

    def test_is_available_false_when_empty_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 = GoogleProvider.is_available()
            assert result is False

    def test_is_available_false_on_exception(self):
        """Returns False when exception occurs."""
        with patch(
            "local_deep_research.config.thread_settings.get_setting_from_snapshot"
        ) as mock_get_setting:
            mock_get_setting.side_effect = Exception("Settings error")

            result = GoogleProvider.is_available()
            assert result is False
