"""
Tests for LibraryRAGSearchEngine (search_engine_library.py, 0% coverage).

Covers:
- __init__: initialization with/without username
- search: no username, no collections, exception handling
- _get_previews: delegates to search
- _get_full_content: no username, no doc_id, success path
- close: no-op
"""

from unittest.mock import MagicMock, Mock, patch


MODULE = "local_deep_research.web_search_engines.engines.search_engine_library"


def _make_engine(username="testuser", settings_snapshot=None):
    """Create a LibraryRAGSearchEngine with mocked dependencies."""
    from local_deep_research.web_search_engines.engines.search_engine_library import (
        LibraryRAGSearchEngine,
    )

    snapshot = settings_snapshot or {"_username": username}
    engine = LibraryRAGSearchEngine(
        llm=MagicMock(),
        max_results=10,
        settings_snapshot=snapshot,
    )
    return engine


class TestLibraryRAGSearchEngineInit:
    def test_init_with_username(self):
        engine = _make_engine(username="testuser")
        assert engine.username == "testuser"
        assert engine.is_local is True

    def test_init_without_username(self):
        engine = _make_engine(settings_snapshot={"_username": None})
        assert engine.username is None

    def test_init_reads_embedding_settings(self):
        engine = _make_engine(username="user1")
        # Defaults should be set
        assert engine.embedding_model is not None
        assert engine.chunk_size is not None


