"""
Tests for Ollama LLM provider.
"""

import pytest
from unittest.mock import Mock, patch
import requests

from local_deep_research.llm.providers.implementations.ollama import (
    OllamaProvider,
)


class TestOllamaProviderMetadata:
    """Tests for OllamaProvider class metadata."""

    def test_provider_name(self):
        """Provider name is correct."""
        assert OllamaProvider.provider_name == "Ollama"

    def test_provider_key(self):
        """Provider key is correct."""
        assert OllamaProvider.provider_key == "OLLAMA"

    def test_is_not_cloud(self):
        """Ollama is a local provider."""
        assert OllamaProvider.is_cloud is False

    def test_default_model_is_empty(self):
        """Default model is empty by design — users must explicitly pick
        one. Mirrors the API-key-not-configured pattern; we don't silently
        download a multi-GB binary the user never asked for."""
        assert OllamaProvider.default_model == ""


class TestOllamaGetAuthHeaders:
    """Tests for _get_auth_headers method."""

    def test_no_auth_headers_without_key(self):
        """Returns empty headers when no API key."""
        headers = OllamaProvider._get_auth_headers()
        assert headers == {}

    def test_auth_headers_with_key(self):
        """Returns Bearer token when API key provided."""
        headers = OllamaProvider._get_auth_headers(api_key="test-key")
        assert "Authorization" in headers
        assert headers["Authorization"] == "Bearer test-key"

    def test_auth_headers_from_settings(self):
        """Gets API key from settings snapshot."""
        with patch(
            "local_deep_research.llm.providers.implementations.ollama.get_setting_from_snapshot"
        ) as mock_get_setting:
            mock_get_setting.return_value = "settings-key"

            headers = OllamaProvider._get_auth_headers(
                settings_snapshot={"llm.ollama.api_key": "settings-key"}
            )

            assert "Authorization" in headers
            assert headers["Authorization"] == "Bearer settings-key"


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

    def test_not_available_without_url(self):
        """Returns False when URL not configured."""
        with patch(
            "local_deep_research.llm.providers.implementations.ollama.get_setting_from_snapshot"
        ) as mock_get_setting:
            mock_get_setting.return_value = None

            result = OllamaProvider.is_available()
            assert result is False

    def test_available_with_working_server(self):
        """Returns True when Ollama server responds."""
        with patch(
            "local_deep_research.llm.providers.implementations.ollama.get_setting_from_snapshot"
        ) as mock_get_setting:
            mock_get_setting.return_value = "http://localhost:11434"

            with patch("requests.get") as mock_get:
                mock_response = Mock()
                mock_response.status_code = 200
                mock_response.text = '{"models":[]}'
                mock_get.return_value = mock_response

                result = OllamaProvider.is_available()
                assert result is True

    def test_not_available_with_error_response(self):
        """Returns False when server returns error."""
        with patch(
            "local_deep_research.llm.providers.implementations.ollama.get_setting_from_snapshot"
        ) as mock_get_setting:
            mock_get_setting.return_value = "http://localhost:11434"

            with patch("requests.get") as mock_get:
                mock_response = Mock()
                mock_response.status_code = 500
                mock_get.return_value = mock_response

                result = OllamaProvider.is_available()
                assert result is False

    def test_not_available_with_connection_error(self):
        """Returns False when connection fails."""
        with patch(
            "local_deep_research.llm.providers.implementations.ollama.get_setting_from_snapshot"
        ) as mock_get_setting:
            mock_get_setting.return_value = "http://localhost:11434"

            with patch("requests.get") as mock_get:
                mock_get.side_effect = requests.exceptions.ConnectionError()

                result = OllamaProvider.is_available()
                assert result is False

    def test_not_available_with_timeout(self):
        """Returns False when request times out."""
        with patch(
            "local_deep_research.llm.providers.implementations.ollama.get_setting_from_snapshot"
        ) as mock_get_setting:
            mock_get_setting.return_value = "http://localhost:11434"

            with patch("requests.get") as mock_get:
                mock_get.side_effect = requests.exceptions.Timeout()

                result = OllamaProvider.is_available()
                assert result is False


