"""
Comprehensive tests for LibraryRAGSearchEngine (replacement for deleted search_engine_local.py).

Covers:
- Init configuration and settings handling
- Search result formatting (title fallbacks, snippet truncation, score conversion)
- Multi-collection search with score-based sorting
- Per-collection error handling (no RAG index, no indexed docs, exceptions)
- _get_full_content with snippets-only mode, missing username, DB retrieval
- PDF URL generation path
- Edge cases (empty metadata, None settings_snapshot)
"""

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

# ---------------------------------------------------------------------------
# Common patch paths
# ---------------------------------------------------------------------------
_MOD = "local_deep_research.web_search_engines.engines.search_engine_library"
_PATCH_SETTING = f"{_MOD}.get_setting_from_snapshot"
_PATCH_SERVER_URL = f"{_MOD}.get_server_url"
_PATCH_LIB_SERVICE = f"{_MOD}.LibraryService"
_PATCH_RAG_SERVICE = f"{_MOD}.LibraryRAGService"
_PATCH_DB_SESSION = f"{_MOD}.get_user_db_session"
_PATCH_PDF_MANAGER = f"{_MOD}.PDFStorageManager"
_PATCH_LIB_DIR = f"{_MOD}.get_library_directory"


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _make_engine(settings_snapshot=None, **kwargs):
    """Create a LibraryRAGSearchEngine with all external deps patched."""
    from local_deep_research.web_search_engines.engines.search_engine_library import (
        LibraryRAGSearchEngine,
    )

    with (
        patch(_PATCH_SETTING, return_value=None),
        patch(_PATCH_SERVER_URL, return_value="http://localhost:5000"),
    ):
        return LibraryRAGSearchEngine(
            settings_snapshot=settings_snapshot, **kwargs
        )


def _make_doc(page_content="content", metadata=None):
    """Create a mock Document."""
    doc = Mock()
    doc.page_content = page_content
    doc.metadata = metadata if metadata is not None else {}
    return doc


def _mock_rag_index(
    embedding_model="all-MiniLM-L6-v2",
    embedding_provider="sentence_transformers",
    chunk_size=1000,
    chunk_overlap=200,
):
    idx = Mock()
    idx.embedding_model = embedding_model
    idx.embedding_model_type = Mock(value=embedding_provider)
    idx.chunk_size = chunk_size
    idx.chunk_overlap = chunk_overlap
    return idx


# ---------------------------------------------------------------------------
# Init / Configuration
# ---------------------------------------------------------------------------
class TestInitConfiguration:
    """Tests for initialization and settings handling."""

    def test_username_from_settings_snapshot(self):
        engine = _make_engine(settings_snapshot={"_username": "alice"})
        assert engine.username == "alice"

    def test_username_none_when_no_snapshot(self):
        engine = _make_engine(settings_snapshot=None)
        assert engine.username is None

    def test_username_none_when_snapshot_missing_key(self):
        engine = _make_engine(settings_snapshot={"other_key": "val"})
        assert engine.username is None

    def test_embedding_settings_from_snapshot(self):
        from local_deep_research.web_search_engines.engines.search_engine_library import (
            LibraryRAGSearchEngine,
        )

        def side_effect(key, snapshot, default):
            mapping = {
                "local_search_embedding_model": "custom-model",
                "local_search_embedding_provider": "openai",
                "local_search_chunk_size": 2000,
                "local_search_chunk_overlap": 400,
            }
            return mapping.get(key, default)

        with (
            patch(_PATCH_SETTING, side_effect=side_effect),
            patch(_PATCH_SERVER_URL, return_value="http://localhost:5000"),
        ):
            engine = LibraryRAGSearchEngine(
                settings_snapshot={"_username": "bob"}
            )

        assert engine.embedding_model == "custom-model"
        assert engine.embedding_provider == "openai"
        assert engine.chunk_size == 2000
        assert engine.chunk_overlap == 400

    def test_server_url_stored(self):
        from local_deep_research.web_search_engines.engines.search_engine_library import (
            LibraryRAGSearchEngine,
        )

        with (
            patch(_PATCH_SETTING, return_value=None),
            patch(_PATCH_SERVER_URL, return_value="https://myhost:8080"),
        ):
            engine = LibraryRAGSearchEngine()
        assert engine.server_url == "https://myhost:8080"

    def test_max_filtered_results_passed_to_base(self):
        engine = _make_engine(max_filtered_results=5)
        assert engine.max_filtered_results == 5

    def test_is_local_class_attribute(self):
        from local_deep_research.web_search_engines.engines.search_engine_library import (
            LibraryRAGSearchEngine,
        )

        assert LibraryRAGSearchEngine.is_local is True


