"""Coverage tests for LibraryRAGService — index/stats/remove paths.

Targets uncovered lines in library_rag_service.py:
- index_document: no text_content (error), force_reindex (delete old chunks first)
- index_documents_batch: batch processing (not found, skip, no text, success)
- get_current_index_info: no index found → returns None
- get_rag_stats: stats calculation with and without chunk sample
- remove_document_from_rag: success deletion path
"""

from unittest.mock import MagicMock, patch

from langchain_core.documents import Document as LangchainDocument

_MOD = "local_deep_research.research_library.services.library_rag_service"


# ---------------------------------------------------------------------------
# Shared helpers
# ---------------------------------------------------------------------------


def _make_service(**overrides):
    """Instantiate LibraryRAGService with all heavy deps fully mocked."""
    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


def _make_session_ctx(session):
    """Return a context-manager mock wrapping *session*."""
    ctx = MagicMock()
    ctx.__enter__ = MagicMock(return_value=session)
    ctx.__exit__ = MagicMock(return_value=None)
    return ctx


def _make_document(
    doc_id="doc-1",
    text_content="Long enough text content here for indexing.",
    title="Test Doc",
    filename=None,
):
    """Build a minimal mock Document ORM object."""
    doc = MagicMock()
    doc.id = doc_id
    doc.text_content = text_content
    doc.title = title
    doc.filename = filename
    doc.original_url = "http://example.com/doc"
    doc.authors = None
    doc.published_date = None
    doc.doi = None
    doc.arxiv_id = None
    doc.pmid = None
    doc.pmcid = None
    doc.extraction_method = None
    doc.word_count = None
    return doc


def _make_doc_collection(
    doc_id="doc-1",
    collection_id="coll-1",
    indexed=False,
    chunk_count=0,
):
    dc = MagicMock()
    dc.document_id = doc_id
    dc.collection_id = collection_id
    dc.indexed = indexed
    dc.chunk_count = chunk_count
    return dc


# ---------------------------------------------------------------------------
# 1. test_index_document_no_text_content
# ---------------------------------------------------------------------------


class TestIndexDocumentNoTextContent:
    """index_document returns {'status': 'error'} when document has no text."""

    @patch(f"{_MOD}.ensure_in_collection")
    @patch(f"{_MOD}.get_user_db_session")
    def test_none_text_content_returns_error(
        self, mock_session_ctx, mock_ensure
    ):
        svc = _make_service()
        mock_session = MagicMock()
        mock_session_ctx.return_value = _make_session_ctx(mock_session)

        mock_doc = _make_document(text_content=None)

        mock_session.query.return_value.filter_by.return_value.first.return_value = mock_doc

        mock_ensure.return_value = MagicMock(indexed=False, chunk_count=0)

        result = svc.index_document("doc-1", "coll-1")

        assert result["status"] == "error"
        assert "no text content" in result["error"]

    @patch(f"{_MOD}.ensure_in_collection")
    @patch(f"{_MOD}.get_user_db_session")
    def test_empty_string_text_content_returns_error(
        self, mock_session_ctx, mock_ensure
    ):
        svc = _make_service()
        mock_session = MagicMock()
        mock_session_ctx.return_value = _make_session_ctx(mock_session)

        mock_doc = _make_document(text_content="")

        mock_session.query.return_value.filter_by.return_value.first.return_value = mock_doc

        mock_ensure.return_value = MagicMock(indexed=False, chunk_count=0)

        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_document_not_found_returns_error(self, mock_session_ctx):
        svc = _make_service()
        mock_session = MagicMock()
        mock_session_ctx.return_value = _make_session_ctx(mock_session)

        def query_side(model):
            q = MagicMock()
            name = getattr(model, "__name__", str(model))
            if "Document" == name:
                q.filter_by.return_value.first.return_value = None
            return q

        mock_session.query = MagicMock(side_effect=query_side)

        result = svc.index_document("missing-doc", "coll-1")

        assert result["status"] == "error"
        assert "not found" in result["error"]

    @patch(f"{_MOD}.ensure_in_collection")
    @patch(f"{_MOD}.get_user_db_session")
    def test_already_indexed_without_force_returns_skipped(
        self, mock_session_ctx, mock_ensure
    ):
        svc = _make_service()
        mock_session = MagicMock()
        mock_session_ctx.return_value = _make_session_ctx(mock_session)

        mock_doc = _make_document()

        mock_session.query.return_value.filter_by.return_value.first.return_value = mock_doc

        mock_ensure.return_value = MagicMock(indexed=True, chunk_count=5)

        result = svc.index_document("doc-1", "coll-1", force_reindex=False)

        assert result["status"] == "skipped"
        assert result["chunk_count"] == 5