class TestOllamaListModels:
    """Tests for list_models_for_api method."""

    def test_list_models_returns_list(self, mock_ollama_response):
        """Returns list of models."""
        with patch(
            "local_deep_research.utilities.llm_utils.fetch_ollama_models"
        ) as mock_fetch:
            mock_fetch.return_value = [
                {"value": "llama2:latest", "label": "llama2 (Ollama)"},
                {"value": "gemma3:12b", "label": "gemma3 12b (Ollama)"},
            ]

            # Pass base_url directly - this is the correct API usage
            result = OllamaProvider.list_models_for_api(
                base_url="http://localhost:11434"
            )

            assert isinstance(result, list)
            assert len(result) == 2

    def test_list_models_empty_without_url(self):
        """Returns empty list when URL not provided."""
        # No base_url passed - should return empty list
        result = OllamaProvider.list_models_for_api()
        assert result == []

    def test_list_models_includes_provider_info(self, mock_ollama_response):
        """Model entries include provider information."""
        with patch(
            "local_deep_research.utilities.llm_utils.fetch_ollama_models"
        ) as mock_fetch:
            mock_fetch.return_value = [
                {"value": "llama2:latest", "label": "llama2"},
            ]

            # Pass base_url directly - this is the correct API usage
            result = OllamaProvider.list_models_for_api(
                base_url="http://localhost:11434"
            )

            assert len(result) > 0
            assert "provider" in result[0]
            assert result[0]["provider"] == "OLLAMA"


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

    def test_create_llm_raises_without_model(self):
        """Raises ValueError when no model name is provided."""
        with patch(
            "local_deep_research.llm.providers.implementations.ollama.get_setting_from_snapshot"
        ) as mock_get_setting:
            mock_get_setting.return_value = "http://localhost:11434"

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

            assert "model not configured" in str(exc_info.value).lower()

    def test_create_llm_raises_without_url(self):
        """Raises ValueError when URL not configured (model is provided)."""
        with patch(
            "local_deep_research.llm.providers.implementations.ollama.get_setting_from_snapshot"
        ) as mock_get_setting:
            mock_get_setting.return_value = None

            with pytest.raises(ValueError) as exc_info:
                OllamaProvider.create_llm(model_name="llama3.1:8b")

            assert "url not configured" in str(exc_info.value).lower()

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

        def mock_get_setting_side_effect(key, default=None, *args, **kwargs):
            settings_map = {
                "llm.ollama.url": "http://localhost:11434",
                "llm.local_context_window_size": 8192,
                "llm.supports_max_tokens": True,
                "llm.max_tokens": 4096,
            }
            return settings_map.get(key, default)

        with patch(
            "local_deep_research.llm.providers.implementations.ollama.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.implementations.ollama.ChatOllama"
            ) as mock_chat_ollama:
                mock_llm = Mock()
                mock_chat_ollama.return_value = mock_llm

                result = OllamaProvider.create_llm(model_name="llama3.1:8b")

                assert result is mock_llm
                mock_chat_ollama.assert_called_once()

    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.ollama.url": "http://localhost:11434",
                "llm.local_context_window_size": 8192,
                "llm.supports_max_tokens": True,
                "llm.max_tokens": 4096,
            }
            return settings_map.get(key, default)

        with patch(
            "local_deep_research.llm.providers.implementations.ollama.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.implementations.ollama.ChatOllama"
            ) as mock_chat_ollama:
                OllamaProvider.create_llm(
                    model_name="llama3.1:8b", temperature=0.5
                )

                call_kwargs = mock_chat_ollama.call_args[1]
                assert call_kwargs["temperature"] == 0.5


class TestOllamaEnableThinking:
    """Tests for the enable_thinking → reasoning kwarg port.

    Previously this setting was only honored in the dead procedural
    code in llm_config.get_llm("ollama"). After the collapse it lives
    in OllamaProvider.create_llm so the live registered-LLM path
    actually respects llm.ollama.enable_thinking.
    """

    @staticmethod
    def _settings(enable_thinking_value):
        def side_effect(key, default=None, *args, **kwargs):
            return {
                "llm.ollama.url": "http://localhost:11434",
                "llm.local_context_window_size": 8192,
                "llm.supports_max_tokens": True,
                "llm.max_tokens": 4096,
                "llm.ollama.enable_thinking": enable_thinking_value,
            }.get(key, default)

        return side_effect

    def test_enable_thinking_true_sets_reasoning_true(self):
        with patch(
            "local_deep_research.llm.providers.implementations.ollama.get_setting_from_snapshot",
            side_effect=self._settings(True),
        ):
            with patch(
                "local_deep_research.llm.providers.implementations.ollama.ChatOllama"
            ) as mock_chat:
                OllamaProvider.create_llm(model_name="deepseek-r1:14b")
                assert mock_chat.call_args[1]["reasoning"] is True

    def test_enable_thinking_false_sets_reasoning_false(self):
        with patch(
            "local_deep_research.llm.providers.implementations.ollama.get_setting_from_snapshot",
            side_effect=self._settings(False),
        ):
            with patch(
                "local_deep_research.llm.providers.implementations.ollama.ChatOllama"
            ) as mock_chat:
                OllamaProvider.create_llm(model_name="deepseek-r1:14b")
                assert mock_chat.call_args[1]["reasoning"] is False

    def test_non_bool_enable_thinking_omits_reasoning_kwarg(self):
        """Non-bool values (None, strings, etc.) are ignored — defensive."""
        with patch(
            "local_deep_research.llm.providers.implementations.ollama.get_setting_from_snapshot",
            side_effect=self._settings("not-a-bool"),
        ):
            with patch(
                "local_deep_research.llm.providers.implementations.ollama.ChatOllama"
            ) as mock_chat:
                OllamaProvider.create_llm(model_name="llama3.1:8b")
                assert "reasoning" not in mock_chat.call_args[1]

    def test_default_when_setting_absent_resolves_to_true(self):
        """Setting is absent from snapshot → uses get_setting_from_snapshot's
        ``True`` default → reasoning=True is passed.

        This exercises the most common production path: a fresh install
        where the user hasn't touched ``llm.ollama.enable_thinking`` and
        the setting comes from default_settings.json (which ships True).
        """

        def side_effect(key, default=None, *args, **kwargs):
            base = {
                "llm.ollama.url": "http://localhost:11434",
                "llm.local_context_window_size": 8192,
                "llm.supports_max_tokens": True,
                "llm.max_tokens": 4096,
            }
            # Note: llm.ollama.enable_thinking deliberately absent — the
            # caller's `default=True` should win.
            return base.get(key, default)

        with patch(
            "local_deep_research.llm.providers.implementations.ollama.get_setting_from_snapshot",
            side_effect=side_effect,
        ):
            with patch(
                "local_deep_research.llm.providers.implementations.ollama.ChatOllama"
            ) as mock_chat:
                OllamaProvider.create_llm(model_name="deepseek-r1:14b")
                # default=True is the fallback; isinstance(True, bool) → kwarg set
                assert mock_chat.call_args[1].get("reasoning") is True