# ---------------------------------------------------------------------------
# Title fallback chain
# ---------------------------------------------------------------------------
class TestTitleFallbacks:
    """Title should fall back: document_title -> title -> 'Document {id}' -> 'Untitled'."""

    def _run_search_with_metadata(self, metadata, page_content="text"):
        """Run a search that returns one document with the given metadata."""
        doc = _make_doc(page_content=page_content, metadata=metadata)

        engine = _make_engine(settings_snapshot={"_username": "u"})

        with (
            patch(_PATCH_LIB_SERVICE) as lib_svc,
            patch(_PATCH_DB_SESSION) as db_sess,
            patch(_PATCH_RAG_SERVICE) as rag_svc,
        ):
            lib_svc.return_value.get_all_collections.return_value = [
                {"id": 1, "name": "C"}
            ]

            # RAG index lookup
            db_sess.return_value.__enter__.return_value.query.return_value.filter_by.return_value.first.return_value = _mock_rag_index()

            # RAG service context manager
            rag_inst = Mock()
            rag_inst.get_rag_stats.return_value = {"indexed_documents": 1}
            vs = Mock()
            vs.similarity_search_with_score.return_value = [(doc, 0.1)]
            rag_inst.load_or_create_faiss_index.return_value = vs
            rag_svc.return_value.__enter__.return_value = rag_inst
            rag_svc.return_value.__exit__.return_value = None

            return engine.search("q")[0]

    def test_document_title_preferred(self):
        result = self._run_search_with_metadata(
            {"document_title": "Doc Title", "title": "Other", "source_id": "1"}
        )
        assert result["title"] == "Doc Title"

    def test_title_used_when_no_document_title(self):
        result = self._run_search_with_metadata(
            {"title": "Fallback Title", "source_id": "1"}
        )
        assert result["title"] == "Fallback Title"

    def test_document_id_fallback(self):
        result = self._run_search_with_metadata({"source_id": "42"})
        assert result["title"] == "Document 42"

    def test_untitled_when_no_metadata(self):
        result = self._run_search_with_metadata({})
        assert result["title"] == "Untitled"


# ---------------------------------------------------------------------------
# Snippet truncation
# ---------------------------------------------------------------------------
class TestSnippetTruncation:
    """Snippet should be truncated at SNIPPET_LENGTH_LONG with '...' appended."""

    def _search_with_content(self, content):
        doc = _make_doc(page_content=content, metadata={"source_id": "1"})
        engine = _make_engine(settings_snapshot={"_username": "u"})

        with (
            patch(_PATCH_LIB_SERVICE) as lib_svc,
            patch(_PATCH_DB_SESSION) as db_sess,
            patch(_PATCH_RAG_SERVICE) as rag_svc,
        ):
            lib_svc.return_value.get_all_collections.return_value = [
                {"id": 1, "name": "C"}
            ]
            db_sess.return_value.__enter__.return_value.query.return_value.filter_by.return_value.first.return_value = _mock_rag_index()
            rag_inst = Mock()
            rag_inst.get_rag_stats.return_value = {"indexed_documents": 1}
            vs = Mock()
            vs.similarity_search_with_score.return_value = [(doc, 0.1)]
            rag_inst.load_or_create_faiss_index.return_value = vs
            rag_svc.return_value.__enter__.return_value = rag_inst
            rag_svc.return_value.__exit__.return_value = None

            return engine.search("q")[0]

    def test_short_content_not_truncated(self):
        result = self._search_with_content("short")
        assert result["snippet"] == "short"
        assert not result["snippet"].endswith("...")

    def test_long_content_truncated(self):
        from local_deep_research.constants import SNIPPET_LENGTH_LONG

        long_text = "x" * (SNIPPET_LENGTH_LONG + 100)
        result = self._search_with_content(long_text)
        assert result["snippet"].endswith("...")
        # The snippet body (without "...") should be SNIPPET_LENGTH_LONG chars
        assert len(result["snippet"]) == SNIPPET_LENGTH_LONG + 3

    def test_exact_length_not_truncated(self):
        from local_deep_research.constants import SNIPPET_LENGTH_LONG

        exact_text = "y" * SNIPPET_LENGTH_LONG
        result = self._search_with_content(exact_text)
        assert result["snippet"] == exact_text
        assert not result["snippet"].endswith("...")


