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

from unittest.mock import Mock, patch

import requests

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


# ── 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 ────────────────────────────────────────────────────


def _make_settings_side_effect(values):
    """Build a side_effect for get_setting_from_snapshot that dispatches by key.

    ``values`` maps setting keys (e.g. "embeddings.ollama.model") to the value
    that lookup should return. Keys absent from ``values`` fall back to the
    caller-provided ``default`` argument.
    """

    def _side_effect(key, default=None, **_kwargs):
        return values.get(key, default)

    return _side_effect


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.side_effect = _make_settings_side_effect(
                {
                    "embeddings.ollama.model": "nomic-embed-text",
                    "embeddings.ollama.num_ctx": 8192,
                }
            )

            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_setting_from_snapshot"
        ) as mock_get_setting:
            mock_get_setting.side_effect = _make_settings_side_effect(
                {"embeddings.ollama.num_ctx": 8192}
            )

            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}.get_setting_from_snapshot"
        ) as mock_get_setting:
            mock_get_setting.side_effect = _make_settings_side_effect(
                {"embeddings.ollama.num_ctx": 8192}
            )

            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.side_effect = _make_settings_side_effect(
                {
                    "embeddings.ollama.model": "custom-model",
                    "embeddings.ollama.num_ctx": 8192,
                }
            )

            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()

    def test_create_passes_num_ctx_when_set(self):
        """Forwards num_ctx to OllamaEmbeddings when configured."""
        with patch(
            f"{_OLLAMA_MODULE}.get_setting_from_snapshot"
        ) as mock_get_setting:
            mock_get_setting.side_effect = _make_settings_side_effect(
                {
                    "embeddings.ollama.model": "nomic-embed-text",
                    "embeddings.ollama.num_ctx": 8192,
                }
            )

            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:
                    OllamaEmbeddingsProvider.create_embeddings()

                    call_kwargs = mock_ollama.call_args[1]
                    assert call_kwargs["num_ctx"] == 8192

    def test_create_omits_num_ctx_when_unset(self):
        """Does not pass num_ctx when the setting is None."""
        with patch(
            f"{_OLLAMA_MODULE}.get_setting_from_snapshot"
        ) as mock_get_setting:
            mock_get_setting.side_effect = _make_settings_side_effect(
                {
                    "embeddings.ollama.model": "nomic-embed-text",
                    "embeddings.ollama.num_ctx": None,
                }
            )

            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:
                    OllamaEmbeddingsProvider.create_embeddings()

                    call_kwargs = mock_ollama.call_args[1]
                    assert "num_ctx" not in call_kwargs

    def test_create_passes_custom_num_ctx(self):
        """Forwards a non-default num_ctx (proves the value is pass-through, not hardcoded)."""
        with patch(
            f"{_OLLAMA_MODULE}.get_setting_from_snapshot"
        ) as mock_get_setting:
            mock_get_setting.side_effect = _make_settings_side_effect(
                {
                    "embeddings.ollama.model": "nomic-embed-text",
                    "embeddings.ollama.num_ctx": 16384,
                }
            )

            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:
                    OllamaEmbeddingsProvider.create_embeddings()

                    call_kwargs = mock_ollama.call_args[1]
                    assert call_kwargs["num_ctx"] == 16384

    def test_create_coerces_string_num_ctx_to_int(self):
        """Coerces string num_ctx to int (settings storage may return strings)."""
        with patch(
            f"{_OLLAMA_MODULE}.get_setting_from_snapshot"
        ) as mock_get_setting:
            mock_get_setting.side_effect = _make_settings_side_effect(
                {
                    "embeddings.ollama.model": "nomic-embed-text",
                    "embeddings.ollama.num_ctx": "8192",
                }
            )

            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:
                    OllamaEmbeddingsProvider.create_embeddings()

                    call_kwargs = mock_ollama.call_args[1]
                    assert call_kwargs["num_ctx"] == 8192
                    assert isinstance(call_kwargs["num_ctx"], int)

    def test_create_coerces_float_num_ctx_to_int(self):
        """Coerces float num_ctx to int (JSON deserialization may yield floats)."""
        with patch(
            f"{_OLLAMA_MODULE}.get_setting_from_snapshot"
        ) as mock_get_setting:
            mock_get_setting.side_effect = _make_settings_side_effect(
                {
                    "embeddings.ollama.model": "nomic-embed-text",
                    "embeddings.ollama.num_ctx": 8192.0,
                }
            )

            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:
                    OllamaEmbeddingsProvider.create_embeddings()

                    call_kwargs = mock_ollama.call_args[1]
                    assert call_kwargs["num_ctx"] == 8192
                    assert isinstance(call_kwargs["num_ctx"], int)

    def test_create_combines_custom_model_and_num_ctx(self):
        """Custom model arg and num_ctx setting both flow through."""
        with patch(
            f"{_OLLAMA_MODULE}.get_setting_from_snapshot"
        ) as mock_get_setting:
            mock_get_setting.side_effect = _make_settings_side_effect(
                {"embeddings.ollama.num_ctx": 4096}
            )

            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:
                    OllamaEmbeddingsProvider.create_embeddings(
                        model="bge-m3",
                    )

                    call_kwargs = mock_ollama.call_args[1]
                    assert call_kwargs["model"] == "bge-m3"
                    assert call_kwargs["num_ctx"] == 4096

    def test_create_uses_default_num_ctx_when_snapshot_missing_key(self):
        """Falls back to default=8192 when snapshot doesn't contain num_ctx."""
        # _make_settings_side_effect returns the caller's `default=` for
        # absent keys, mirroring what the real settings system does when
        # neither DB nor JSON defaults have the key (shouldn't happen in
        # practice once the JSON ships, but verifies the explicit default
        # in create_embeddings is wired correctly).
        with patch(
            f"{_OLLAMA_MODULE}.get_setting_from_snapshot"
        ) as mock_get_setting:
            mock_get_setting.side_effect = _make_settings_side_effect(
                {"embeddings.ollama.model": "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:
                    OllamaEmbeddingsProvider.create_embeddings()

                    call_kwargs = mock_ollama.call_args[1]
                    assert call_kwargs["num_ctx"] == 8192


# ── weakref.finalize safety net ─────────────────────────────────────────


class TestOllamaEmbeddingsFinalizerSafetyNet:
    """The provider factory registers a ``weakref.finalize`` so callers
    that bypass ``LocalEmbeddingManager`` (e.g. the programmatic-API
    example scripts under ``examples/api_usage/``, direct test
    constructions) still get cleanup at GC time.

    The manager-driven explicit close path remains the load-bearing
    primary cleanup; the finalizer is the last-resort safety net only.
    """

    def test_create_registers_finalizer_with_inner_clients(self):
        """Provider passes the inner sync/async ``ollama.Client`` objects to
        ``weakref.finalize`` — not the wrapping ``OllamaEmbeddings`` itself.
        Passing the instance would defeat the finalizer's purpose by keeping
        the instance alive through the registry's strong-ref."""
        with patch(
            f"{_OLLAMA_MODULE}.get_setting_from_snapshot"
        ) as mock_get_setting:
            mock_get_setting.side_effect = _make_settings_side_effect(
                {
                    "embeddings.ollama.model": "nomic-embed-text",
                    "embeddings.ollama.num_ctx": 8192,
                }
            )

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

                # A simple object (not Mock) so the auto-attr behavior of
                # Mock doesn't mask a missing _client/_async_client.
                class _FakeOllamaEmbeddings:
                    def __init__(self):
                        self._client = object()
                        self._async_client = object()

                fake_instance = _FakeOllamaEmbeddings()

                with patch(
                    f"{_OLLAMA_MODULE}.OllamaEmbeddings",
                    return_value=fake_instance,
                ):
                    with patch(
                        f"{_OLLAMA_MODULE}.weakref.finalize"
                    ) as mock_finalize:
                        result = OllamaEmbeddingsProvider.create_embeddings()

                        assert result is fake_instance
                        mock_finalize.assert_called_once()
                        args, _ = mock_finalize.call_args
                        # weakref.finalize(instance, callback, sync, async)
                        assert args[0] is fake_instance
                        assert args[2] is fake_instance._client
                        assert args[3] is fake_instance._async_client

    def test_finalizer_does_not_crash_if_inner_attrs_missing(self):
        """A future langchain_ollama version might rename _client /
        _async_client. The factory must not crash in that case — the
        explicit close path (or the upstream's own cleanup) remains
        responsible for resource release.
        """
        with patch(
            f"{_OLLAMA_MODULE}.get_setting_from_snapshot"
        ) as mock_get_setting:
            mock_get_setting.side_effect = _make_settings_side_effect(
                {
                    "embeddings.ollama.model": "nomic-embed-text",
                    "embeddings.ollama.num_ctx": 8192,
                }
            )

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

                # Slotted class with no _client / _async_client → attribute
                # access raises AttributeError, exercising the except branch.
                class _ReshapedEmbeddings:
                    __slots__ = ()

                with patch(
                    f"{_OLLAMA_MODULE}.OllamaEmbeddings",
                    return_value=_ReshapedEmbeddings(),
                ):
                    # Must not raise.
                    result = OllamaEmbeddingsProvider.create_embeddings()
                    assert result is not None

    def test_real_ollama_embeddings_finalizer_closes_clients_on_gc(self):
        """End-to-end: construct a real ``langchain_ollama.OllamaEmbeddings``
        via the provider, drop the reference, force GC, and verify both
        inner httpx clients are closed. This is the canary for the
        #3816-shaped FD leak on the embeddings side.
        """
        import gc
        import weakref as _weakref

        with patch(
            f"{_OLLAMA_MODULE}.get_setting_from_snapshot"
        ) as mock_get_setting:
            mock_get_setting.side_effect = _make_settings_side_effect(
                {
                    "embeddings.ollama.model": "nomic-embed-text",
                    "embeddings.ollama.num_ctx": 8192,
                }
            )

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

                # No OllamaEmbeddings patch — uses the real class.
                instance = OllamaEmbeddingsProvider.create_embeddings()
                sync_httpx = instance._client._client
                async_httpx = instance._async_client._client
                assert sync_httpx.is_closed is False
                assert async_httpx.is_closed is False

                instance_ref = _weakref.ref(instance)

            # End the patches before dropping the ref so the finalizer
            # runs against the real ``_close_inner_ollama_clients``.
            del instance
            gc.collect()

            assert instance_ref() is None, (
                "OllamaEmbeddings instance must be GC'able — the finalizer "
                "must not strong-ref it"
            )
            assert sync_httpx.is_closed is True
            assert async_httpx.is_closed is True


# ── _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_returns_none_when_capabilities_unavailable(self):
        """Older Ollama (no capabilities in /api/show) → None, not a guess.

        We refuse to guess from the model name; the caller can decide
        whether to tag the model, hide it, or just trust the user.
        """
        with patch(
            f"{_OLLAMA_MODULE}.get_ollama_base_url",
            return_value="http://localhost:11434",
        ):
            with patch.object(
                OllamaEmbeddingsProvider,
                "_get_model_capabilities",
                return_value=None,
            ):
                assert (
                    OllamaEmbeddingsProvider.is_embedding_model(
                        "nomic-embed-text"
                    )
                    is None
                )
                assert (
                    OllamaEmbeddingsProvider.is_embedding_model(
                        "deepseek-r1:32b"
                    )
                    is None
                )


# ── 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_capabilities_unavailable_returns_untagged_models(self):
        """Older Ollama (no /api/show capabilities) → every model is
        still listed, just without an ``is_embedding`` flag. The
        provider doesn't guess from the name."""
        all_models = [
            {
                "value": "nomic-embed-text:latest",
                "label": "nomic-embed-text:latest",
            },
            {"value": "qwen3:4b", "label": "qwen3:4b"},
        ]
        caps = {}  # No model has capabilities → all untagged

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

        assert {m["value"] for m in result} == {
            "nomic-embed-text:latest",
            "qwen3:4b",
        }
        assert all("is_embedding" not in m for m in result)

    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
