"""
Tests for embeddings/providers/implementations/openai.py

Tests cover:
- OpenAIEmbeddingsProvider.create_embeddings()
- OpenAIEmbeddingsProvider.is_available()
- OpenAIEmbeddingsProvider.get_available_models()
- Class attributes and metadata
"""

import pytest
from unittest.mock import patch, MagicMock


class TestOpenAIEmbeddingsProviderMetadata:
    """Tests for OpenAIEmbeddingsProvider class metadata."""

    def test_provider_name(self):
        """Test provider name is set correctly."""
        from local_deep_research.embeddings.providers.implementations.openai import (
            OpenAIEmbeddingsProvider,
        )

        assert OpenAIEmbeddingsProvider.provider_name == "OpenAI"

    def test_provider_key(self):
        """Test provider key is set correctly."""
        from local_deep_research.embeddings.providers.implementations.openai import (
            OpenAIEmbeddingsProvider,
        )

        assert OpenAIEmbeddingsProvider.provider_key == "OPENAI"

    def test_requires_api_key(self):
        """Test that OpenAI requires API key."""
        from local_deep_research.embeddings.providers.implementations.openai import (
            OpenAIEmbeddingsProvider,
        )

        assert OpenAIEmbeddingsProvider.requires_api_key is True

    def test_supports_local(self):
        """Test that OpenAI does not support local."""
        from local_deep_research.embeddings.providers.implementations.openai import (
            OpenAIEmbeddingsProvider,
        )

        assert OpenAIEmbeddingsProvider.supports_local is False

    def test_default_model(self):
        """Test default model is set."""
        from local_deep_research.embeddings.providers.implementations.openai import (
            OpenAIEmbeddingsProvider,
        )

        assert (
            OpenAIEmbeddingsProvider.default_model == "text-embedding-3-small"
        )


class TestOpenAIEmbeddingsProviderCreateEmbeddings:
    """Tests for OpenAIEmbeddingsProvider.create_embeddings method."""

    def test_create_embeddings_with_api_key(self):
        """Test creating embeddings with API key provided."""
        from local_deep_research.embeddings.providers.implementations.openai import (
            OpenAIEmbeddingsProvider,
        )

        mock_embeddings = MagicMock()

        # Mock get_setting_from_snapshot to return None for other settings
        with patch(
            "local_deep_research.embeddings.providers.implementations.openai.get_setting_from_snapshot",
            return_value=None,
        ):
            with patch(
                "langchain_openai.OpenAIEmbeddings",
                return_value=mock_embeddings,
            ) as mock_class:
                result = OpenAIEmbeddingsProvider.create_embeddings(
                    model="text-embedding-3-small",
                    api_key="test-api-key",
                )

                assert result is mock_embeddings
                mock_class.assert_called_once()
                call_kwargs = mock_class.call_args[1]
                assert call_kwargs["model"] == "text-embedding-3-small"
                assert call_kwargs["openai_api_key"] == "test-api-key"

    def test_create_embeddings_missing_api_key_raises(self):
        """Test that missing API key raises ValueError."""
        from local_deep_research.embeddings.providers.implementations.openai import (
            OpenAIEmbeddingsProvider,
        )

        with patch(
            "local_deep_research.embeddings.providers.implementations.openai.get_setting_from_snapshot",
            return_value=None,
        ):
            with pytest.raises(ValueError, match="API key not configured"):
                OpenAIEmbeddingsProvider.create_embeddings()

    def test_create_embeddings_with_settings_snapshot(self):
        """Test creating embeddings with settings snapshot."""
        from local_deep_research.embeddings.providers.implementations.openai import (
            OpenAIEmbeddingsProvider,
        )

        mock_embeddings = MagicMock()
        settings = {"embeddings.openai.api_key": "snapshot-key"}

        def mock_get_setting(key, default=None, settings_snapshot=None):
            if key == "embeddings.openai.api_key":
                return "snapshot-key"
            return default

        with patch(
            "local_deep_research.embeddings.providers.implementations.openai.get_setting_from_snapshot",
            side_effect=mock_get_setting,
        ):
            with patch(
                "langchain_openai.OpenAIEmbeddings",
                return_value=mock_embeddings,
            ):
                result = OpenAIEmbeddingsProvider.create_embeddings(
                    settings_snapshot=settings
                )

                assert result is mock_embeddings

    def test_create_embeddings_with_base_url(self):
        """Test creating embeddings with custom base URL."""
        from local_deep_research.embeddings.providers.implementations.openai import (
            OpenAIEmbeddingsProvider,
        )

        mock_embeddings = MagicMock()

        with patch(
            "local_deep_research.embeddings.providers.implementations.openai.get_setting_from_snapshot",
            return_value=None,
        ):
            with patch(
                "langchain_openai.OpenAIEmbeddings",
                return_value=mock_embeddings,
            ) as mock_class:
                OpenAIEmbeddingsProvider.create_embeddings(
                    api_key="test-key",
                    base_url="https://custom.openai.com",
                )

                call_kwargs = mock_class.call_args[1]
                assert (
                    call_kwargs["openai_api_base"]
                    == "https://custom.openai.com"
                )

    def test_create_embeddings_with_dimensions(self):
        """Test creating embeddings with custom dimensions for v3 model."""
        from local_deep_research.embeddings.providers.implementations.openai import (
            OpenAIEmbeddingsProvider,
        )

        mock_embeddings = MagicMock()

        with patch(
            "local_deep_research.embeddings.providers.implementations.openai.get_setting_from_snapshot",
            return_value=None,
        ):
            with patch(
                "langchain_openai.OpenAIEmbeddings",
                return_value=mock_embeddings,
            ) as mock_class:
                OpenAIEmbeddingsProvider.create_embeddings(
                    model="text-embedding-3-small",
                    api_key="test-key",
                    dimensions=256,
                )

                call_kwargs = mock_class.call_args[1]
                assert call_kwargs["dimensions"] == 256

    def test_create_embeddings_dimensions_ignored_for_non_v3_model(self):
        """Test that dimensions are ignored for non-v3 models."""
        from local_deep_research.embeddings.providers.implementations.openai import (
            OpenAIEmbeddingsProvider,
        )

        mock_embeddings = MagicMock()

        with patch(
            "local_deep_research.embeddings.providers.implementations.openai.get_setting_from_snapshot",
            return_value=None,
        ):
            with patch(
                "langchain_openai.OpenAIEmbeddings",
                return_value=mock_embeddings,
            ) as mock_class:
                OpenAIEmbeddingsProvider.create_embeddings(
                    model="text-embedding-ada-002",
                    api_key="test-key",
                    dimensions=256,
                )

                call_kwargs = mock_class.call_args[1]
                assert "dimensions" not in call_kwargs