# ---------------------------------------------------------------------------
# Relevance score conversion
# ---------------------------------------------------------------------------
class TestRelevanceScore:
    """relevance_score should be 1/(1+distance)."""

    def _search_with_score(self, distance):
        doc = _make_doc(metadata={"source_id": "1"})
        engine = _make_engine(settings_snapshot={"_username": "u"})

        with (
            patch(_PATCH_LIB_SERVICE) as lib_svc,
            patch(_PATCH_DB_SESSION) as db_sess,
            patch(_PATCH_RAG_SERVICE) as rag_svc,
        ):
            lib_svc.return_value.get_all_collections.return_value = [
                {"id": 1, "name": "C"}
            ]
            db_sess.return_value.__enter__.return_value.query.return_value.filter_by.return_value.first.return_value = _mock_rag_index()
            rag_inst = Mock()
            rag_inst.get_rag_stats.return_value = {"indexed_documents": 1}
            vs = Mock()
            vs.similarity_search_with_score.return_value = [(doc, distance)]
            rag_inst.load_or_create_faiss_index.return_value = vs
            rag_svc.return_value.__enter__.return_value = rag_inst
            rag_svc.return_value.__exit__.return_value = None

            return engine.search("q")[0]

    def test_zero_distance(self):
        result = self._search_with_score(0.0)
        assert result["relevance_score"] == pytest.approx(1.0)

    def test_distance_one(self):
        result = self._search_with_score(1.0)
        assert result["relevance_score"] == pytest.approx(0.5)

    def test_large_distance(self):
        result = self._search_with_score(99.0)
        assert result["relevance_score"] == pytest.approx(1.0 / 100.0)


# ---------------------------------------------------------------------------
# Multi-collection sorting
# ---------------------------------------------------------------------------
class TestMultiCollectionSorting:
    """Results across collections should be sorted by distance (ascending)."""

    def test_results_sorted_by_score(self):
        doc_close = _make_doc(
            page_content="close",
            metadata={"source_id": "1", "document_title": "Close"},
        )
        doc_far = _make_doc(
            page_content="far",
            metadata={"source_id": "2", "document_title": "Far"},
        )

        engine = _make_engine(settings_snapshot={"_username": "u"})

        with (
            patch(_PATCH_LIB_SERVICE) as lib_svc,
            patch(_PATCH_DB_SESSION) as db_sess,
            patch(_PATCH_RAG_SERVICE) as rag_svc,
        ):
            lib_svc.return_value.get_all_collections.return_value = [
                {"id": 1, "name": "A"},
                {"id": 2, "name": "B"},
            ]

            db_sess.return_value.__enter__.return_value.query.return_value.filter_by.return_value.first.return_value = _mock_rag_index()

            # Two collections, each returning one doc. Collection B has
            # the closer match (lower distance).
            call_count = [0]

            def rag_enter(self_rag):
                inst = Mock()
                inst.get_rag_stats.return_value = {"indexed_documents": 1}
                vs = Mock()
                if call_count[0] == 0:
                    vs.similarity_search_with_score.return_value = [
                        (doc_far, 5.0)
                    ]
                else:
                    vs.similarity_search_with_score.return_value = [
                        (doc_close, 0.2)
                    ]
                call_count[0] += 1
                inst.load_or_create_faiss_index.return_value = vs
                return inst

            rag_svc.return_value.__enter__ = rag_enter
            rag_svc.return_value.__exit__ = Mock(return_value=None)

            results = engine.search("q", limit=10)

        assert len(results) == 2
        # Closer result first
        assert results[0]["title"] == "Close"
        assert results[1]["title"] == "Far"

    def test_limit_respected_across_collections(self):
        """Only top `limit` results returned even when there are more."""
        docs = [
            _make_doc(
                page_content=f"doc{i}",
                metadata={"source_id": str(i), "document_title": f"D{i}"},
            )
            for i in range(5)
        ]
        engine = _make_engine(settings_snapshot={"_username": "u"})

        with (
            patch(_PATCH_LIB_SERVICE) as lib_svc,
            patch(_PATCH_DB_SESSION) as db_sess,
            patch(_PATCH_RAG_SERVICE) as rag_svc,
        ):
            lib_svc.return_value.get_all_collections.return_value = [
                {"id": 1, "name": "C"}
            ]
            db_sess.return_value.__enter__.return_value.query.return_value.filter_by.return_value.first.return_value = _mock_rag_index()
            rag_inst = Mock()
            rag_inst.get_rag_stats.return_value = {"indexed_documents": 5}
            vs = Mock()
            vs.similarity_search_with_score.return_value = [
                (docs[i], float(i)) for i in range(5)
            ]
            rag_inst.load_or_create_faiss_index.return_value = vs
            rag_svc.return_value.__enter__.return_value = rag_inst
            rag_svc.return_value.__exit__.return_value = None

            results = engine.search("q", limit=2)

        assert len(results) == 2


