"""
Coverage tests for LibraryRAGService.

Focuses on logic paths not exercised by the existing test_library_rag_service.py:
- _get_index_hash edge cases
- _get_index_path cache directory details
- _deduplicate_chunks order preservation and empty input
- _get_or_create_rag_index new vs existing
- load_or_create_faiss_index HNSW/IVF/L2/IP variants, integrity failure, dimension mismatch,
  load failure, corrupted unlink failure
- index_document chunk indexing, empty text, skip already indexed
- index_all_documents with progress callback, stores settings
"""

import hashlib
from pathlib import Path
from unittest.mock import MagicMock, patch

from langchain_core.documents import Document as LangchainDocument

# ---------------------------------------------------------------------------
# Module-level patch path prefix
# ---------------------------------------------------------------------------
_MOD = "local_deep_research.research_library.services.library_rag_service"


def _make_service(**overrides):
    """Create a LibraryRAGService with all external deps mocked out."""
    with (
        patch(f"{_MOD}.LocalEmbeddingManager") as _lem,
        patch(f"{_MOD}.get_user_db_session"),
        patch(f"{_MOD}.FileIntegrityManager") as _fim,
        patch(f"{_MOD}.get_text_splitter") as _gts,
    ):
        _lem.return_value.embeddings = MagicMock()
        from local_deep_research.research_library.services.library_rag_service import (
            LibraryRAGService,
        )

        defaults = dict(username="testuser", db_password="pw")
        defaults.update(overrides)
        svc = LibraryRAGService(**defaults)
    return svc


# =========================================================================
# _get_index_hash
# =========================================================================
class TestGetIndexHash:
    """Tests for _get_index_hash determinism and sensitivity."""

    def test_hash_is_deterministic(self):
        svc = _make_service()
        h1 = svc._get_index_hash("col_a", "model_x", "sentence_transformers")
        h2 = svc._get_index_hash("col_a", "model_x", "sentence_transformers")
        assert h1 == h2

    def test_hash_changes_with_collection_name(self):
        svc = _make_service()
        h1 = svc._get_index_hash("col_a", "model_x", "sentence_transformers")
        h2 = svc._get_index_hash("col_b", "model_x", "sentence_transformers")
        assert h1 != h2

    def test_hash_changes_with_model(self):
        svc = _make_service()
        h1 = svc._get_index_hash("col_a", "model_x", "sentence_transformers")
        h2 = svc._get_index_hash("col_a", "model_y", "sentence_transformers")
        assert h1 != h2

    def test_hash_changes_with_provider(self):
        svc = _make_service()
        h1 = svc._get_index_hash("col_a", "model_x", "sentence_transformers")
        h2 = svc._get_index_hash("col_a", "model_x", "ollama")
        assert h1 != h2

    def test_hash_is_sha256_hex(self):
        svc = _make_service()
        h = svc._get_index_hash("c", "m", "p")
        expected = hashlib.sha256("c:m:p".encode()).hexdigest()
        assert h == expected

    def test_hash_length_is_64_chars(self):
        svc = _make_service()
        h = svc._get_index_hash("x", "y", "z")
        assert len(h) == 64


# =========================================================================
# _get_index_path
# =========================================================================
class TestGetIndexPath:
    """Tests for _get_index_path."""

    @patch(f"{_MOD}.get_cache_directory")
    def test_path_under_rag_indices_subdir(self, mock_cache_dir, tmp_path):
        mock_cache_dir.return_value = tmp_path
        svc = _make_service()
        p = svc._get_index_path("abc123")
        assert p.parent.name == "rag_indices"

    @patch(f"{_MOD}.get_cache_directory")
    def test_path_filename_contains_hash(self, mock_cache_dir, tmp_path):
        mock_cache_dir.return_value = tmp_path
        svc = _make_service()
        p = svc._get_index_path("abc123")
        assert p.name == "abc123.faiss"

    @patch(f"{_MOD}.get_cache_directory")
    def test_path_creates_directory(self, mock_cache_dir, tmp_path):
        mock_cache_dir.return_value = tmp_path
        svc = _make_service()
        svc._get_index_path("abc123")
        assert (tmp_path / "rag_indices").is_dir()

    @patch(f"{_MOD}.get_cache_directory")
    def test_path_returns_path_object(self, mock_cache_dir, tmp_path):
        mock_cache_dir.return_value = tmp_path
        svc = _make_service()
        p = svc._get_index_path("somehash")
        assert isinstance(p, Path)