class TestOpenAIEmbeddingsProviderIsAvailable:
    """Tests for OpenAIEmbeddingsProvider.is_available method."""

    def test_is_available_with_api_key(self):
        """Test that provider is available when API key is set."""
        from local_deep_research.embeddings.providers.implementations.openai import (
            OpenAIEmbeddingsProvider,
        )

        with patch(
            "local_deep_research.embeddings.providers.implementations.openai.get_setting_from_snapshot",
            return_value="test-api-key",
        ):
            assert OpenAIEmbeddingsProvider.is_available() is True

    def test_is_available_without_api_key(self):
        """Test that provider is not available without API key."""
        from local_deep_research.embeddings.providers.implementations.openai import (
            OpenAIEmbeddingsProvider,
        )

        with patch(
            "local_deep_research.embeddings.providers.implementations.openai.get_setting_from_snapshot",
            return_value=None,
        ):
            assert OpenAIEmbeddingsProvider.is_available() is False

    def test_is_available_with_empty_api_key(self):
        """Test that provider is not available with empty API key."""
        from local_deep_research.embeddings.providers.implementations.openai import (
            OpenAIEmbeddingsProvider,
        )

        with patch(
            "local_deep_research.embeddings.providers.implementations.openai.get_setting_from_snapshot",
            return_value="",
        ):
            assert OpenAIEmbeddingsProvider.is_available() is False

    def test_is_available_exception_returns_false(self):
        """Test that exception during availability check returns False."""
        from local_deep_research.embeddings.providers.implementations.openai import (
            OpenAIEmbeddingsProvider,
        )

        with patch(
            "local_deep_research.embeddings.providers.implementations.openai.get_setting_from_snapshot",
            side_effect=Exception("Settings error"),
        ):
            assert OpenAIEmbeddingsProvider.is_available() is False