# ---------------------------------------------------------------------------
# Per-collection error paths
# ---------------------------------------------------------------------------
class TestPerCollectionErrors:
    """Errors in one collection should not prevent results from others."""

    def _setup_two_collections(
        self, first_raises=False, first_no_rag=False, first_no_docs=False
    ):
        """Helper: two collections, configure first to fail in various ways."""
        good_doc = _make_doc(
            page_content="good",
            metadata={"source_id": "99", "document_title": "Good"},
        )
        engine = _make_engine(settings_snapshot={"_username": "u"})

        with (
            patch(_PATCH_LIB_SERVICE) as lib_svc,
            patch(_PATCH_DB_SESSION) as db_sess,
            patch(_PATCH_RAG_SERVICE) as rag_svc,
        ):
            lib_svc.return_value.get_all_collections.return_value = [
                {"id": 1, "name": "Bad"},
                {"id": 2, "name": "Good"},
            ]

            call_count = [0]
            good_rag_index = _mock_rag_index()

            def session_enter(*a, **kw):
                ctx = MagicMock()
                sess = MagicMock()

                if call_count[0] == 0:
                    call_count[0] += 1
                    if first_no_rag:
                        sess.query.return_value.filter_by.return_value.first.return_value = None
                    elif first_raises:
                        sess.query.return_value.filter_by.return_value.first.side_effect = Exception(
                            "boom"
                        )
                    else:
                        sess.query.return_value.filter_by.return_value.first.return_value = good_rag_index
                else:
                    sess.query.return_value.filter_by.return_value.first.return_value = good_rag_index

                ctx.__enter__ = Mock(return_value=sess)
                ctx.__exit__ = Mock(return_value=None)
                return ctx

            db_sess.side_effect = session_enter

            # RAG service
            rag_call = [0]

            def rag_enter(self_rag):
                inst = Mock()
                if rag_call[0] == 0 and first_no_docs:
                    inst.get_rag_stats.return_value = {"indexed_documents": 0}
                else:
                    inst.get_rag_stats.return_value = {"indexed_documents": 1}
                    vs = Mock()
                    vs.similarity_search_with_score.return_value = [
                        (good_doc, 0.1)
                    ]
                    inst.load_or_create_faiss_index.return_value = vs
                rag_call[0] += 1
                return inst

            rag_svc.return_value.__enter__ = rag_enter
            rag_svc.return_value.__exit__ = Mock(return_value=None)

            results = engine.search("q")

        return results

    def test_collection_with_no_rag_index_skipped(self):
        results = self._setup_two_collections(first_no_rag=True)
        assert len(results) == 1
        assert results[0]["title"] == "Good"

    def test_collection_with_no_indexed_docs_skipped(self):
        results = self._setup_two_collections(first_no_docs=True)
        assert len(results) == 1
        assert results[0]["title"] == "Good"

    def test_collection_exception_skipped(self):
        results = self._setup_two_collections(first_raises=True)
        assert len(results) == 1
        assert results[0]["title"] == "Good"

    def test_collection_with_no_id_skipped(self):
        engine = _make_engine(settings_snapshot={"_username": "u"})

        with (
            patch(_PATCH_LIB_SERVICE) as lib_svc,
            patch(_PATCH_DB_SESSION),
            patch(_PATCH_RAG_SERVICE),
        ):
            # Collection without "id" key
            lib_svc.return_value.get_all_collections.return_value = [
                {"name": "No ID"}
            ]
            results = engine.search("q")

        assert results == []