# ---------------------------------------------------------------------------
# 2. test_index_document_force_reindex
# ---------------------------------------------------------------------------


class TestIndexDocumentForceReindex:
    """force_reindex=True deletes old FAISS chunks before adding new ones."""

    @patch(f"{_MOD}.get_user_db_session")
    def test_force_reindex_calls_faiss_delete_for_existing_ids(
        self, mock_session_ctx
    ):
        svc = _make_service()
        mock_session = MagicMock()
        mock_session_ctx.return_value = _make_session_ctx(mock_session)

        existing_chunk_id = "old-chunk-id-1"

        # Document with text
        mock_doc = _make_document()
        # DocumentCollection already indexed but force_reindex=True bypasses
        mock_dc = _make_doc_collection(indexed=True, chunk_count=3)
        mock_collection = MagicMock()

        def query_side(model):
            q = MagicMock()
            name = getattr(model, "__name__", str(model))
            if "Document" == name:
                q.filter_by.return_value.first.return_value = mock_doc
            elif "DocumentCollection" in name:
                q.filter_by.return_value.all.return_value = [mock_dc]
                q.filter_by.return_value.update = MagicMock()
            elif "Collection" == name:
                q.filter_by.return_value.first.return_value = mock_collection
            elif "RAGIndex" in name:
                q.filter_by.return_value.first.return_value = MagicMock()
            elif "RagDocumentStatus" in name:
                q.filter_by.return_value.first.return_value = None
            return q

        mock_session.query = MagicMock(side_effect=query_side)
        mock_session.merge = MagicMock()
        mock_session.execute = MagicMock()

        # Chunks from text splitter
        mock_chunk = LangchainDocument(page_content="chunk content")
        svc.text_splitter = MagicMock()
        svc.text_splitter.split_documents.return_value = [mock_chunk]

        # Embedding manager returns ID that exists in FAISS docstore
        svc.embedding_manager = MagicMock()
        svc.embedding_manager._store_chunks_to_db.return_value = [
            existing_chunk_id
        ]

        # Mock FAISS index with the existing chunk in docstore
        mock_faiss = MagicMock()
        mock_faiss.docstore._dict = {existing_chunk_id: MagicMock()}
        svc.faiss_index = mock_faiss

        # Mock rag_index_record
        mock_rag_record = MagicMock()
        mock_rag_record.id = "rag-idx-1"
        mock_rag_record.index_path = "/tmp/test_idx.faiss"
        svc.rag_index_record = mock_rag_record

        svc.integrity_manager = MagicMock()

        result = svc.index_document("doc-1", "coll-1", force_reindex=True)

        # FAISS delete should have been called with the old chunk ID
        mock_faiss.delete.assert_called_once_with([existing_chunk_id])
        # Result should be success
        assert result["status"] == "success"

    @patch(f"{_MOD}.get_user_db_session")
    def test_force_reindex_no_existing_ids_skips_delete(self, mock_session_ctx):
        """If there are no matching chunk IDs in FAISS, delete is never called."""
        svc = _make_service()
        mock_session = MagicMock()
        mock_session_ctx.return_value = _make_session_ctx(mock_session)

        mock_doc = _make_document()
        mock_dc = _make_doc_collection(indexed=False)
        mock_collection = MagicMock()

        def query_side(model):
            q = MagicMock()
            name = getattr(model, "__name__", str(model))
            if "Document" == name:
                q.filter_by.return_value.first.return_value = mock_doc
            elif "DocumentCollection" in name:
                q.filter_by.return_value.all.return_value = [mock_dc]
                q.filter_by.return_value.update = MagicMock()
            elif "Collection" == name:
                q.filter_by.return_value.first.return_value = mock_collection
            elif "RAGIndex" in name:
                q.filter_by.return_value.first.return_value = MagicMock()
            elif "RagDocumentStatus" in name:
                q.filter_by.return_value.first.return_value = None
            return q

        mock_session.query = MagicMock(side_effect=query_side)
        mock_session.merge = MagicMock()
        mock_session.execute = MagicMock()

        mock_chunk = LangchainDocument(page_content="chunk")
        svc.text_splitter = MagicMock()
        svc.text_splitter.split_documents.return_value = [mock_chunk]

        svc.embedding_manager = MagicMock()
        svc.embedding_manager._store_chunks_to_db.return_value = ["new-id-1"]

        # FAISS docstore does NOT contain "new-id-1"
        mock_faiss = MagicMock()
        mock_faiss.docstore._dict = {}
        svc.faiss_index = mock_faiss

        mock_rag_record = MagicMock()
        mock_rag_record.id = "rag-idx-2"
        mock_rag_record.index_path = "/tmp/idx2.faiss"
        svc.rag_index_record = mock_rag_record
        svc.integrity_manager = MagicMock()

        result = svc.index_document("doc-1", "coll-1", force_reindex=True)

        # No existing IDs matched → delete not called
        mock_faiss.delete.assert_not_called()
        assert result["status"] == "success"