class TestOpenAIEmbeddingsProviderGetAvailableModels:
    """Tests for OpenAIEmbeddingsProvider.get_available_models method."""

    def test_get_available_models_success(self):
        """Test getting available models from OpenAI API."""
        from local_deep_research.embeddings.providers.implementations.openai import (
            OpenAIEmbeddingsProvider,
        )

        mock_model1 = MagicMock()
        mock_model1.id = "text-embedding-3-small"
        mock_model2 = MagicMock()
        mock_model2.id = "text-embedding-3-large"
        mock_model3 = MagicMock()
        mock_model3.id = "gpt-4"  # Not an embedding model

        mock_response = MagicMock()
        mock_response.data = [mock_model1, mock_model2, mock_model3]

        mock_client = MagicMock()
        mock_client.models.list.return_value = mock_response

        with patch(
            "local_deep_research.embeddings.providers.implementations.openai.get_setting_from_snapshot",
            return_value="test-api-key",
        ):
            with patch(
                "openai.OpenAI",
                return_value=mock_client,
            ):
                models = OpenAIEmbeddingsProvider.get_available_models()

                # Should only return embedding models
                assert len(models) == 2
                assert models[0]["value"] == "text-embedding-3-small"
                assert models[1]["value"] == "text-embedding-3-large"

    def test_get_available_models_no_api_key(self):
        """Test getting models returns empty list when no API key."""
        from local_deep_research.embeddings.providers.implementations.openai import (
            OpenAIEmbeddingsProvider,
        )

        with patch(
            "local_deep_research.embeddings.providers.implementations.openai.get_setting_from_snapshot",
            return_value=None,
        ):
            models = OpenAIEmbeddingsProvider.get_available_models()
            assert models == []

    def test_get_available_models_api_error(self):
        """Test getting models returns empty list on API error."""
        from local_deep_research.embeddings.providers.implementations.openai import (
            OpenAIEmbeddingsProvider,
        )

        with patch(
            "local_deep_research.embeddings.providers.implementations.openai.get_setting_from_snapshot",
            return_value="test-api-key",
        ):
            with patch(
                "openai.OpenAI",
                side_effect=Exception("API error"),
            ):
                models = OpenAIEmbeddingsProvider.get_available_models()
                assert models == []


class TestOpenAIEmbeddingsProviderCompatibleEndpoint:
    """Regression tests for issue #3883.

    OpenAI-compatible local servers (LM Studio, vLLM, llama.cpp) speak
    the same wire protocol as the OpenAI API but typically run on
    ``http://localhost:<port>/v1`` and do not require an API key. The
    provider must:

      1. report ``is_available() == True`` when the user has set a
         ``base_url`` even with no API key (so the UI lists it);
      2. accept a missing API key at ``create_embeddings`` time as long
         as a base_url is set, falling back to a placeholder so the
         OpenAI client request still goes out;
      3. still raise the configuration error when *neither* an API key
         nor a base_url is set (so a blank install doesn't silently
         hit an unconfigured endpoint).
    """

    @staticmethod
    def _settings_mock(api_key, base_url):
        """Return a side_effect callable for get_setting_from_snapshot.

        The provider reads four ``embeddings.openai.*`` keys; this
        helper routes each to a per-test value and returns ``default``
        for anything else.
        """

        values = {
            "embeddings.openai.api_key": api_key,
            "embeddings.openai.base_url": base_url,
        }

        def _side_effect(key, default=None, settings_snapshot=None):
            if key in values:
                return values[key]
            return default

        return _side_effect

    def test_is_available_with_base_url_only(self):
        """Provider must be advertised when only base_url is set.

        Red on master, green on branch — this is the headline
        regression for issue #3883.
        """
        from local_deep_research.embeddings.providers.implementations.openai import (
            OpenAIEmbeddingsProvider,
        )

        with patch(
            "local_deep_research.embeddings.providers.implementations.openai.get_setting_from_snapshot",
            side_effect=self._settings_mock(
                api_key=None, base_url="http://localhost:1234/v1"
            ),
        ):
            assert OpenAIEmbeddingsProvider.is_available() is True

    def test_is_available_with_blank_base_url_and_no_key(self):
        """Provider stays hidden on a blank install."""
        from local_deep_research.embeddings.providers.implementations.openai import (
            OpenAIEmbeddingsProvider,
        )

        with patch(
            "local_deep_research.embeddings.providers.implementations.openai.get_setting_from_snapshot",
            side_effect=self._settings_mock(api_key="", base_url=""),
        ):
            assert OpenAIEmbeddingsProvider.is_available() is False

    def test_is_available_whitespace_only_settings_still_unavailable(self):
        """Whitespace-only values should not count as configured."""
        from local_deep_research.embeddings.providers.implementations.openai import (
            OpenAIEmbeddingsProvider,
        )

        with patch(
            "local_deep_research.embeddings.providers.implementations.openai.get_setting_from_snapshot",
            side_effect=self._settings_mock(api_key="   ", base_url="\t \n"),
        ):
            assert OpenAIEmbeddingsProvider.is_available() is False

    def test_create_embeddings_base_url_only_uses_placeholder_key(self):
        """No API key + base_url set → use placeholder, pass base_url through."""
        from local_deep_research.embeddings.providers.implementations.openai import (
            OpenAIEmbeddingsProvider,
        )

        with patch(
            "local_deep_research.embeddings.providers.implementations.openai.get_setting_from_snapshot",
            side_effect=self._settings_mock(
                api_key=None,
                base_url="http://host.docker.internal:1234/v1",
            ),
        ):
            with patch(
                "langchain_openai.OpenAIEmbeddings",
                return_value=MagicMock(),
            ) as mock_class:
                OpenAIEmbeddingsProvider.create_embeddings(
                    model="nomic-embed-text-v1.5",
                )

                call_kwargs = mock_class.call_args[1]
                assert (
                    call_kwargs["openai_api_key"]
                    == OpenAIEmbeddingsProvider._PLACEHOLDER_API_KEY
                )
                assert (
                    call_kwargs["openai_api_base"]
                    == "http://host.docker.internal:1234/v1"
                )

    def test_create_embeddings_error_message_mentions_base_url(self):
        """When both api_key and base_url are missing, the error must
        tell the user about the local-server fallback path so they
        don't go hunting for a key they don't have."""
        from local_deep_research.embeddings.providers.implementations.openai import (
            OpenAIEmbeddingsProvider,
        )

        with patch(
            "local_deep_research.embeddings.providers.implementations.openai.get_setting_from_snapshot",
            side_effect=self._settings_mock(api_key=None, base_url=None),
        ):
            with pytest.raises(ValueError, match="base_url"):
                OpenAIEmbeddingsProvider.create_embeddings()

    def test_get_available_models_uses_base_url_when_no_key(self):
        """Model discovery on a keyless local server must route through
        the configured base_url instead of api.openai.com."""
        from local_deep_research.embeddings.providers.implementations.openai import (
            OpenAIEmbeddingsProvider,
        )

        fake_client = MagicMock()
        fake_client.models.list.return_value = MagicMock(data=[])

        with patch(
            "local_deep_research.embeddings.providers.implementations.openai.get_setting_from_snapshot",
            side_effect=self._settings_mock(
                api_key=None, base_url="http://localhost:1234/v1"
            ),
        ):
            with patch(
                "openai.OpenAI", return_value=fake_client
            ) as mock_openai:
                OpenAIEmbeddingsProvider.get_available_models()

                kwargs = mock_openai.call_args[1]
                assert kwargs["base_url"] == "http://localhost:1234/v1"
                assert (
                    kwargs["api_key"]
                    == OpenAIEmbeddingsProvider._PLACEHOLDER_API_KEY
                )