class TestSearch:
    def test_search_no_username_returns_empty(self):
        engine = _make_engine(settings_snapshot={"_username": None})
        result = engine.search("test query")
        assert result == []

    def test_search_no_collections_returns_empty(self):
        engine = _make_engine()
        mock_service = MagicMock()
        mock_service.get_all_collections.return_value = []
        with patch(f"{MODULE}.LibraryService", return_value=mock_service):
            result = engine.search("test query")
        assert result == []

    def test_search_exception_propagates(self):
        """A failed search raises instead of masquerading as no results."""
        import pytest

        engine = _make_engine()
        with patch(
            f"{MODULE}.LibraryService", side_effect=RuntimeError("fail")
        ):
            with pytest.raises(RuntimeError, match="fail"):
                engine.search("test query")

    def test_search_collection_no_rag_index_skips(self):
        engine = _make_engine()
        mock_service = MagicMock()
        mock_service.get_all_collections.return_value = [
            {"id": "col1", "name": "Test Collection"}
        ]

        mock_session = MagicMock()
        mock_session.__enter__ = Mock(return_value=mock_session)
        mock_session.__exit__ = Mock(return_value=False)
        # RAGIndex query returns None
        mock_session.query.return_value.filter_by.return_value.first.return_value = None

        with patch(f"{MODULE}.LibraryService", return_value=mock_service):
            with patch(
                f"{MODULE}.get_user_db_session", return_value=mock_session
            ):
                result = engine.search("test query")

        assert result == []

    def test_all_collections_fail_with_no_results_raises(self):
        """Zero results + collection errors must not look like no matches."""
        import pytest

        engine = _make_engine()
        mock_service = MagicMock()
        mock_service.get_all_collections.return_value = [
            {"id": "col1", "name": "Collection 1"},
        ]

        with patch(f"{MODULE}.LibraryService", return_value=mock_service):
            with patch(
                f"{MODULE}.get_user_db_session",
                side_effect=RuntimeError("db error"),
            ):
                with pytest.raises(RuntimeError, match="failed for 1"):
                    engine.search("test query")

    @staticmethod
    def _run_partial_failure_search(engine):
        """Search two collections where col1 errors and col2 returns a hit."""
        mock_service = MagicMock()
        mock_service.get_all_collections.return_value = [
            {"id": "col1", "name": "Collection 1"},
            {"id": "col2", "name": "Collection 2"},
        ]

        mock_session = MagicMock()
        mock_session.__enter__ = Mock(return_value=mock_session)
        mock_session.__exit__ = Mock(return_value=False)
        mock_rag_index = MagicMock()
        mock_rag_index.embedding_model = "all-MiniLM-L6-v2"
        mock_rag_index.embedding_model_type = MagicMock(
            value="sentence_transformers"
        )
        mock_rag_index.chunk_size = 1000
        mock_rag_index.chunk_overlap = 200
        mock_session.query.return_value.filter_by.return_value.first.return_value = mock_rag_index

        # col1 fails when its RAG service is created; col2 succeeds
        mock_doc = MagicMock()
        mock_doc.metadata = {}
        mock_doc.page_content = "matching text from collection 2"
        mock_vector_store = MagicMock()
        mock_vector_store.similarity_search_with_score.return_value = [
            (mock_doc, 0.5)
        ]
        mock_rag_service = MagicMock()
        mock_rag_service.__enter__ = Mock(return_value=mock_rag_service)
        mock_rag_service.__exit__ = Mock(return_value=False)
        mock_rag_service.get_rag_stats.return_value = {"indexed_documents": 1}
        mock_rag_service.load_or_create_faiss_index.return_value = (
            mock_vector_store
        )

        with patch(f"{MODULE}.LibraryService", return_value=mock_service):
            with patch(
                f"{MODULE}.get_user_db_session", return_value=mock_session
            ):
                with patch(
                    f"{MODULE}.LibraryRAGService",
                    side_effect=[
                        RuntimeError("col1 broken"),
                        mock_rag_service,
                    ],
                ):
                    result = engine.search("test query")

        return result

    def test_search_collection_exception_continues(self):
        """Exception in one collection doesn't stop search of others."""
        engine = _make_engine()

        result = self._run_partial_failure_search(engine)

        assert len(result) == 1
        assert result[0]["metadata"]["collection_name"] == "Collection 2"

    def test_partial_failure_warns_results_incomplete(self, loguru_caplog):
        """Partial success warns the user (research log) by collection name."""
        engine = _make_engine()

        with loguru_caplog.at_level("WARNING"):
            result = self._run_partial_failure_search(engine)

        assert len(result) == 1
        assert "1 of 2 collection(s) failed" in loguru_caplog.text
        assert "Collection 1" in loguru_caplog.text

    def test_search_no_results_across_collections(self):
        """When all collections have no indexed docs, returns empty."""
        engine = _make_engine()
        mock_service = MagicMock()
        mock_service.get_all_collections.return_value = [
            {"id": "col1", "name": "Empty Collection"}
        ]

        mock_session = MagicMock()
        mock_session.__enter__ = Mock(return_value=mock_session)
        mock_session.__exit__ = Mock(return_value=False)

        mock_rag_index = MagicMock()
        mock_rag_index.embedding_model = "all-MiniLM-L6-v2"
        mock_rag_index.embedding_model_type = MagicMock(
            value="sentence_transformers"
        )
        mock_rag_index.chunk_size = 1000
        mock_rag_index.chunk_overlap = 200
        mock_session.query.return_value.filter_by.return_value.first.return_value = mock_rag_index

        mock_rag_service = MagicMock()
        mock_rag_service.__enter__ = Mock(return_value=mock_rag_service)
        mock_rag_service.__exit__ = Mock(return_value=False)
        mock_rag_service.get_rag_stats.return_value = {"indexed_documents": 0}

        with patch(f"{MODULE}.LibraryService", return_value=mock_service):
            with patch(
                f"{MODULE}.get_user_db_session", return_value=mock_session
            ):
                with patch(
                    f"{MODULE}.LibraryRAGService", return_value=mock_rag_service
                ):
                    result = engine.search("test query")

        assert result == []


class TestGetPreviews:
    def test_delegates_to_search(self):
        engine = _make_engine()
        with patch.object(
            engine, "search", return_value=[{"title": "test"}]
        ) as mock_search:
            result = engine._get_previews("test query", limit=5)
        mock_search.assert_called_once_with("test query", 5, None, None)
        assert result == [{"title": "test"}]


class TestGetFullContent:
    def test_items_without_document_id_returned_unchanged(self):
        """Items lacking metadata.document_id are skipped and returned as-is."""
        engine = _make_engine()
        items = [{"title": "test", "snippet": "content"}]

        result = engine._get_full_content(items)

        assert result == items

    def test_no_username_returns_items(self):
        engine = _make_engine(settings_snapshot={"_username": None})
        items = [{"title": "test"}]
        result = engine._get_full_content(items)
        assert result == items

    def test_exception_returns_items(self):
        engine = _make_engine()
        items = [{"title": "test", "metadata": {"document_id": "doc1"}}]

        with patch(
            f"{MODULE}.get_user_db_session",
            side_effect=RuntimeError("fail"),
        ):
            result = engine._get_full_content(items)

        assert result == items


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