# ---------------------------------------------------------------------------
# 3. test_index_documents_batch
# ---------------------------------------------------------------------------


class TestIndexDocumentsBatch:
    """Batch processing: multiple code paths in index_documents_batch."""

    @patch(f"{_MOD}.get_user_db_session")
    def test_document_not_found_in_db(self, mock_session_ctx):
        """When a doc_id is missing from the DB, result is error 'not found'."""
        svc = _make_service()
        mock_session = MagicMock()
        mock_session_ctx.return_value = _make_session_ctx(mock_session)

        # Both Document and DocumentCollection queries return empty
        mock_session.query.return_value.filter.return_value.all.return_value = []

        result = svc.index_documents_batch([("missing-doc", "Title")], "coll-1")

        assert result["missing-doc"]["status"] == "error"
        assert "not found" in result["missing-doc"]["error"]

    @patch(f"{_MOD}.get_user_db_session")
    def test_already_indexed_document_is_skipped(self, mock_session_ctx):
        """Indexed document without force_reindex → skipped result."""
        svc = _make_service()
        mock_session = MagicMock()
        mock_session_ctx.return_value = _make_session_ctx(mock_session)

        mock_doc = _make_document(doc_id="doc-skip")
        mock_dc = _make_doc_collection(
            doc_id="doc-skip", indexed=True, chunk_count=4
        )

        mock_session.query.return_value.filter.return_value.all.side_effect = [
            [mock_doc],  # Document.id.in_(...) query
            [mock_dc],  # DocumentCollection query
        ]

        result = svc.index_documents_batch(
            [("doc-skip", "Skip Me")], "coll-1", force_reindex=False
        )

        assert result["doc-skip"]["status"] == "skipped"
        assert result["doc-skip"]["chunk_count"] == 4

    @patch(f"{_MOD}.get_user_db_session")
    def test_document_without_text_content_returns_error(
        self, mock_session_ctx
    ):
        """Document with no text_content → error in batch result."""
        svc = _make_service()
        mock_session = MagicMock()
        mock_session_ctx.return_value = _make_session_ctx(mock_session)

        mock_doc = _make_document(doc_id="doc-notext", text_content=None)
        mock_dc = _make_doc_collection(doc_id="doc-notext", indexed=False)

        mock_session.query.return_value.filter.return_value.all.side_effect = [
            [mock_doc],
            [mock_dc],
        ]

        result = svc.index_documents_batch([("doc-notext", "Title")], "coll-1")

        assert result["doc-notext"]["status"] == "error"
        assert "no text content" in result["doc-notext"]["error"]

    @patch(f"{_MOD}.get_user_db_session")
    def test_successful_document_delegates_to_index_document(
        self, mock_session_ctx
    ):
        """A doc with text and not yet indexed calls self.index_document."""
        svc = _make_service()
        mock_session = MagicMock()
        mock_session_ctx.return_value = _make_session_ctx(mock_session)

        mock_doc = _make_document(doc_id="doc-ok")
        mock_dc = _make_doc_collection(doc_id="doc-ok", indexed=False)

        mock_session.query.return_value.filter.return_value.all.side_effect = [
            [mock_doc],
            [mock_dc],
        ]

        svc.index_document = MagicMock(
            return_value={
                "status": "success",
                "chunk_count": 3,
                "embedding_ids": [],
            }
        )

        result = svc.index_documents_batch([("doc-ok", "Good Doc")], "coll-1")

        svc.index_document.assert_called_once_with("doc-ok", "coll-1", False)
        assert result["doc-ok"]["status"] == "success"
        assert result["doc-ok"]["chunk_count"] == 3

    @patch(f"{_MOD}.get_user_db_session")
    def test_exception_in_index_document_captured_per_doc(
        self, mock_session_ctx
    ):
        """Exception from index_document is caught; other docs still process."""
        svc = _make_service()
        mock_session = MagicMock()
        mock_session_ctx.return_value = _make_session_ctx(mock_session)

        mock_doc = _make_document(doc_id="doc-bad")
        mock_dc = _make_doc_collection(doc_id="doc-bad", indexed=False)

        mock_session.query.return_value.filter.return_value.all.side_effect = [
            [mock_doc],
            [mock_dc],
        ]

        svc.index_document = MagicMock(
            side_effect=RuntimeError("splitter crash")
        )

        result = svc.index_documents_batch([("doc-bad", "Bad Doc")], "coll-1")

        assert result["doc-bad"]["status"] == "error"
        assert "RuntimeError" in result["doc-bad"]["error"]

    @patch(f"{_MOD}.get_user_db_session")
    def test_force_reindex_passed_through_to_index_document(
        self, mock_session_ctx
    ):
        """force_reindex flag is forwarded to index_document call."""
        svc = _make_service()
        mock_session = MagicMock()
        mock_session_ctx.return_value = _make_session_ctx(mock_session)

        mock_doc = _make_document(doc_id="doc-force")
        mock_dc = _make_doc_collection(
            doc_id="doc-force", indexed=True, chunk_count=2
        )

        mock_session.query.return_value.filter.return_value.all.side_effect = [
            [mock_doc],
            [mock_dc],
        ]

        svc.index_document = MagicMock(
            return_value={
                "status": "success",
                "chunk_count": 5,
                "embedding_ids": [],
            }
        )

        result = svc.index_documents_batch(
            [("doc-force", "Force Me")], "coll-1", force_reindex=True
        )

        # With force_reindex=True, skip logic is bypassed → index_document called
        svc.index_document.assert_called_once_with("doc-force", "coll-1", True)
        assert result["doc-force"]["status"] == "success"

    @patch(f"{_MOD}.get_user_db_session")
    def test_multiple_documents_all_processed(self, mock_session_ctx):
        """Multiple docs in one batch all receive individual results."""
        svc = _make_service()
        mock_session = MagicMock()
        mock_session_ctx.return_value = _make_session_ctx(mock_session)

        doc_a = _make_document(doc_id="doc-a", text_content="content a")
        doc_b = _make_document(doc_id="doc-b", text_content=None)

        dc_a = _make_doc_collection(doc_id="doc-a", indexed=False)
        dc_b = _make_doc_collection(doc_id="doc-b", indexed=False)

        mock_session.query.return_value.filter.return_value.all.side_effect = [
            [doc_a, doc_b],  # Documents query
            [dc_a, dc_b],  # DocumentCollection query
        ]

        svc.index_document = MagicMock(
            return_value={
                "status": "success",
                "chunk_count": 2,
                "embedding_ids": [],
            }
        )

        result = svc.index_documents_batch(
            [("doc-a", "A"), ("doc-b", "B")], "coll-1"
        )

        # doc-a: success (has text)
        assert result["doc-a"]["status"] == "success"
        # doc-b: error (no text)
        assert result["doc-b"]["status"] == "error"


