"""
Tests for rag_service_factory.get_rag_service().
"""

import uuid
from contextlib import contextmanager
from unittest.mock import MagicMock, patch

import pytest

from local_deep_research.database.models.library import (
    Collection,
    EmbeddingProvider,
)
from local_deep_research.research_library.services.rag_service_factory import (
    get_rag_service,
)

FACTORY_MODULE = (
    "local_deep_research.research_library.services.rag_service_factory"
)


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------


@pytest.fixture
def mock_settings():
    """Mock SettingsManager with default embedding settings."""
    mgr = MagicMock()
    mgr.get_setting.side_effect = lambda key, default=None: {
        "local_search_embedding_model": "all-MiniLM-L6-v2",
        "local_search_embedding_provider": "sentence_transformers",
        "local_search_chunk_size": 1000,
        "local_search_chunk_overlap": 200,
        "local_search_splitter_type": "recursive",
        "local_search_text_separators": None,
        "local_search_distance_metric": "cosine",
        "local_search_index_type": "Flat",
    }.get(key, default)
    mgr.get_bool_setting.return_value = True
    return mgr


@pytest.fixture
def patch_settings(mock_settings):
    """Patch get_settings_manager to return our mock."""
    with patch(
        f"{FACTORY_MODULE}.get_settings_manager", return_value=mock_settings
    ):
        yield mock_settings


@pytest.fixture
def mock_db_session(library_session):
    """Yield a context-managed library_session for the factory's DB calls."""

    @contextmanager
    def _ctx(*args, **kwargs):
        yield library_session

    with patch(f"{FACTORY_MODULE}.get_user_db_session", _ctx):
        yield library_session


@pytest.fixture
def mock_rag_cls():
    """Patch LibraryRAGService so we capture constructor args without side-effects."""
    with patch(f"{FACTORY_MODULE}.LibraryRAGService") as cls:
        cls.return_value = MagicMock(name="RAGServiceInstance")
        yield cls


# ---------------------------------------------------------------------------
# Tests — no collection_id
# ---------------------------------------------------------------------------


class TestGetRagServiceDefaults:
    def test_returns_service_with_defaults(self, patch_settings, mock_rag_cls):
        """Without collection_id, should create service with default settings."""
        service = get_rag_service("alice")

        mock_rag_cls.assert_called_once()
        kwargs = mock_rag_cls.call_args.kwargs
        assert kwargs["username"] == "alice"
        assert kwargs["embedding_model"] == "all-MiniLM-L6-v2"
        assert kwargs["embedding_provider"] == "sentence_transformers"
        assert kwargs["chunk_size"] == 1000
        assert kwargs["chunk_overlap"] == 200
        assert kwargs["db_password"] is None
        assert service is mock_rag_cls.return_value

    def test_passes_db_password(self, patch_settings, mock_rag_cls):
        """db_password should be forwarded to LibraryRAGService."""
        get_rag_service("alice", db_password="secret")

        kwargs = mock_rag_cls.call_args.kwargs
        assert kwargs["db_password"] == "secret"

    def test_fallback_when_settings_return_none(self, mock_rag_cls):
        """When all settings return None, factory should use hardcoded defaults."""
        mgr = MagicMock()
        mgr.get_setting.return_value = None
        mgr.get_bool_setting.return_value = None

        with patch(f"{FACTORY_MODULE}.get_settings_manager", return_value=mgr):
            get_rag_service("alice")

        kwargs = mock_rag_cls.call_args.kwargs
        assert kwargs["embedding_model"] == "all-MiniLM-L6-v2"
        assert kwargs["embedding_provider"] == "sentence_transformers"
        assert kwargs["chunk_size"] == 1000
        assert kwargs["chunk_overlap"] == 200
        assert kwargs["splitter_type"] == "recursive"
        assert kwargs["distance_metric"] == "cosine"
        assert kwargs["index_type"] == "Flat"
        assert kwargs["text_separators"] == ["\n\n", "\n", ". ", " ", ""]

    def test_text_separators_json_string_parsed(self, mock_rag_cls):
        """JSON-encoded text_separators string should be parsed to a list."""
        mgr = MagicMock()
        mgr.get_setting.side_effect = lambda key, default=None: {
            "local_search_text_separators": '["\\n", ". "]',
        }.get(key, default)
        mgr.get_bool_setting.return_value = True

        with patch(f"{FACTORY_MODULE}.get_settings_manager", return_value=mgr):
            get_rag_service("alice")

        kwargs = mock_rag_cls.call_args.kwargs
        assert kwargs["text_separators"] == ["\n", ". "]

    def test_invalid_text_separators_json_uses_default(self, mock_rag_cls):
        """Invalid JSON for text_separators should fall back to defaults."""
        mgr = MagicMock()
        mgr.get_setting.side_effect = lambda key, default=None: {
            "local_search_text_separators": "not valid json",
        }.get(key, default)
        mgr.get_bool_setting.return_value = True

        with patch(f"{FACTORY_MODULE}.get_settings_manager", return_value=mgr):
            get_rag_service("alice")

        kwargs = mock_rag_cls.call_args.kwargs
        assert kwargs["text_separators"] == ["\n\n", "\n", ". ", " ", ""]