# ---------------------------------------------------------------------------
# Document URL / PDF path
# ---------------------------------------------------------------------------
class TestDocumentURL:
    """URL generation: default /library/document/{id}, PDF if has_pdf."""

    def _search_with_pdf_check(self, has_pdf):
        doc = _make_doc(
            page_content="text",
            metadata={"source_id": "10", "document_title": "T"},
        )
        # Need to keep get_setting_from_snapshot patched during search() too,
        # since it's called for "research_library.storage_path" inside the
        # result formatting loop.
        with (
            patch(_PATCH_SETTING, return_value="/tmp/lib"),
            patch(_PATCH_SERVER_URL, return_value="http://localhost:5000"),
            patch(_PATCH_LIB_SERVICE) as lib_svc,
            patch(_PATCH_DB_SESSION) as db_sess,
            patch(_PATCH_RAG_SERVICE) as rag_svc,
            patch(_PATCH_PDF_MANAGER) as pdf_mgr,
            patch(_PATCH_LIB_DIR, return_value="/tmp/lib"),
        ):
            from local_deep_research.web_search_engines.engines.search_engine_library import (
                LibraryRAGSearchEngine,
            )

            engine = LibraryRAGSearchEngine(
                settings_snapshot={"_username": "u"}
            )

            lib_svc.return_value.get_all_collections.return_value = [
                {"id": 1, "name": "C"}
            ]

            # get_user_db_session is called twice:
            # 1) For RAG index lookup (inside collection loop)
            # 2) For document/PDF check (inside result formatting)
            mock_rag_index = _mock_rag_index()
            mock_document = Mock()
            mock_document.id = 10

            call_count = [0]

            def db_session_side_effect(username):
                ctx = MagicMock()
                sess = MagicMock()
                if call_count[0] == 0:
                    # First call: RAG index query
                    sess.query.return_value.filter_by.return_value.first.return_value = mock_rag_index
                else:
                    # Second call: Document query for PDF check
                    sess.query.return_value.filter_by.return_value.first.return_value = mock_document
                call_count[0] += 1
                ctx.__enter__ = Mock(return_value=sess)
                ctx.__exit__ = Mock(return_value=None)
                return ctx

            db_sess.side_effect = db_session_side_effect

            rag_inst = Mock()
            rag_inst.get_rag_stats.return_value = {"indexed_documents": 1}
            vs = Mock()
            vs.similarity_search_with_score.return_value = [(doc, 0.5)]
            rag_inst.load_or_create_faiss_index.return_value = vs
            rag_svc.return_value.__enter__.return_value = rag_inst
            rag_svc.return_value.__exit__.return_value = None

            pdf_mgr.return_value.has_pdf.return_value = has_pdf

            results = engine.search("q")

        return results[0]

    def test_pdf_url_when_has_pdf(self):
        result = self._search_with_pdf_check(True)
        assert result["url"] == "/library/document/10/pdf"

    def test_default_url_when_no_pdf(self):
        result = self._search_with_pdf_check(False)
        assert result["url"] == "/library/document/10"

    def test_url_hash_when_no_doc_id(self):
        doc = _make_doc(page_content="text", metadata={})
        engine = _make_engine(settings_snapshot={"_username": "u"})

        with (
            patch(_PATCH_LIB_SERVICE) as lib_svc,
            patch(_PATCH_DB_SESSION) as db_sess,
            patch(_PATCH_RAG_SERVICE) as rag_svc,
        ):
            lib_svc.return_value.get_all_collections.return_value = [
                {"id": 1, "name": "C"}
            ]
            db_sess.return_value.__enter__.return_value.query.return_value.filter_by.return_value.first.return_value = _mock_rag_index()
            rag_inst = Mock()
            rag_inst.get_rag_stats.return_value = {"indexed_documents": 1}
            vs = Mock()
            vs.similarity_search_with_score.return_value = [(doc, 0.1)]
            rag_inst.load_or_create_faiss_index.return_value = vs
            rag_svc.return_value.__enter__.return_value = rag_inst
            rag_svc.return_value.__exit__.return_value = None

            results = engine.search("q")

        assert results[0]["url"] == "#"
        assert results[0]["link"] == "#"