# ---------------------------------------------------------------------------
# 4. test_get_current_index_info_no_index
# ---------------------------------------------------------------------------


class TestGetCurrentIndexInfoNoIndex:
    """When no RAGIndex record exists for the collection, returns None."""

    @patch(f"{_MOD}.get_user_db_session")
    def test_returns_none_when_rag_index_missing(self, mock_session_ctx):
        svc = _make_service()
        mock_session = MagicMock()
        mock_session_ctx.return_value = _make_session_ctx(mock_session)

        def query_side(model_or_expr):
            q = MagicMock()
            name = getattr(model_or_expr, "__name__", str(model_or_expr))
            if "Collection" in name:
                q.filter_by.return_value.first.return_value = MagicMock()
            elif "RAGIndex" in name:
                # No index found
                q.filter_by.return_value.first.return_value = None
                q.all.return_value = []
            return q

        mock_session.query = MagicMock(side_effect=query_side)

        result = svc.get_current_index_info("coll-99")

        assert result is None

    @patch(f"{_MOD}.get_user_db_session")
    def test_returns_none_when_collection_not_found_but_rag_also_missing(
        self, mock_session_ctx
    ):
        """Even with a known collection_id, None is returned when no RAGIndex."""
        svc = _make_service()
        mock_session = MagicMock()
        mock_session_ctx.return_value = _make_session_ctx(mock_session)

        def query_side(model_or_expr):
            q = MagicMock()
            name = getattr(model_or_expr, "__name__", str(model_or_expr))
            if "Collection" in name:
                # collection lookup returns None (not found)
                q.filter_by.return_value.first.return_value = None
            elif "RAGIndex" in name:
                q.filter_by.return_value.first.return_value = None
                q.all.return_value = []
            return q

        mock_session.query = MagicMock(side_effect=query_side)

        result = svc.get_current_index_info("coll-unknown")

        assert result is None

    @patch(f"{_MOD}.get_user_db_session")
    def test_returns_dict_when_index_exists(self, mock_session_ctx):
        """Positive case: when RAGIndex exists, a dict is returned."""
        svc = _make_service()
        mock_session = MagicMock()
        mock_session_ctx.return_value = _make_session_ctx(mock_session)

        mock_rag_index = MagicMock()
        mock_rag_index.embedding_model = "all-MiniLM-L6-v2"
        mock_rag_index.embedding_model_type = MagicMock()
        mock_rag_index.embedding_model_type.value = "sentence_transformers"
        mock_rag_index.embedding_dimension = 384
        mock_rag_index.chunk_size = 1000
        mock_rag_index.chunk_overlap = 200
        mock_rag_index.created_at = MagicMock()
        mock_rag_index.created_at.isoformat.return_value = "2025-01-01T00:00:00"
        mock_rag_index.last_updated_at = MagicMock()
        mock_rag_index.last_updated_at.isoformat.return_value = (
            "2025-06-01T00:00:00"
        )

        def query_side(model_or_expr):
            q = MagicMock()
            name = getattr(model_or_expr, "__name__", str(model_or_expr))
            if "Collection" in name:
                q.filter_by.return_value.first.return_value = MagicMock()
            elif "RAGIndex" in name:
                q.filter_by.return_value.first.return_value = mock_rag_index
            else:
                # RagDocumentStatus / func.sum queries
                q.filter_by.return_value.scalar.return_value = 10
                q.filter_by.return_value.count.return_value = 2
            return q

        mock_session.query = MagicMock(side_effect=query_side)

        result = svc.get_current_index_info("coll-abc")

        assert isinstance(result, dict)
        assert result["embedding_model"] == "all-MiniLM-L6-v2"
        assert result["chunk_size"] == 1000
        assert result["total_documents"] == 2
        assert result["chunk_count"] == 10

    @patch(f"{_MOD}.get_user_db_session")
    def test_embedding_model_type_none_in_result(self, mock_session_ctx):
        """embedding_model_type=None should yield None in the result dict."""
        svc = _make_service()
        mock_session = MagicMock()
        mock_session_ctx.return_value = _make_session_ctx(mock_session)

        mock_rag_index = MagicMock()
        mock_rag_index.embedding_model = "custom-model"
        mock_rag_index.embedding_model_type = None  # None branch
        mock_rag_index.embedding_dimension = 768
        mock_rag_index.chunk_size = 500
        mock_rag_index.chunk_overlap = 50
        mock_rag_index.created_at = MagicMock()
        mock_rag_index.created_at.isoformat.return_value = "2025-01-01T00:00:00"
        mock_rag_index.last_updated_at = MagicMock()
        mock_rag_index.last_updated_at.isoformat.return_value = (
            "2025-06-01T00:00:00"
        )

        def query_side(model_or_expr):
            q = MagicMock()
            name = getattr(model_or_expr, "__name__", str(model_or_expr))
            if "Collection" in name:
                q.filter_by.return_value.first.return_value = MagicMock()
            elif "RAGIndex" in name:
                q.filter_by.return_value.first.return_value = mock_rag_index
            else:
                q.filter_by.return_value.scalar.return_value = 5
                q.filter_by.return_value.count.return_value = 1
            return q

        mock_session.query = MagicMock(side_effect=query_side)

        result = svc.get_current_index_info("coll-xyz")

        assert result is not None
        assert result["embedding_model_type"] is None