# ---------------------------------------------------------------------------
# Tests — with collection_id
# ---------------------------------------------------------------------------


class TestGetRagServiceWithCollection:
    def test_uses_stored_collection_settings(
        self, patch_settings, mock_db_session, mock_rag_cls
    ):
        """When collection has stored embedding settings, those should be used."""
        coll = Collection(
            id=str(uuid.uuid4()),
            name="Test Collection",
            is_default=False,
            collection_type="user_collection",
            embedding_model="nomic-embed-text",
            embedding_model_type=EmbeddingProvider.OLLAMA,
            chunk_size=500,
            chunk_overlap=100,
            splitter_type="token",
            distance_metric="l2",
            normalize_vectors=True,
            index_type="hnsw",
        )
        mock_db_session.add(coll)
        mock_db_session.commit()

        get_rag_service("alice", collection_id=coll.id)

        kwargs = mock_rag_cls.call_args.kwargs
        assert kwargs["embedding_model"] == "nomic-embed-text"
        assert kwargs["embedding_provider"] == "ollama"
        assert kwargs["chunk_size"] == 500
        assert kwargs["chunk_overlap"] == 100

    def test_new_collection_uses_defaults(
        self, patch_settings, mock_db_session, mock_rag_cls
    ):
        """Collection without stored embedding_model should get defaults."""
        coll = Collection(
            id=str(uuid.uuid4()),
            name="New Collection",
            is_default=False,
            collection_type="user_collection",
            embedding_model=None,
        )
        mock_db_session.add(coll)
        mock_db_session.commit()

        get_rag_service("alice", collection_id=coll.id)

        kwargs = mock_rag_cls.call_args.kwargs
        assert kwargs["embedding_model"] == "all-MiniLM-L6-v2"

    def test_use_defaults_ignores_stored_settings(
        self, patch_settings, mock_db_session, mock_rag_cls
    ):
        """use_defaults=True should bypass stored collection settings."""
        coll = Collection(
            id=str(uuid.uuid4()),
            name="Collection With Settings",
            is_default=False,
            collection_type="user_collection",
            embedding_model="nomic-embed-text",
            embedding_model_type=EmbeddingProvider.OLLAMA,
        )
        mock_db_session.add(coll)
        mock_db_session.commit()

        get_rag_service("alice", collection_id=coll.id, use_defaults=True)

        kwargs = mock_rag_cls.call_args.kwargs
        assert kwargs["embedding_model"] == "all-MiniLM-L6-v2"

    def test_nonexistent_collection_uses_defaults(
        self, patch_settings, mock_db_session, mock_rag_cls
    ):
        """Unknown collection_id should fall back to defaults."""
        get_rag_service("alice", collection_id="nonexistent-id")

        kwargs = mock_rag_cls.call_args.kwargs
        assert kwargs["embedding_model"] == "all-MiniLM-L6-v2"

    def test_nullable_embedding_model_type_in_log(
        self, patch_settings, mock_db_session, mock_rag_cls
    ):
        """Collection with embedding_model but NULL embedding_model_type should not crash."""
        coll = Collection(
            id=str(uuid.uuid4()),
            name="Partial Settings",
            is_default=False,
            collection_type="user_collection",
            embedding_model="some-model",
            embedding_model_type=None,
        )
        mock_db_session.add(coll)
        mock_db_session.commit()

        # Should not raise AttributeError on .value
        get_rag_service("alice", collection_id=coll.id)

    def test_normalize_vectors_false_propagated(
        self, patch_settings, mock_db_session, mock_rag_cls
    ):
        """normalize_vectors=False should propagate to the service."""
        coll = Collection(
            id=str(uuid.uuid4()),
            name="No Normalize",
            is_default=False,
            collection_type="user_collection",
            embedding_model="test-model",
            embedding_model_type=EmbeddingProvider.SENTENCE_TRANSFORMERS,
            normalize_vectors=False,
        )
        mock_db_session.add(coll)
        mock_db_session.commit()

        get_rag_service("alice", collection_id=coll.id)

        kwargs = mock_rag_cls.call_args.kwargs
        assert kwargs["normalize_vectors"] is False

    def test_normalize_vectors_none_uses_default(
        self, patch_settings, mock_db_session, mock_rag_cls
    ):
        """normalize_vectors=None should fall back to default setting."""
        coll = Collection(
            id=str(uuid.uuid4()),
            name="None Normalize",
            is_default=False,
            collection_type="user_collection",
            embedding_model="test-model",
            embedding_model_type=EmbeddingProvider.SENTENCE_TRANSFORMERS,
            normalize_vectors=None,
        )
        mock_db_session.add(coll)
        mock_db_session.commit()

        get_rag_service("alice", collection_id=coll.id)

        kwargs = mock_rag_cls.call_args.kwargs
        # Default from mock_settings.get_bool_setting is True
        assert kwargs["normalize_vectors"] is True
