"""
Tests for Ollama embedding provider.
"""

from unittest.mock import Mock, patch

import requests

from local_deep_research.embeddings.providers.implementations.ollama import (
    OllamaEmbeddingsProvider,
    _name_looks_like_embedding,
)


# ── helpers ──────────────────────────────────────────────────────────────
_OLLAMA_MODULE = (
    "local_deep_research.embeddings.providers.implementations.ollama"
)
_LLM_UTILS = "local_deep_research.utilities.llm_utils"


def _mock_capabilities(caps_by_model):
    """Return a side_effect function for _get_model_capabilities.

    ``caps_by_model`` maps model names to capability lists
    (e.g. {"nomic-embed-text:latest": ["embedding"]}).
    """

    def _side_effect(_base_url, model_name):
        return caps_by_model.get(model_name)

    return _side_effect


# ── metadata ─────────────────────────────────────────────────────────────


class TestOllamaEmbeddingsProviderMetadata:
    """Tests for OllamaEmbeddingsProvider class metadata."""

    def test_provider_name(self):
        """Provider name is 'Ollama'."""
        assert OllamaEmbeddingsProvider.provider_name == "Ollama"

    def test_provider_key(self):
        """Provider key is 'OLLAMA'."""
        assert OllamaEmbeddingsProvider.provider_key == "OLLAMA"

    def test_requires_api_key_false(self):
        """Does not require API key."""
        assert OllamaEmbeddingsProvider.requires_api_key is False

    def test_supports_local_true(self):
        """Supports local execution."""
        assert OllamaEmbeddingsProvider.supports_local is True

    def test_default_model(self):
        """Has a default embedding model."""
        assert OllamaEmbeddingsProvider.default_model == "nomic-embed-text"


# ── is_available ─────────────────────────────────────────────────────────


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

    def test_available_when_server_responds(self):
        """Returns True when Ollama server responds."""
        with patch(f"{_OLLAMA_MODULE}.get_ollama_base_url") as mock_get_url:
            mock_get_url.return_value = "http://localhost:11434"

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

                result = OllamaEmbeddingsProvider.is_available()
                assert result is True

    def test_not_available_when_server_error(self):
        """Returns False when server returns error."""
        with patch(f"{_OLLAMA_MODULE}.get_ollama_base_url") as mock_get_url:
            mock_get_url.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 = OllamaEmbeddingsProvider.is_available()
                assert result is False

    def test_not_available_when_connection_fails(self):
        """Returns False when connection fails."""
        with patch(f"{_OLLAMA_MODULE}.get_ollama_base_url") as mock_get_url:
            mock_get_url.return_value = "http://localhost:11434"

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

                result = OllamaEmbeddingsProvider.is_available()
                assert result is False

    def test_not_available_when_timeout(self):
        """Returns False when request times out."""
        with patch(f"{_OLLAMA_MODULE}.get_ollama_base_url") as mock_get_url:
            mock_get_url.return_value = "http://localhost:11434"

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

                result = OllamaEmbeddingsProvider.is_available()
                assert result is False


# ── create_embeddings ────────────────────────────────────────────────────


class TestOllamaEmbeddingsCreate:
    """Tests for create_embeddings method."""

    def test_create_with_default_model(self):
        """Creates embeddings with default model."""
        with patch(
            f"{_OLLAMA_MODULE}.get_setting_from_snapshot"
        ) as mock_get_setting:
            mock_get_setting.return_value = "nomic-embed-text"

            with patch(f"{_OLLAMA_MODULE}.get_ollama_base_url") as mock_get_url:
                mock_get_url.return_value = "http://localhost:11434"

                with patch(f"{_OLLAMA_MODULE}.OllamaEmbeddings") as mock_ollama:
                    mock_instance = Mock()
                    mock_ollama.return_value = mock_instance

                    result = OllamaEmbeddingsProvider.create_embeddings()

                    assert result is mock_instance
                    mock_ollama.assert_called_once()
                    call_kwargs = mock_ollama.call_args[1]
                    assert call_kwargs["model"] == "nomic-embed-text"

    def test_create_with_custom_model(self):
        """Creates embeddings with custom model."""
        with patch(f"{_OLLAMA_MODULE}.get_ollama_base_url") as mock_get_url:
            mock_get_url.return_value = "http://localhost:11434"

            with patch(f"{_OLLAMA_MODULE}.OllamaEmbeddings") as mock_ollama:
                mock_instance = Mock()
                mock_ollama.return_value = mock_instance

                OllamaEmbeddingsProvider.create_embeddings(
                    model="mxbai-embed-large"
                )

                call_kwargs = mock_ollama.call_args[1]
                assert call_kwargs["model"] == "mxbai-embed-large"

    def test_create_with_custom_base_url(self):
        """Creates embeddings with custom base URL."""
        with patch(f"{_OLLAMA_MODULE}.OllamaEmbeddings") as mock_ollama:
            mock_instance = Mock()
            mock_ollama.return_value = mock_instance

            OllamaEmbeddingsProvider.create_embeddings(
                model="nomic-embed-text",
                base_url="http://custom:8080",
            )

            call_kwargs = mock_ollama.call_args[1]
            assert call_kwargs["base_url"] == "http://custom:8080"

    def test_create_uses_settings_snapshot(self):
        """Uses settings snapshot when provided."""
        mock_settings = {"embeddings.ollama.model": "custom-model"}

        with patch(
            f"{_OLLAMA_MODULE}.get_setting_from_snapshot"
        ) as mock_get_setting:
            mock_get_setting.return_value = "custom-model"

            with patch(f"{_OLLAMA_MODULE}.get_ollama_base_url") as mock_get_url:
                mock_get_url.return_value = "http://localhost:11434"

                with patch(f"{_OLLAMA_MODULE}.OllamaEmbeddings"):
                    OllamaEmbeddingsProvider.create_embeddings(
                        settings_snapshot=mock_settings
                    )

                    # Verify get_setting_from_snapshot was called with settings
                    mock_get_setting.assert_called()