# =========================================================================
# _deduplicate_chunks  (static method)
# =========================================================================
class TestDeduplicateChunks:
    """Tests for _deduplicate_chunks."""

    def _doc(self, text):
        return LangchainDocument(page_content=text)

    def test_empty_input_returns_empty(self):
        from local_deep_research.research_library.services.library_rag_service import (
            LibraryRAGService,
        )

        chunks, ids = LibraryRAGService._deduplicate_chunks([], [])
        assert chunks == []
        assert ids == []

    def test_no_duplicates_preserved(self):
        from local_deep_research.research_library.services.library_rag_service import (
            LibraryRAGService,
        )

        d1, d2 = self._doc("a"), self._doc("b")
        chunks, ids = LibraryRAGService._deduplicate_chunks(
            [d1, d2], ["id1", "id2"]
        )
        assert ids == ["id1", "id2"]
        assert chunks == [d1, d2]

    def test_duplicate_ids_keeps_first(self):
        from local_deep_research.research_library.services.library_rag_service import (
            LibraryRAGService,
        )

        d1, d2, d3 = self._doc("a"), self._doc("b"), self._doc("c")
        chunks, ids = LibraryRAGService._deduplicate_chunks(
            [d1, d2, d3], ["id1", "id1", "id2"]
        )
        assert ids == ["id1", "id2"]
        assert chunks == [d1, d3]

    def test_existing_ids_excluded(self):
        from local_deep_research.research_library.services.library_rag_service import (
            LibraryRAGService,
        )

        d1, d2 = self._doc("a"), self._doc("b")
        chunks, ids = LibraryRAGService._deduplicate_chunks(
            [d1, d2], ["id1", "id2"], existing_ids={"id1"}
        )
        assert ids == ["id2"]
        assert chunks == [d2]

    def test_none_existing_ids_allows_all(self):
        from local_deep_research.research_library.services.library_rag_service import (
            LibraryRAGService,
        )

        d1 = self._doc("a")
        chunks, ids = LibraryRAGService._deduplicate_chunks(
            [d1], ["id1"], existing_ids=None
        )
        assert ids == ["id1"]

    def test_order_preservation(self):
        from local_deep_research.research_library.services.library_rag_service import (
            LibraryRAGService,
        )

        docs = [self._doc(str(i)) for i in range(5)]
        id_list = ["e", "d", "c", "b", "a"]
        chunks, ids = LibraryRAGService._deduplicate_chunks(docs, id_list)
        assert ids == ["e", "d", "c", "b", "a"]

    def test_all_existing_returns_empty(self):
        from local_deep_research.research_library.services.library_rag_service import (
            LibraryRAGService,
        )

        d1, d2 = self._doc("a"), self._doc("b")
        chunks, ids = LibraryRAGService._deduplicate_chunks(
            [d1, d2], ["id1", "id2"], existing_ids={"id1", "id2"}
        )
        assert chunks == []
        assert ids == []