# ---------------------------------------------------------------------------
# document_id fallback (source_id vs document_id metadata key)
# ---------------------------------------------------------------------------
class TestDocIdFallback:
    def _search_with_metadata(self, metadata):
        doc = _make_doc(page_content="text", metadata=metadata)
        engine = _make_engine(settings_snapshot={"_username": "u"})

        with (
            patch(_PATCH_LIB_SERVICE) as lib_svc,
            patch(_PATCH_DB_SESSION) as db_sess,
            patch(_PATCH_RAG_SERVICE) as rag_svc,
        ):
            lib_svc.return_value.get_all_collections.return_value = [
                {"id": 1, "name": "C"}
            ]
            db_sess.return_value.__enter__.return_value.query.return_value.filter_by.return_value.first.return_value = _mock_rag_index()
            rag_inst = Mock()
            rag_inst.get_rag_stats.return_value = {"indexed_documents": 1}
            vs = Mock()
            vs.similarity_search_with_score.return_value = [(doc, 0.1)]
            rag_inst.load_or_create_faiss_index.return_value = vs
            rag_svc.return_value.__enter__.return_value = rag_inst
            rag_svc.return_value.__exit__.return_value = None

            return engine.search("q")[0]

    def test_source_id_preferred(self):
        result = self._search_with_metadata(
            {"source_id": "A", "document_id": "B"}
        )
        assert "/library/document/A" in result["url"]

    def test_document_id_fallback(self):
        result = self._search_with_metadata({"document_id": "B"})
        assert "/library/document/B" in result["url"]


# ---------------------------------------------------------------------------
# Result metadata enrichment
# ---------------------------------------------------------------------------
class TestResultMetadata:
    """Collection info should be injected into doc metadata."""

    def test_collection_metadata_added(self):
        doc = _make_doc(
            page_content="text",
            metadata={"source_id": "1", "document_title": "T"},
        )
        engine = _make_engine(settings_snapshot={"_username": "u"})

        with (
            patch(_PATCH_LIB_SERVICE) as lib_svc,
            patch(_PATCH_DB_SESSION) as db_sess,
            patch(_PATCH_RAG_SERVICE) as rag_svc,
        ):
            lib_svc.return_value.get_all_collections.return_value = [
                {"id": 7, "name": "My Coll"}
            ]
            db_sess.return_value.__enter__.return_value.query.return_value.filter_by.return_value.first.return_value = _mock_rag_index()
            rag_inst = Mock()
            rag_inst.get_rag_stats.return_value = {"indexed_documents": 1}
            vs = Mock()
            vs.similarity_search_with_score.return_value = [(doc, 0.1)]
            rag_inst.load_or_create_faiss_index.return_value = vs
            rag_svc.return_value.__enter__.return_value = rag_inst
            rag_svc.return_value.__exit__.return_value = None

            results = engine.search("q")

        meta = results[0]["metadata"]
        assert meta["collection_id"] == 7
        assert meta["collection_name"] == "My Coll"

    def test_none_metadata_gets_initialized(self):
        """If doc.metadata is None, it should be set to a dict."""
        doc = _make_doc(page_content="text", metadata=None)
        engine = _make_engine(settings_snapshot={"_username": "u"})

        with (
            patch(_PATCH_LIB_SERVICE) as lib_svc,
            patch(_PATCH_DB_SESSION) as db_sess,
            patch(_PATCH_RAG_SERVICE) as rag_svc,
        ):
            lib_svc.return_value.get_all_collections.return_value = [
                {"id": 1, "name": "C"}
            ]
            db_sess.return_value.__enter__.return_value.query.return_value.filter_by.return_value.first.return_value = _mock_rag_index()
            rag_inst = Mock()
            rag_inst.get_rag_stats.return_value = {"indexed_documents": 1}
            vs = Mock()
            vs.similarity_search_with_score.return_value = [(doc, 0.1)]
            rag_inst.load_or_create_faiss_index.return_value = vs
            rag_svc.return_value.__enter__.return_value = rag_inst
            rag_svc.return_value.__exit__.return_value = None

            results = engine.search("q")

        # Should not crash; metadata should contain collection info
        assert results[0]["metadata"]["collection_id"] == 1