class TestOpenAIEmbeddingsSettingsRegistration:
    """The settings file shipped for issue #3883 must register the
    keys the UI needs to surface the embeddings form, and the OpenAI
    provider must be wired up by ``_get_provider_classes`` regardless
    of API-key presence so it can be selected for keyless local
    servers."""

    def test_openai_embeddings_settings_file_registers_all_keys(self):
        """``settings_openai_embeddings.json`` must declare the four
        ``embeddings.openai.*`` keys that the provider reads at
        runtime (api_key, base_url, model, dimensions)."""
        import json
        from pathlib import Path
        import local_deep_research.defaults as defaults_pkg

        defaults_dir = Path(defaults_pkg.__file__).parent
        path = defaults_dir / "settings_openai_embeddings.json"
        assert path.exists(), (
            "Issue #3883: ship settings_openai_embeddings.json so the "
            "UI can surface the embeddings configuration form."
        )
        with open(path) as f:
            data = json.load(f)

        required = {
            "embeddings.openai.api_key",
            "embeddings.openai.base_url",
            "embeddings.openai.model",
            "embeddings.openai.dimensions",
        }
        missing = required - set(data.keys())
        assert not missing, f"settings file missing keys: {missing}"

        base_url_meta = data["embeddings.openai.base_url"]
        assert base_url_meta["category"] == "embeddings"
        assert base_url_meta["editable"] is True
        # The description is what tells users *why* this matters for
        # local servers — guard against silent removal.
        desc_lower = base_url_meta["description"].lower()
        assert any(
            tag in desc_lower for tag in ("lm studio", "vllm", "llama.cpp")
        ), "base_url description should mention an OpenAI-compatible server"

    def test_openai_in_provider_classes_dict(self):
        """The 'openai' key must remain wired to the provider class so
        the embeddings dispatch path can resolve it."""
        from local_deep_research.embeddings.embeddings_config import (
            _get_provider_classes,
        )
        from local_deep_research.embeddings.providers.implementations.openai import (
            OpenAIEmbeddingsProvider,
        )

        classes = _get_provider_classes()
        assert classes.get("openai") is OpenAIEmbeddingsProvider