# =========================================================================
# _get_or_create_rag_index
# =========================================================================
class TestGetOrCreateRagIndex:
    """Tests for _get_or_create_rag_index."""

    @patch(f"{_MOD}.get_user_db_session")
    def test_returns_existing_index(self, mock_session_ctx):
        svc = _make_service()
        mock_session = MagicMock()
        mock_session_ctx.return_value.__enter__ = MagicMock(
            return_value=mock_session
        )
        mock_session_ctx.return_value.__exit__ = MagicMock(return_value=None)

        existing_index = MagicMock()
        mock_session.query.return_value.filter_by.return_value.first.return_value = existing_index

        result = svc._get_or_create_rag_index("coll-123")
        assert result is existing_index
        # Should NOT call session.add since index already existed
        mock_session.add.assert_not_called()

    @patch(f"{_MOD}.get_cache_directory")
    @patch(f"{_MOD}.get_user_db_session")
    def test_creates_new_index_when_none_exists(
        self, mock_session_ctx, mock_cache_dir, tmp_path
    ):
        mock_cache_dir.return_value = tmp_path
        svc = _make_service()
        svc.embedding_manager = MagicMock()
        svc.embedding_manager.embeddings.embed_query.return_value = [0.0] * 384

        mock_session = MagicMock()
        mock_session_ctx.return_value.__enter__ = MagicMock(
            return_value=mock_session
        )
        mock_session_ctx.return_value.__exit__ = MagicMock(return_value=None)

        # No existing index found
        mock_session.query.return_value.filter_by.return_value.first.return_value = None

        svc._get_or_create_rag_index("coll-456")

        # Should have called session.add for the new RAGIndex
        mock_session.add.assert_called_once()
        mock_session.commit.assert_called_once()
        mock_session.refresh.assert_called_once()

    @patch(f"{_MOD}.get_cache_directory")
    @patch(f"{_MOD}.get_user_db_session")
    def test_embeds_test_string_for_dimension(
        self, mock_session_ctx, mock_cache_dir, tmp_path
    ):
        mock_cache_dir.return_value = tmp_path
        svc = _make_service()
        svc.embedding_manager = MagicMock()
        svc.embedding_manager.embeddings.embed_query.return_value = [0.1] * 768

        mock_session = MagicMock()
        mock_session_ctx.return_value.__enter__ = MagicMock(
            return_value=mock_session
        )
        mock_session_ctx.return_value.__exit__ = MagicMock(return_value=None)
        mock_session.query.return_value.filter_by.return_value.first.return_value = None

        svc._get_or_create_rag_index("coll-789")

        svc.embedding_manager.embeddings.embed_query.assert_called_once_with(
            "test"
        )