# ---------------------------------------------------------------------------
# 5. test_get_rag_stats
# ---------------------------------------------------------------------------


class TestGetRagStats:
    """get_rag_stats returns correct counts and embedding info."""

    @patch(f"{_MOD}.get_user_db_session")
    def test_stats_with_all_fields_populated(self, mock_session_ctx):
        svc = _make_service()
        mock_session = MagicMock()
        mock_session_ctx.return_value = _make_session_ctx(mock_session)

        mock_collection = MagicMock()
        mock_chunk_sample = MagicMock()
        mock_chunk_sample.embedding_model = "bge-small"
        mock_chunk_sample.embedding_model_type = MagicMock()
        mock_chunk_sample.embedding_model_type.value = "sentence_transformers"
        mock_chunk_sample.embedding_dimension = 512

        def query_side(model_or_expr):
            q = MagicMock()
            name = getattr(model_or_expr, "__name__", str(model_or_expr))
            if "DocumentCollection" in name:
                q.filter_by.return_value.count.return_value = 10
            elif "RagDocumentStatus" in name:
                q.filter_by.return_value.count.return_value = 7
                q.filter_by.return_value.scalar.return_value = 70
            elif "Collection" == name:
                q.filter_by.return_value.first.return_value = mock_collection
            elif "DocumentChunk" in name:
                q.filter_by.return_value.first.return_value = mock_chunk_sample
            else:
                q.filter_by.return_value.scalar.return_value = 70
            return q

        mock_session.query = MagicMock(side_effect=query_side)

        result = svc.get_rag_stats("coll-test")

        assert result["total_documents"] == 10
        assert result["indexed_documents"] == 7
        assert result["unindexed_documents"] == 3
        assert result["total_chunks"] == 70
        assert result["embedding_info"]["model"] == "bge-small"
        assert result["embedding_info"]["model_type"] == "sentence_transformers"
        assert result["embedding_info"]["dimension"] == 512

    @patch(f"{_MOD}.get_user_db_session")
    def test_stats_no_chunk_sample_embedding_info_empty(self, mock_session_ctx):
        """When no DocumentChunk exists, embedding_info is an empty dict."""
        svc = _make_service()
        mock_session = MagicMock()
        mock_session_ctx.return_value = _make_session_ctx(mock_session)

        def query_side(model_or_expr):
            q = MagicMock()
            name = getattr(model_or_expr, "__name__", str(model_or_expr))
            if "DocumentCollection" in name:
                q.filter_by.return_value.count.return_value = 3
            elif "RagDocumentStatus" in name:
                q.filter_by.return_value.count.return_value = 0
                q.filter_by.return_value.scalar.return_value = None
            elif "Collection" == name:
                q.filter_by.return_value.first.return_value = None
            elif "DocumentChunk" in name:
                q.filter_by.return_value.first.return_value = None
            else:
                q.filter_by.return_value.scalar.return_value = None
            return q

        mock_session.query = MagicMock(side_effect=query_side)

        result = svc.get_rag_stats("coll-empty")

        assert result["embedding_info"] == {}
        assert result["total_chunks"] == 0
        assert result["unindexed_documents"] == 3

    @patch(f"{_MOD}.get_user_db_session")
    def test_stats_chunk_sample_embedding_model_type_none(
        self, mock_session_ctx
    ):
        """embedding_model_type = None on chunk_sample yields None in embedding_info."""
        svc = _make_service()
        mock_session = MagicMock()
        mock_session_ctx.return_value = _make_session_ctx(mock_session)

        mock_chunk_sample = MagicMock()
        mock_chunk_sample.embedding_model = "custom"
        mock_chunk_sample.embedding_model_type = None
        mock_chunk_sample.embedding_dimension = 256

        def query_side(model_or_expr):
            q = MagicMock()
            name = getattr(model_or_expr, "__name__", str(model_or_expr))
            if "DocumentCollection" in name:
                q.filter_by.return_value.count.return_value = 2
            elif "RagDocumentStatus" in name:
                q.filter_by.return_value.count.return_value = 2
                q.filter_by.return_value.scalar.return_value = 20
            elif "Collection" == name:
                q.filter_by.return_value.first.return_value = MagicMock()
            elif "DocumentChunk" in name:
                q.filter_by.return_value.first.return_value = mock_chunk_sample
            else:
                q.filter_by.return_value.scalar.return_value = 20
            return q

        mock_session.query = MagicMock(side_effect=query_side)

        result = svc.get_rag_stats("coll-custom")

        assert result["embedding_info"]["model_type"] is None

    @patch(f"{_MOD}.get_user_db_session")
    def test_chunk_size_and_overlap_from_service_config(self, mock_session_ctx):
        """chunk_size and chunk_overlap in result come from service config, not DB."""
        svc = _make_service()
        svc.chunk_size = 512
        svc.chunk_overlap = 64

        mock_session = MagicMock()
        mock_session_ctx.return_value = _make_session_ctx(mock_session)

        def query_side(model_or_expr):
            q = MagicMock()
            name = getattr(model_or_expr, "__name__", str(model_or_expr))
            if "DocumentCollection" in name:
                q.filter_by.return_value.count.return_value = 0
            elif "RagDocumentStatus" in name:
                q.filter_by.return_value.count.return_value = 0
                q.filter_by.return_value.scalar.return_value = 0
            elif "Collection" == name:
                q.filter_by.return_value.first.return_value = MagicMock()
            elif "DocumentChunk" in name:
                q.filter_by.return_value.first.return_value = None
            else:
                q.filter_by.return_value.scalar.return_value = 0
            return q

        mock_session.query = MagicMock(side_effect=query_side)

        result = svc.get_rag_stats("coll-cfg")

        assert result["chunk_size"] == 512
        assert result["chunk_overlap"] == 64