# ---------------------------------------------------------------------------
# _get_previews delegation
# ---------------------------------------------------------------------------
class TestGetPreviews:
    def test_delegates_all_args(self):
        engine = _make_engine()
        sentinel = [{"title": "x"}]
        with patch.object(engine, "search", return_value=sentinel) as m:
            result = engine._get_previews(
                "query", limit=3, llm_callback="cb", extra_params={"k": "v"}
            )
            m.assert_called_once_with("query", 3, "cb", {"k": "v"})
            assert result is sentinel


# ---------------------------------------------------------------------------
# Top-level search error handling
# ---------------------------------------------------------------------------
class TestSearchTopLevelErrors:
    def test_search_returns_empty_on_exception(self):
        engine = _make_engine(settings_snapshot={"_username": "u"})

        with patch(_PATCH_LIB_SERVICE) as lib_svc:
            lib_svc.side_effect = Exception("total failure")
            results = engine.search("q")

        assert results == []

    def test_search_empty_when_no_results_across_collections(self):
        engine = _make_engine(settings_snapshot={"_username": "u"})

        with (
            patch(_PATCH_LIB_SERVICE) as lib_svc,
            patch(_PATCH_DB_SESSION) as db_sess,
            patch(_PATCH_RAG_SERVICE),
        ):
            lib_svc.return_value.get_all_collections.return_value = [
                {"id": 1, "name": "C"}
            ]
            # No RAG index -> skip
            db_sess.return_value.__enter__.return_value.query.return_value.filter_by.return_value.first.return_value = None

            results = engine.search("q")

        assert results == []


# ---------------------------------------------------------------------------
# close()
# ---------------------------------------------------------------------------
class TestClose:
    def test_close_is_noop(self):
        engine = _make_engine()
        engine.close()  # Should not raise


# ---------------------------------------------------------------------------
# Result structure completeness
# ---------------------------------------------------------------------------
class TestResultStructure:
    """Each result dict should contain all required keys."""

    def test_all_keys_present(self):
        doc = _make_doc(
            page_content="content",
            metadata={"source_id": "1", "document_title": "T"},
        )
        engine = _make_engine(settings_snapshot={"_username": "u"})

        with (
            patch(_PATCH_LIB_SERVICE) as lib_svc,
            patch(_PATCH_DB_SESSION) as db_sess,
            patch(_PATCH_RAG_SERVICE) as rag_svc,
        ):
            lib_svc.return_value.get_all_collections.return_value = [
                {"id": 1, "name": "C"}
            ]
            db_sess.return_value.__enter__.return_value.query.return_value.filter_by.return_value.first.return_value = _mock_rag_index()
            rag_inst = Mock()
            rag_inst.get_rag_stats.return_value = {"indexed_documents": 1}
            vs = Mock()
            vs.similarity_search_with_score.return_value = [(doc, 0.5)]
            rag_inst.load_or_create_faiss_index.return_value = vs
            rag_svc.return_value.__enter__.return_value = rag_inst
            rag_svc.return_value.__exit__.return_value = None

            results = engine.search("q")

        r = results[0]
        expected_keys = {
            "title",
            "snippet",
            "url",
            "link",
            "source",
            "source_type",
            "relevance_score",
            "metadata",
        }
        assert expected_keys.issubset(r.keys())
        assert r["source"] == "library"
        assert r["source_type"] == "library"
        assert r["link"] == r["url"]
        assert isinstance(r["relevance_score"], float)