# =========================================================================
# load_or_create_faiss_index
# =========================================================================
class TestLoadOrCreateFaissIndex:
    """Tests for load_or_create_faiss_index."""

    def _patch_get_or_create(self, svc, rag_index):
        """Patch _get_or_create_rag_index on a service instance."""
        svc._get_or_create_rag_index = MagicMock(return_value=rag_index)

    def _make_rag_index(self, index_path="/tmp/test.faiss", dim=384):
        idx = MagicMock()
        idx.index_path = index_path
        idx.embedding_dimension = dim
        idx.id = "rag-idx-1"
        return idx

    @patch(f"{_MOD}.FAISS")
    @patch(f"{_MOD}.InMemoryDocstore")
    @patch(f"{_MOD}.IndexFlatIP")
    def test_creates_flat_ip_for_cosine(
        self, mock_flat_ip, mock_docstore, mock_faiss
    ):
        svc = _make_service(distance_metric="cosine", index_type="flat")
        rag_idx = self._make_rag_index(index_path="/nonexistent/test.faiss")
        self._patch_get_or_create(svc, rag_idx)
        svc.embedding_manager = MagicMock()

        svc.load_or_create_faiss_index("coll-1")
        mock_flat_ip.assert_called_once_with(384)

    @patch(f"{_MOD}.FAISS")
    @patch(f"{_MOD}.InMemoryDocstore")
    @patch(f"{_MOD}.IndexFlatL2")
    def test_creates_flat_l2_for_l2_metric(
        self, mock_flat_l2, mock_docstore, mock_faiss
    ):
        svc = _make_service(distance_metric="l2", index_type="flat")
        rag_idx = self._make_rag_index(index_path="/nonexistent/test.faiss")
        self._patch_get_or_create(svc, rag_idx)
        svc.embedding_manager = MagicMock()

        svc.load_or_create_faiss_index("coll-1")
        mock_flat_l2.assert_called_once_with(384)

    @patch(f"{_MOD}.FAISS")
    @patch(f"{_MOD}.InMemoryDocstore")
    @patch(f"{_MOD}.IndexHNSWFlat")
    def test_creates_hnsw_index(self, mock_hnsw, mock_docstore, mock_faiss):
        svc = _make_service(index_type="hnsw")
        rag_idx = self._make_rag_index(index_path="/nonexistent/test.faiss")
        self._patch_get_or_create(svc, rag_idx)
        svc.embedding_manager = MagicMock()

        svc.load_or_create_faiss_index("coll-1")
        mock_hnsw.assert_called_once_with(384, 32)

    @patch(f"{_MOD}.FAISS")
    @patch(f"{_MOD}.InMemoryDocstore")
    @patch(f"{_MOD}.IndexFlatIP")
    def test_ivf_falls_back_to_flat_ip_for_cosine(
        self, mock_flat_ip, mock_docstore, mock_faiss
    ):
        svc = _make_service(index_type="ivf", distance_metric="cosine")
        rag_idx = self._make_rag_index(index_path="/nonexistent/test.faiss")
        self._patch_get_or_create(svc, rag_idx)
        svc.embedding_manager = MagicMock()

        svc.load_or_create_faiss_index("coll-1")
        mock_flat_ip.assert_called_once_with(384)

    @patch(f"{_MOD}.FAISS")
    @patch(f"{_MOD}.InMemoryDocstore")
    @patch(f"{_MOD}.IndexFlatL2")
    def test_ivf_falls_back_to_flat_l2_for_l2(
        self, mock_flat_l2, mock_docstore, mock_faiss
    ):
        svc = _make_service(index_type="ivf", distance_metric="l2")
        rag_idx = self._make_rag_index(index_path="/nonexistent/test.faiss")
        self._patch_get_or_create(svc, rag_idx)
        svc.embedding_manager = MagicMock()

        svc.load_or_create_faiss_index("coll-1")
        mock_flat_l2.assert_called_once_with(384)

    @patch(f"{_MOD}.FAISS")
    @patch(f"{_MOD}.InMemoryDocstore")
    @patch(f"{_MOD}.IndexFlatIP")
    def test_verified_load_returns_existing_index(
        self, mock_flat_ip, mock_docstore, mock_faiss_cls
    ):
        svc = _make_service()
        svc.embedding_manager = MagicMock()
        svc.embedding_manager.embeddings.embed_query.return_value = [0.0] * 384
        svc.integrity_manager = MagicMock()
        svc.integrity_manager.verify_file.return_value = (True, None)

        rag_idx = self._make_rag_index(dim=384)
        rag_idx.index_path = "/tmp/existing.faiss"
        self._patch_get_or_create(svc, rag_idx)

        mock_loaded = MagicMock()
        mock_faiss_cls.load_local.return_value = mock_loaded

        with patch.object(Path, "exists", return_value=True):
            result = svc.load_or_create_faiss_index("coll-1")

        assert result is mock_loaded

    @patch(f"{_MOD}.FAISS")
    @patch(f"{_MOD}.InMemoryDocstore")
    @patch(f"{_MOD}.IndexFlatIP")
    def test_integrity_failure_creates_new_index(
        self, mock_flat_ip, mock_docstore, mock_faiss_cls
    ):
        svc = _make_service()
        svc.embedding_manager = MagicMock()
        svc.integrity_manager = MagicMock()
        svc.integrity_manager.verify_file.return_value = (
            False,
            "hash mismatch",
        )

        rag_idx = self._make_rag_index()
        rag_idx.index_path = "/tmp/corrupt.faiss"
        self._patch_get_or_create(svc, rag_idx)

        with (
            patch.object(Path, "exists", return_value=True),
            patch.object(Path, "unlink") as mock_unlink,
        ):
            svc.load_or_create_faiss_index("coll-1")

        # Should have tried to unlink the corrupted file
        mock_unlink.assert_called_once()
        # Should return a new FAISS instance, not load_local
        mock_faiss_cls.load_local.assert_not_called()
        mock_faiss_cls.assert_called_once()

    @patch(f"{_MOD}.FAISS")
    @patch(f"{_MOD}.InMemoryDocstore")
    @patch(f"{_MOD}.IndexFlatIP")
    def test_corrupted_unlink_failure_still_creates_new_index(
        self, mock_flat_ip, mock_docstore, mock_faiss_cls
    ):
        svc = _make_service()
        svc.embedding_manager = MagicMock()
        svc.integrity_manager = MagicMock()
        svc.integrity_manager.verify_file.return_value = (False, "bad hash")

        rag_idx = self._make_rag_index()
        rag_idx.index_path = "/tmp/corrupt.faiss"
        self._patch_get_or_create(svc, rag_idx)

        with (
            patch.object(Path, "exists", return_value=True),
            patch.object(Path, "unlink", side_effect=PermissionError("denied")),
        ):
            svc.load_or_create_faiss_index("coll-1")

        # Should still create a new FAISS, not crash
        mock_faiss_cls.assert_called_once()

    @patch(f"{_MOD}.get_user_db_session")
    @patch(f"{_MOD}.FAISS")
    @patch(f"{_MOD}.InMemoryDocstore")
    @patch(f"{_MOD}.IndexFlatIP")
    def test_dimension_mismatch_deletes_and_rebuilds(
        self, mock_flat_ip, mock_docstore, mock_faiss_cls, mock_session_ctx
    ):
        svc = _make_service()
        svc.embedding_manager = MagicMock()
        # Current model returns dim 768 but index was stored with 384
        svc.embedding_manager.embeddings.embed_query.return_value = [0.0] * 768
        svc.integrity_manager = MagicMock()
        svc.integrity_manager.verify_file.return_value = (True, None)

        mock_session = MagicMock()
        mock_session_ctx.return_value.__enter__ = MagicMock(
            return_value=mock_session
        )
        mock_session_ctx.return_value.__exit__ = MagicMock(return_value=None)
        mock_session.query.return_value.filter_by.return_value.first.return_value = MagicMock()

        rag_idx = self._make_rag_index(dim=384)
        rag_idx.index_path = "/tmp/old_dim.faiss"
        self._patch_get_or_create(svc, rag_idx)

        with (
            patch.object(Path, "exists", return_value=True),
            patch.object(Path, "unlink") as mock_unlink,
            patch.object(Path, "with_suffix") as mock_with_suffix,
        ):
            mock_pkl = MagicMock()
            mock_pkl.exists.return_value = True
            mock_with_suffix.return_value = mock_pkl
            svc.load_or_create_faiss_index("coll-1")

        # The old file should have been unlinked
        mock_unlink.assert_called()
        # A new FAISS should be created (not loaded)
        mock_faiss_cls.assert_called_once()

    @patch(f"{_MOD}.FAISS")
    @patch(f"{_MOD}.InMemoryDocstore")
    @patch(f"{_MOD}.IndexFlatIP")
    def test_load_failure_creates_new_index(
        self, mock_flat_ip, mock_docstore, mock_faiss_cls
    ):
        svc = _make_service()
        svc.embedding_manager = MagicMock()
        svc.embedding_manager.embeddings.embed_query.return_value = [0.0] * 384
        svc.integrity_manager = MagicMock()
        svc.integrity_manager.verify_file.return_value = (True, None)

        rag_idx = self._make_rag_index(dim=384)
        rag_idx.index_path = "/tmp/broken.faiss"
        self._patch_get_or_create(svc, rag_idx)

        mock_faiss_cls.load_local.side_effect = RuntimeError("corrupted file")

        with patch.object(Path, "exists", return_value=True):
            svc.load_or_create_faiss_index("coll-1")

        # Should fall through and create new index
        mock_faiss_cls.assert_called_once()

    @patch(f"{_MOD}.FAISS")
    @patch(f"{_MOD}.InMemoryDocstore")
    @patch(f"{_MOD}.IndexFlatIP")
    def test_normalize_vectors_passed_to_faiss(
        self, mock_flat_ip, mock_docstore, mock_faiss_cls
    ):
        svc = _make_service(normalize_vectors=False)
        rag_idx = self._make_rag_index(index_path="/nonexistent/test.faiss")
        self._patch_get_or_create(svc, rag_idx)
        svc.embedding_manager = MagicMock()

        svc.load_or_create_faiss_index("coll-1")
        call_kwargs = mock_faiss_cls.call_args
        assert call_kwargs[1]["normalize_L2"] is False