# ---------------------------------------------------------------------------
# 6. test_remove_document_from_rag
# ---------------------------------------------------------------------------


class TestRemoveDocumentFromRag:
    """remove_document_from_rag deletes chunks and resets DocumentCollection flags."""

    @patch(f"{_MOD}.get_user_db_session")
    def test_success_path_updates_doc_collection_fields(self, mock_session_ctx):
        svc = _make_service()
        svc.embedding_manager = MagicMock()
        svc.embedding_manager._delete_chunks_from_db.return_value = 8

        mock_session = MagicMock()
        mock_session_ctx.return_value = _make_session_ctx(mock_session)

        mock_dc = _make_doc_collection(indexed=True, chunk_count=8)
        mock_collection = MagicMock()

        def query_side(model):
            q = MagicMock()
            name = getattr(model, "__name__", str(model))
            if "DocumentCollection" in name:
                q.filter_by.return_value.first.return_value = mock_dc
            elif "Collection" == name:
                q.filter_by.return_value.first.return_value = mock_collection
            return q

        mock_session.query = MagicMock(side_effect=query_side)

        result = svc.remove_document_from_rag("doc-1", "coll-1")

        assert result["status"] == "success"
        assert result["deleted_count"] == 8
        assert mock_dc.indexed is False
        assert mock_dc.chunk_count == 0
        assert mock_dc.last_indexed_at is None
        mock_session.commit.assert_called_once()

    @patch(f"{_MOD}.get_user_db_session")
    def test_collection_name_format_used_in_delete_call(self, mock_session_ctx):
        """_delete_chunks_from_db is called with 'collection_<uuid>' format."""
        svc = _make_service()
        svc.embedding_manager = MagicMock()
        svc.embedding_manager._delete_chunks_from_db.return_value = 3

        mock_session = MagicMock()
        mock_session_ctx.return_value = _make_session_ctx(mock_session)

        mock_dc = _make_doc_collection(indexed=True)
        mock_collection = MagicMock()

        def query_side(model):
            q = MagicMock()
            name = getattr(model, "__name__", str(model))
            if "DocumentCollection" in name:
                q.filter_by.return_value.first.return_value = mock_dc
            elif "Collection" == name:
                q.filter_by.return_value.first.return_value = mock_collection
            return q

        mock_session.query = MagicMock(side_effect=query_side)

        svc.remove_document_from_rag("doc-2", "coll-abc")

        svc.embedding_manager._delete_chunks_from_db.assert_called_once_with(
            collection_name="collection_coll-abc",
            source_id="doc-2",
        )

    @patch(f"{_MOD}.get_user_db_session")
    def test_collection_not_found_returns_unknown_in_name(
        self, mock_session_ctx
    ):
        """When Collection lookup returns None, collection_name becomes 'unknown'."""
        svc = _make_service()
        svc.embedding_manager = MagicMock()
        svc.embedding_manager._delete_chunks_from_db.return_value = 0

        mock_session = MagicMock()
        mock_session_ctx.return_value = _make_session_ctx(mock_session)

        mock_dc = _make_doc_collection(indexed=True)

        def query_side(model):
            q = MagicMock()
            name = getattr(model, "__name__", str(model))
            if "DocumentCollection" in name:
                q.filter_by.return_value.first.return_value = mock_dc
            elif "Collection" == name:
                q.filter_by.return_value.first.return_value = None  # not found
            return q

        mock_session.query = MagicMock(side_effect=query_side)

        result = svc.remove_document_from_rag("doc-3", "coll-missing")

        # collection_name falls back to 'unknown'
        svc.embedding_manager._delete_chunks_from_db.assert_called_once_with(
            collection_name="unknown",
            source_id="doc-3",
        )
        assert result["status"] == "success"

    @patch(f"{_MOD}.get_user_db_session")
    def test_doc_collection_not_in_collection_returns_error(
        self, mock_session_ctx
    ):
        """When DocumentCollection is not found, error is returned immediately."""
        svc = _make_service()
        mock_session = MagicMock()
        mock_session_ctx.return_value = _make_session_ctx(mock_session)

        mock_session.query.return_value.filter_by.return_value.first.return_value = None

        result = svc.remove_document_from_rag("doc-gone", "coll-1")

        assert result["status"] == "error"
        assert "not found" in result["error"]

    @patch(f"{_MOD}.get_user_db_session")
    def test_exception_in_delete_returns_error(self, mock_session_ctx):
        """Exception from _delete_chunks_from_db is caught and returned as error."""
        svc = _make_service()
        svc.embedding_manager = MagicMock()
        svc.embedding_manager._delete_chunks_from_db.side_effect = RuntimeError(
            "db crash"
        )

        mock_session = MagicMock()
        mock_session_ctx.return_value = _make_session_ctx(mock_session)

        mock_dc = _make_doc_collection(indexed=True)
        mock_collection = MagicMock()

        def query_side(model):
            q = MagicMock()
            name = getattr(model, "__name__", str(model))
            if "DocumentCollection" in name:
                q.filter_by.return_value.first.return_value = mock_dc
            elif "Collection" == name:
                q.filter_by.return_value.first.return_value = mock_collection
            return q

        mock_session.query = MagicMock(side_effect=query_side)

        result = svc.remove_document_from_rag("doc-err", "coll-1")

        assert result["status"] == "error"
        assert "RuntimeError" in result["error"]