# ── _get_model_capabilities ─────────────────────────────────────────────


class TestGetModelCapabilities:
    """Tests for _get_model_capabilities private helper."""

    def test_returns_capabilities_on_success(self):
        """Returns capability list from /api/show response."""
        mock_resp = Mock()
        mock_resp.status_code = 200
        mock_resp.json.return_value = {"capabilities": ["embedding"]}

        with patch(f"{_OLLAMA_MODULE}.safe_post", return_value=mock_resp):
            caps = OllamaEmbeddingsProvider._get_model_capabilities(
                "http://localhost:11434", "nomic-embed-text"
            )
            assert caps == ["embedding"]

    def test_returns_none_on_http_error(self):
        """Returns None when server returns non-200."""
        mock_resp = Mock()
        mock_resp.status_code = 404

        with patch(f"{_OLLAMA_MODULE}.safe_post", return_value=mock_resp):
            caps = OllamaEmbeddingsProvider._get_model_capabilities(
                "http://localhost:11434", "nonexistent-model"
            )
            assert caps is None

    def test_returns_none_on_exception(self):
        """Returns None when request raises exception."""
        with patch(
            f"{_OLLAMA_MODULE}.safe_post",
            side_effect=Exception("connection refused"),
        ):
            caps = OllamaEmbeddingsProvider._get_model_capabilities(
                "http://localhost:11434", "nomic-embed-text"
            )
            assert caps is None

    def test_returns_none_when_capabilities_missing(self):
        """Returns None when response lacks capabilities field."""
        mock_resp = Mock()
        mock_resp.status_code = 200
        mock_resp.json.return_value = {"details": {"family": "nomic-bert"}}

        with patch(f"{_OLLAMA_MODULE}.safe_post", return_value=mock_resp):
            caps = OllamaEmbeddingsProvider._get_model_capabilities(
                "http://localhost:11434", "nomic-embed-text"
            )
            assert caps is None

    def test_returns_completion_for_llm(self):
        """Returns completion capabilities for LLM models."""
        mock_resp = Mock()
        mock_resp.status_code = 200
        mock_resp.json.return_value = {"capabilities": ["completion", "tools"]}

        with patch(f"{_OLLAMA_MODULE}.safe_post", return_value=mock_resp):
            caps = OllamaEmbeddingsProvider._get_model_capabilities(
                "http://localhost:11434", "qwen3:4b"
            )
            assert caps == ["completion", "tools"]


# ── is_embedding_model ───────────────────────────────────────────────────