# =========================================================================
# index_document
# =========================================================================
class TestIndexDocument:
    """Tests for index_document."""

    @patch(f"{_MOD}.get_user_db_session")
    def test_returns_error_when_document_not_found(self, mock_session_ctx):
        svc = _make_service()
        mock_session = MagicMock()
        mock_session_ctx.return_value.__enter__ = MagicMock(
            return_value=mock_session
        )
        mock_session_ctx.return_value.__exit__ = MagicMock(return_value=None)
        mock_session.query.return_value.filter_by.return_value.first.return_value = None

        result = svc.index_document("doc-1", "coll-1")
        assert result["status"] == "error"
        assert "not found" in result["error"]

    @patch(f"{_MOD}.get_user_db_session")
    def test_returns_error_when_no_text_content(self, mock_session_ctx):
        svc = _make_service()
        mock_session = MagicMock()
        mock_session_ctx.return_value.__enter__ = MagicMock(
            return_value=mock_session
        )
        mock_session_ctx.return_value.__exit__ = MagicMock(return_value=None)

        mock_document = MagicMock()
        mock_document.text_content = None

        mock_doc_collection = MagicMock()
        mock_doc_collection.indexed = False

        # query(Document).filter_by().first() -> document
        # query(DocumentCollection).filter_by().all() -> [doc_collection]
        def side_effect_query(model):
            q = MagicMock()
            if model.__name__ == "Document":
                q.filter_by.return_value.first.return_value = mock_document
            elif model.__name__ == "DocumentCollection":
                q.filter_by.return_value.all.return_value = [
                    mock_doc_collection
                ]
            return q

        mock_session.query = MagicMock(side_effect=side_effect_query)

        result = svc.index_document("doc-1", "coll-1")
        assert result["status"] == "error"
        assert "no text content" in result["error"]

    @patch(f"{_MOD}.get_user_db_session")
    def test_skips_already_indexed_document(self, mock_session_ctx):
        svc = _make_service()
        mock_session = MagicMock()
        mock_session_ctx.return_value.__enter__ = MagicMock(
            return_value=mock_session
        )
        mock_session_ctx.return_value.__exit__ = MagicMock(return_value=None)

        mock_document = MagicMock()
        mock_doc_collection = MagicMock()
        mock_doc_collection.indexed = True
        mock_doc_collection.chunk_count = 42

        def side_effect_query(model):
            q = MagicMock()
            if model.__name__ == "Document":
                q.filter_by.return_value.first.return_value = mock_document
            elif model.__name__ == "DocumentCollection":
                q.filter_by.return_value.all.return_value = [
                    mock_doc_collection
                ]
            return q

        mock_session.query = MagicMock(side_effect=side_effect_query)

        result = svc.index_document("doc-1", "coll-1", force_reindex=False)
        assert result["status"] == "skipped"
        assert result["chunk_count"] == 42