class TestIsEmbeddingModel:
    """Tests for is_embedding_model method."""

    def test_returns_true_for_embedding_model(self):
        """Returns True when capabilities include 'embedding'."""
        with patch(
            f"{_OLLAMA_MODULE}.get_ollama_base_url",
            return_value="http://localhost:11434",
        ):
            with patch.object(
                OllamaEmbeddingsProvider,
                "_get_model_capabilities",
                return_value=["embedding"],
            ):
                result = OllamaEmbeddingsProvider.is_embedding_model(
                    "nomic-embed-text"
                )
                assert result is True

    def test_returns_false_for_llm_model(self):
        """Returns False when capabilities don't include 'embedding'."""
        with patch(
            f"{_OLLAMA_MODULE}.get_ollama_base_url",
            return_value="http://localhost:11434",
        ):
            with patch.object(
                OllamaEmbeddingsProvider,
                "_get_model_capabilities",
                return_value=["completion", "tools"],
            ):
                result = OllamaEmbeddingsProvider.is_embedding_model("qwen3:4b")
                assert result is False

    def test_falls_back_to_name_heuristic_true(self):
        """Falls back to name heuristic when capabilities unavailable."""
        with patch(
            f"{_OLLAMA_MODULE}.get_ollama_base_url",
            return_value="http://localhost:11434",
        ):
            with patch.object(
                OllamaEmbeddingsProvider,
                "_get_model_capabilities",
                return_value=None,
            ):
                # "embed" in name → True
                result = OllamaEmbeddingsProvider.is_embedding_model(
                    "nomic-embed-text"
                )
                assert result is True

    def test_falls_back_to_name_heuristic_false(self):
        """Falls back to name heuristic and returns False for LLM name."""
        with patch(
            f"{_OLLAMA_MODULE}.get_ollama_base_url",
            return_value="http://localhost:11434",
        ):
            with patch.object(
                OllamaEmbeddingsProvider,
                "_get_model_capabilities",
                return_value=None,
            ):
                result = OllamaEmbeddingsProvider.is_embedding_model(
                    "deepseek-r1:32b"
                )
                assert result is False


# ── _name_looks_like_embedding ───────────────────────────────────────────


class TestNameLooksLikeEmbedding:
    """Tests for _name_looks_like_embedding heuristic."""

    def test_embed_in_name(self):
        assert _name_looks_like_embedding("nomic-embed-text:latest") is True

    def test_embed_prefix(self):
        assert _name_looks_like_embedding("mxbai-embed-large") is True

    def test_bge_in_name(self):
        assert _name_looks_like_embedding("bge-m3:latest") is True

    def test_llm_name_rejected(self):
        assert _name_looks_like_embedding("qwen3:4b") is False

    def test_deepseek_rejected(self):
        assert _name_looks_like_embedding("deepseek-r1:32b") is False

    def test_case_insensitive(self):
        assert _name_looks_like_embedding("BGE-Large-EN") is True

    def test_empty_string(self):
        assert _name_looks_like_embedding("") is False


# ── get_available_models ─────────────────────────────────────────────────