# =========================================================================
# index_all_documents (index_collection in the spec)
# =========================================================================
class TestIndexAllDocuments:
    """Tests for index_all_documents."""

    @patch(f"{_MOD}.get_user_db_session")
    def test_no_documents_returns_info(self, mock_session_ctx):
        svc = _make_service()
        mock_session = MagicMock()
        mock_session_ctx.return_value.__enter__ = MagicMock(
            return_value=mock_session
        )
        mock_session_ctx.return_value.__exit__ = MagicMock(return_value=None)
        mock_session.query.return_value.filter_by.return_value.filter_by.return_value.all.return_value = []

        result = svc.index_all_documents("coll-1")
        assert result["status"] == "info"
        assert result["successful"] == 0

    @patch(f"{_MOD}.get_user_db_session")
    def test_progress_callback_invoked(self, mock_session_ctx):
        svc = _make_service()
        mock_session = MagicMock()
        mock_session_ctx.return_value.__enter__ = MagicMock(
            return_value=mock_session
        )
        mock_session_ctx.return_value.__exit__ = MagicMock(return_value=None)

        mock_dc1 = MagicMock()
        mock_dc1.document_id = "doc-1"
        mock_dc2 = MagicMock()
        mock_dc2.document_id = "doc-2"

        mock_doc = MagicMock()
        mock_doc.title = "Test Doc"

        # filter_by(collection_id=...) -> filter_by(indexed=False) -> all()
        mock_session.query.return_value.filter_by.return_value.filter_by.return_value.all.return_value = [
            mock_dc1,
            mock_dc2,
        ]
        # query(Document).filter_by(id=...).first() for title lookup
        mock_session.query.return_value.filter_by.return_value.first.return_value = mock_doc

        # Mock index_document to return success
        svc.index_document = MagicMock(
            return_value={"status": "success", "chunk_count": 10}
        )

        callback = MagicMock()
        svc.index_all_documents("coll-1", progress_callback=callback)

        assert callback.call_count == 2
        # Check the callback was called with (idx, total, title, status)
        first_call = callback.call_args_list[0]
        assert first_call[0][0] == 1  # idx
        assert first_call[0][1] == 2  # total
        assert first_call[0][3] == "success"  # status

    @patch(f"{_MOD}.get_user_db_session")
    def test_counts_successful_skipped_failed(self, mock_session_ctx):
        svc = _make_service()
        mock_session = MagicMock()
        mock_session_ctx.return_value.__enter__ = MagicMock(
            return_value=mock_session
        )
        mock_session_ctx.return_value.__exit__ = MagicMock(return_value=None)

        mock_dc1 = MagicMock()
        mock_dc1.document_id = "doc-1"
        mock_dc2 = MagicMock()
        mock_dc2.document_id = "doc-2"
        mock_dc3 = MagicMock()
        mock_dc3.document_id = "doc-3"

        mock_doc = MagicMock()
        mock_doc.title = "Title"

        # With force_reindex=True the code does:
        #   query(DocumentCollection).filter_by(collection_id=...).all()
        # (no second filter_by for indexed=False)
        mock_query = MagicMock()
        mock_query.filter_by.return_value.all.return_value = [
            mock_dc1,
            mock_dc2,
            mock_dc3,
        ]
        # Document title lookup
        mock_query.filter_by.return_value.first.return_value = mock_doc
        mock_session.query.return_value = mock_query

        results_sequence = [
            {"status": "success", "chunk_count": 5},
            {
                "status": "skipped",
                "message": "already indexed",
                "chunk_count": 3,
            },
            {"status": "error", "error": "something broke"},
        ]
        svc.index_document = MagicMock(side_effect=results_sequence)

        result = svc.index_all_documents("coll-1", force_reindex=True)
        assert result["successful"] == 1
        assert result["skipped"] == 1
        assert result["failed"] == 1
        assert len(result["errors"]) == 1


# =========================================================================
# db_password property
# =========================================================================
class TestDbPasswordProperty:
    """Tests for the db_password property propagation."""

    def test_getter_returns_value(self):
        svc = _make_service()
        svc._db_password = "secret"
        assert svc.db_password == "secret"

    def test_setter_propagates_to_embedding_manager(self):
        svc = _make_service()
        svc.embedding_manager = MagicMock()
        svc.integrity_manager = MagicMock()
        svc.db_password = "new_pw"
        assert svc.embedding_manager.db_password == "new_pw"

    def test_setter_propagates_to_integrity_manager(self):
        svc = _make_service()
        svc.embedding_manager = MagicMock()
        svc.integrity_manager = MagicMock()
        svc.db_password = "new_pw"
        assert svc.integrity_manager.password == "new_pw"

    def test_setter_handles_none_managers(self):
        svc = _make_service()
        svc.embedding_manager = None
        svc.integrity_manager = None
        # Should not raise
        svc.db_password = "pw"
        assert svc._db_password == "pw"


# =========================================================================
# Context manager / close
# =========================================================================
class TestContextManager:
    """Tests for context manager protocol."""

    def test_enter_returns_self(self):
        svc = _make_service()
        assert svc.__enter__() is svc

    def test_exit_calls_close(self):
        svc = _make_service()
        svc.close = MagicMock()
        svc.__exit__(None, None, None)
        svc.close.assert_called_once()

    def test_exit_returns_false(self):
        svc = _make_service()
        result = svc.__exit__(None, None, None)
        assert result is False