class TestOllamaEmbeddingsGetAvailableModels:
    """Tests for get_available_models method."""

    def _setup_mocks(self, all_models, caps_by_model):
        """Helper: patch base_url, fetch_ollama_models, and capabilities."""
        return (
            patch(
                f"{_OLLAMA_MODULE}.get_ollama_base_url",
                return_value="http://localhost:11434",
            ),
            patch(
                f"{_LLM_UTILS}.fetch_ollama_models",
                return_value=all_models,
            ),
            patch.object(
                OllamaEmbeddingsProvider,
                "_get_model_capabilities",
                side_effect=_mock_capabilities(caps_by_model),
            ),
        )

    def test_embedding_models_sorted_first(self):
        """Embedding models appear before LLM models."""
        all_models = [
            {"value": "qwen3:4b", "label": "qwen3:4b"},
            {
                "value": "nomic-embed-text:latest",
                "label": "nomic-embed-text:latest",
            },
            {"value": "deepseek-r1:32b", "label": "deepseek-r1:32b"},
        ]
        caps = {
            "qwen3:4b": ["completion", "tools"],
            "nomic-embed-text:latest": ["embedding"],
            "deepseek-r1:32b": ["completion"],
        }

        p1, p2, p3 = self._setup_mocks(all_models, caps)
        with p1, p2, p3:
            result = OllamaEmbeddingsProvider.get_available_models()

        assert result[0]["value"] == "nomic-embed-text:latest"
        assert result[0]["is_embedding"] is True

    def test_all_models_returned_with_flag(self):
        """All models are returned, each with an is_embedding flag."""
        all_models = [
            {
                "value": "nomic-embed-text:latest",
                "label": "nomic-embed-text:latest",
            },
            {"value": "qwen3:4b", "label": "qwen3:4b"},
        ]
        caps = {
            "nomic-embed-text:latest": ["embedding"],
            "qwen3:4b": ["completion"],
        }

        p1, p2, p3 = self._setup_mocks(all_models, caps)
        with p1, p2, p3:
            result = OllamaEmbeddingsProvider.get_available_models()

        assert len(result) == 2
        assert all("is_embedding" in m for m in result)

        embed_models = [m for m in result if m["is_embedding"]]
        llm_models = [m for m in result if not m["is_embedding"]]
        assert len(embed_models) == 1
        assert len(llm_models) == 1

    def test_empty_model_list(self):
        """Returns empty list when no models available."""
        p1, p2, p3 = self._setup_mocks([], {})
        with p1, p2, p3:
            result = OllamaEmbeddingsProvider.get_available_models()

        assert result == []

    def test_all_llms_still_returned(self):
        """Even if no embedding models exist, all LLMs are returned."""
        all_models = [
            {"value": "qwen3:4b", "label": "qwen3:4b"},
            {"value": "deepseek-r1:32b", "label": "deepseek-r1:32b"},
        ]
        caps = {
            "qwen3:4b": ["completion"],
            "deepseek-r1:32b": ["completion"],
        }

        p1, p2, p3 = self._setup_mocks(all_models, caps)
        with p1, p2, p3:
            result = OllamaEmbeddingsProvider.get_available_models()

        assert len(result) == 2
        assert all(m["is_embedding"] is False for m in result)

    def test_fallback_to_name_heuristic_when_capabilities_unavailable(self):
        """Uses name heuristic when /api/show returns no capabilities."""
        all_models = [
            {
                "value": "nomic-embed-text:latest",
                "label": "nomic-embed-text:latest",
            },
            {"value": "qwen3:4b", "label": "qwen3:4b"},
        ]
        # Return None for all models → no capabilities available
        caps = {}

        p1, p2, p3 = self._setup_mocks(all_models, caps)
        with p1, p2, p3:
            result = OllamaEmbeddingsProvider.get_available_models()

        assert len(result) == 2
        embed = [m for m in result if m["is_embedding"]]
        assert len(embed) == 1
        assert embed[0]["value"] == "nomic-embed-text:latest"

    def test_multiple_embedding_models(self):
        """Multiple embedding models are all sorted first."""
        all_models = [
            {"value": "qwen3:4b", "label": "qwen3:4b"},
            {
                "value": "nomic-embed-text:latest",
                "label": "nomic-embed-text:latest",
            },
            {
                "value": "mxbai-embed-large:latest",
                "label": "mxbai-embed-large:latest",
            },
            {"value": "deepseek-r1:32b", "label": "deepseek-r1:32b"},
        ]
        caps = {
            "qwen3:4b": ["completion"],
            "nomic-embed-text:latest": ["embedding"],
            "mxbai-embed-large:latest": ["embedding"],
            "deepseek-r1:32b": ["completion"],
        }

        p1, p2, p3 = self._setup_mocks(all_models, caps)
        with p1, p2, p3:
            result = OllamaEmbeddingsProvider.get_available_models()

        # First two should be embedding models
        assert result[0]["is_embedding"] is True
        assert result[1]["is_embedding"] is True
        assert result[2]["is_embedding"] is False
        assert result[3]["is_embedding"] is False

    def test_result_preserves_value_and_label(self):
        """Original value and label fields are preserved."""
        all_models = [
            {
                "value": "nomic-embed-text:latest",
                "label": "nomic-embed-text:latest",
            },
        ]
        caps = {"nomic-embed-text:latest": ["embedding"]}

        p1, p2, p3 = self._setup_mocks(all_models, caps)
        with p1, p2, p3:
            result = OllamaEmbeddingsProvider.get_available_models()

        assert result[0]["value"] == "nomic-embed-text:latest"
        assert result[0]["label"] == "nomic-embed-text:latest"
        assert result[0]["is_embedding"] is True


# ── provider info & config ───────────────────────────────────────────────


class TestOllamaEmbeddingsProviderInfo:
    """Tests for get_provider_info method."""

    def test_provider_info_structure(self):
        """get_provider_info returns expected structure."""
        info = OllamaEmbeddingsProvider.get_provider_info()

        assert "name" in info
        assert "key" in info
        assert "requires_api_key" in info
        assert "supports_local" in info
        assert "default_model" in info

        assert info["name"] == "Ollama"
        assert info["key"] == "OLLAMA"
        assert info["requires_api_key"] is False
        assert info["supports_local"] is True


class TestOllamaEmbeddingsValidateConfig:
    """Tests for validate_config method."""

    def test_validate_config_when_available(self):
        """validate_config returns True when available."""
        with patch.object(
            OllamaEmbeddingsProvider, "is_available", return_value=True
        ):
            is_valid, error = OllamaEmbeddingsProvider.validate_config()
            assert is_valid is True
            assert error is None

    def test_validate_config_when_not_available(self):
        """validate_config returns False when not available."""
        with patch.object(
            OllamaEmbeddingsProvider, "is_available", return_value=False
        ):
            is_valid, error = OllamaEmbeddingsProvider.validate_config()
            assert is_valid is False
            assert error is not None
            assert "not available" in error
