"""
Coverage tests for upload_to_collection and get_collection_documents in rag_routes.py.

Covers:
- upload_to_collection: no files key, empty filename, collection not found,
  existing doc already in collection, existing doc added to collection,
  existing doc pdf upgrade, unsupported extension, no text extracted,
  new doc success (text-only), new doc success (pdf database storage),
  pdf storage failure continues, auto-index triggered, auto-index no password
- get_collection_documents: collection not found, index size formatting (B/KB/MB)
"""

import tempfile
import uuid
from contextlib import contextmanager
from io import BytesIO
from pathlib import Path
from unittest.mock import Mock, patch

import pytest
from flask import Flask, jsonify

from local_deep_research.constants import (
    DEFAULT_LOCAL_SEARCH_TEXT_SEPARATORS_JSON,
)
from local_deep_research.web.auth.routes import auth_bp
from local_deep_research.research_library.routes.rag_routes import rag_bp
from local_deep_research.security.rate_limiter import (
    limiter as _real_limiter,
)

# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------

MODULE = "local_deep_research.research_library.routes.rag_routes"
_DB_CTX = "local_deep_research.database.session_context"
_DB_PASS = "local_deep_research.database.session_passwords"
_DOC_LOADERS = "local_deep_research.document_loaders"
_TEXT_PROC = "local_deep_research.text_processing"

# ---------------------------------------------------------------------------
# Helpers (copied verbatim from test_rag_routes_deep_coverage.py)
# ---------------------------------------------------------------------------


def _uid():
    """Short unique identifier for test isolation."""
    return uuid.uuid4().hex[:12]


def _create_app():
    """Minimal Flask app with rag blueprint."""
    app = Flask(__name__)
    app.config["SECRET_KEY"] = f"test-{_uid()}"
    app.config["WTF_CSRF_ENABLED"] = False
    app.config["TESTING"] = True
    app.register_blueprint(auth_bp)
    app.register_blueprint(rag_bp)

    @app.errorhandler(500)
    def _handle_500(error):
        return jsonify({"error": "Internal server error"}), 500

    return app


def _mock_db_manager():
    """Mock db_manager so login_required passes."""
    m = Mock()
    m.is_user_connected.return_value = True
    m.connections = {"testuser": True}
    m.has_encryption = False
    return m


def _build_mock_query(all_result=None, first_result=None, count_result=0):
    """Build a chainable mock query."""
    q = Mock()
    q.all.return_value = all_result or []
    q.first.return_value = first_result
    q.count.return_value = count_result
    q.filter_by.return_value = q
    q.filter.return_value = q
    q.order_by.return_value = q
    q.outerjoin.return_value = q
    q.join.return_value = q
    q.options.return_value = q
    q.limit.return_value = q
    q.offset.return_value = q
    q.delete.return_value = 0
    q.update.return_value = 0
    return q


def _make_settings_mock(overrides=None):
    """Create a mock settings manager."""
    mock_sm = Mock()
    defaults = {
        "local_search_embedding_model": "all-MiniLM-L6-v2",
        "local_search_embedding_provider": "sentence_transformers",
        "local_search_chunk_size": 1000,
        "local_search_chunk_overlap": 200,
        "local_search_splitter_type": "recursive",
        "local_search_text_separators": DEFAULT_LOCAL_SEARCH_TEXT_SEPARATORS_JSON,
        "local_search_distance_metric": "cosine",
        "local_search_normalize_vectors": True,
        "local_search_index_type": "flat",
        "research_library.upload_pdf_storage": "none",
        "research_library.storage_path": "/tmp/test_lib",
        "rag.indexing_batch_size": 15,
        "research_library.auto_index_enabled": True,
    }
    if overrides:
        defaults.update(overrides)
    mock_sm.get_setting.side_effect = lambda k, d=None: defaults.get(k, d)
    mock_sm.get_bool_setting.side_effect = lambda k, d=None: defaults.get(k, d)
    mock_sm.get_all_settings.return_value = {}
    mock_sm.set_setting = Mock()
    mock_sm.get_settings_snapshot.return_value = {}
    return mock_sm


def _make_db_session():
    """Create a standard mock db session."""
    s = Mock()
    s.query = Mock(return_value=_build_mock_query())
    s.commit = Mock()
    s.add = Mock()
    s.flush = Mock()
    s.expire_all = Mock()
    return s


@contextmanager
def _auth_client(
    app, mock_db_session=None, settings_overrides=None, extra_patches=None
):
    """Context manager providing an authenticated test client with mocking."""
    mock_db = _mock_db_manager()
    db_session = mock_db_session or _make_db_session()
    mock_sm = _make_settings_mock(settings_overrides)

    @contextmanager
    def fake_get_user_db_session(*a, **kw):
        yield db_session

    patches = [
        patch("local_deep_research.web.auth.decorators.db_manager", mock_db),
        patch(
            f"{_DB_CTX}.get_user_db_session",
            side_effect=fake_get_user_db_session,
        ),
        patch(f"{MODULE}.get_settings_manager", return_value=mock_sm),
        patch(
            "local_deep_research.utilities.db_utils.get_settings_manager",
            return_value=mock_sm,
        ),
        patch(f"{MODULE}.limiter", Mock(exempt=lambda f: f)),
        patch(f"{MODULE}.upload_rate_limit_user", lambda f: f),
        patch(f"{MODULE}.upload_rate_limit_ip", lambda f: f),
        # The decorators above were applied at module-import time (closures
        # over the real Limiter), so patching the module symbols can't undo
        # them. Disable the real limiter for the duration of the test
        # instead, so per-test-process rate-limit budget consumed by other
        # tests in the same session can't bleed in here. The patch restores
        # the previous value on exit.
        patch.object(_real_limiter, "enabled", False),
    ]
    if extra_patches:
        patches.extend(extra_patches)

    started = []
    try:
        for p in patches:
            started.append(p.start())
        with app.test_client() as client:
            with client.session_transaction() as sess:
                sess["username"] = "testuser"
                sess["session_id"] = "test-session-id"
            yield client, {"db_session": db_session, "settings": mock_sm}
    finally:
        for p in patches:
            p.stop()


@pytest.fixture
def app():
    """Minimal Flask app fixture."""
    return _create_app()


# ---------------------------------------------------------------------------
# upload_to_collection tests
# ---------------------------------------------------------------------------


class TestUploadToCollection:
    """Tests for the upload_to_collection route."""

    def test_upload_no_files_key(self, app):
        """POST with no 'files' key in the request returns 400."""
        with _auth_client(app) as (client, ctx):
            resp = client.post(
                "/library/api/collections/coll-1/upload",
                data={},
                content_type="multipart/form-data",
            )
        assert resp.status_code == 400
        data = resp.get_json()
        assert data["success"] is False
        assert "No files provided" in data["error"]

    def test_upload_rejects_too_many_files(self, app):
        """File count over MAX_FILES_PER_REQUEST is rejected with 400."""
        from local_deep_research.security.file_upload_validator import (
            FileUploadValidator,
        )

        # Patch the limit low so the test doesn't have to ship 201 files.
        with patch.object(FileUploadValidator, "MAX_FILES_PER_REQUEST", 3):
            with _auth_client(app) as (client, ctx):
                # Send 4 files (one over the patched limit).
                resp = client.post(
                    "/library/api/collections/coll-1/upload",
                    data={
                        "files": [
                            (BytesIO(b"a"), f"f{i}.txt") for i in range(4)
                        ]
                    },
                    content_type="multipart/form-data",
                )
        assert resp.status_code == 400
        data = resp.get_json()
        assert data["success"] is False
        assert "Too many files" in data["error"]

    def test_upload_rejects_oversized_file(self, app):
        """Per-file size over MAX_FILE_SIZE is rejected; other files still process."""
        from local_deep_research.security.file_upload_validator import (
            FileUploadValidator,
        )

        mock_coll = Mock()
        mock_coll.id = "coll-1"

        db_session = _make_db_session()
        q = _build_mock_query(first_result=mock_coll)
        db_session.query = Mock(return_value=q)

        mock_password_store = Mock()
        mock_password_store.get_session_password.return_value = None

        # 100-byte limit; send a 200-byte file → rejected.
        with patch.object(FileUploadValidator, "MAX_FILE_SIZE", 100):
            with _auth_client(
                app,
                mock_db_session=db_session,
                extra_patches=[
                    patch(
                        f"{_DB_PASS}.session_password_store",
                        mock_password_store,
                    ),
                ],
            ) as (client, ctx):
                resp = client.post(
                    "/library/api/collections/coll-1/upload",
                    data={
                        "files": (BytesIO(b"x" * 200), "big.txt"),
                    },
                    content_type="multipart/form-data",
                )

        assert resp.status_code == 200
        data = resp.get_json()
        # Oversized file appears in the per-file errors list, NOT 400 — so a
        # batch with one oversized + others can still succeed for the rest.
        assert data["success"] is True
        assert any(
            "File too large" in e.get("error", "") for e in data["errors"]
        )

    def test_upload_empty_files_list(self, app):
        """File with empty filename is silently skipped; response is success with 0 uploaded."""
        mock_coll = Mock()
        mock_coll.id = "coll-1"

        db_session = _make_db_session()
        q = _build_mock_query(first_result=mock_coll)
        db_session.query = Mock(return_value=q)

        mock_password_store = Mock()
        mock_password_store.get_session_password.return_value = None

        with _auth_client(
            app,
            mock_db_session=db_session,
            extra_patches=[
                patch(
                    f"{_DB_PASS}.session_password_store", mock_password_store
                ),
            ],
        ) as (client, ctx):
            # Send a file with no filename (empty string filename is treated as "no filename")
            resp = client.post(
                "/library/api/collections/coll-1/upload",
                data={"files": (BytesIO(b""), "")},
                content_type="multipart/form-data",
            )
        assert resp.status_code == 200
        rdata = resp.get_json()
        assert rdata["success"] is True
        assert rdata["summary"]["successful"] == 0

    def test_upload_collection_not_found(self, app):
        """Returns 404 when the collection does not exist in the DB."""
        db_session = _make_db_session()
        q = _build_mock_query(first_result=None)
        db_session.query = Mock(return_value=q)

        with _auth_client(app, mock_db_session=db_session) as (client, ctx):
            resp = client.post(
                "/library/api/collections/nonexistent/upload",
                data={"files": (BytesIO(b"content"), "doc.pdf")},
                content_type="multipart/form-data",
            )
        assert resp.status_code == 404
        data = resp.get_json()
        assert data["success"] is False
        assert "Collection not found" in data["error"]

    def test_upload_existing_doc_already_in_collection(self, app):
        """Existing doc that is already in the collection → status 'already_in_collection'."""
        mock_coll = Mock()
        mock_coll.id = "coll-1"

        existing_doc = Mock()
        existing_doc.id = "doc-abc"
        existing_doc.filename = "report.pdf"

        existing_link = Mock()  # doc already linked to collection

        db_session = _make_db_session()
        call_count = {"n": 0}

        def query_side_effect(model):
            call_count["n"] += 1
            q = _build_mock_query()
            if call_count["n"] == 1:
                q.first.return_value = mock_coll  # Collection lookup
            elif call_count["n"] == 2:
                q.first.return_value = existing_doc  # Document hash lookup
            elif call_count["n"] == 3:
                q.first.return_value = (
                    existing_link  # DocumentCollection lookup
                )
            return q

        db_session.query = Mock(side_effect=query_side_effect)

        mock_password_store = Mock()
        mock_password_store.get_session_password.return_value = None

        with _auth_client(
            app,
            mock_db_session=db_session,
            extra_patches=[
                patch(
                    f"{_DB_PASS}.session_password_store", mock_password_store
                ),
            ],
        ) as (client, ctx):
            resp = client.post(
                "/library/api/collections/coll-1/upload",
                data={"files": (BytesIO(b"pdf content"), "report.pdf")},
                content_type="multipart/form-data",
            )
        assert resp.status_code == 200
        rdata = resp.get_json()
        assert rdata["success"] is True
        assert len(rdata["uploaded"]) == 1
        assert rdata["uploaded"][0]["status"] == "already_in_collection"
        assert rdata["uploaded"][0]["pdf_upgraded"] is False

    def test_upload_existing_doc_add_to_collection(self, app):
        """Existing doc not yet in collection → status 'added_to_collection'."""
        mock_coll = Mock()
        mock_coll.id = "coll-1"

        existing_doc = Mock()
        existing_doc.id = "doc-xyz"
        existing_doc.filename = "paper.txt"

        db_session = _make_db_session()
        call_count = {"n": 0}

        def query_side_effect(model):
            call_count["n"] += 1
            q = _build_mock_query()
            if call_count["n"] == 1:
                q.first.return_value = mock_coll  # Collection
            elif call_count["n"] == 2:
                q.first.return_value = existing_doc  # Existing doc by hash
            elif call_count["n"] == 3:
                q.first.return_value = None  # Not yet in collection
            return q

        db_session.query = Mock(side_effect=query_side_effect)

        mock_password_store = Mock()
        mock_password_store.get_session_password.return_value = None

        with _auth_client(
            app,
            mock_db_session=db_session,
            extra_patches=[
                patch(
                    f"{_DB_PASS}.session_password_store", mock_password_store
                ),
            ],
        ) as (client, ctx):
            resp = client.post(
                "/library/api/collections/coll-1/upload",
                data={"files": (BytesIO(b"text data"), "paper.txt")},
                content_type="multipart/form-data",
            )
        assert resp.status_code == 200
        rdata = resp.get_json()
        assert rdata["success"] is True
        assert rdata["uploaded"][0]["status"] == "added_to_collection"
        assert rdata["uploaded"][0]["pdf_upgraded"] is False
        # Confirm the link was added to session
        db_session.add.assert_called()

    def test_upload_existing_doc_pdf_upgrade(self, app):
        """Existing doc already in collection with pdf_upgrade=True → status 'pdf_upgraded'."""
        mock_coll = Mock()
        mock_coll.id = "coll-1"

        existing_doc = Mock()
        existing_doc.id = "doc-pdf"
        existing_doc.filename = "scan.pdf"

        existing_link = Mock()  # already in collection

        db_session = _make_db_session()
        call_count = {"n": 0}

        def query_side_effect(model):
            call_count["n"] += 1
            q = _build_mock_query()
            if call_count["n"] == 1:
                q.first.return_value = mock_coll
            elif call_count["n"] == 2:
                q.first.return_value = existing_doc
            elif call_count["n"] == 3:
                q.first.return_value = existing_link  # already linked
            return q

        db_session.query = Mock(side_effect=query_side_effect)

        mock_password_store = Mock()
        mock_password_store.get_session_password.return_value = None

        mock_pdf_manager = Mock()
        mock_pdf_manager.upgrade_to_pdf.return_value = True  # upgrade happened

        with _auth_client(
            app,
            mock_db_session=db_session,
            settings_overrides={
                "research_library.upload_pdf_storage": "database"
            },
            extra_patches=[
                patch(
                    f"{_DB_PASS}.session_password_store", mock_password_store
                ),
                patch(
                    "local_deep_research.research_library.services.pdf_storage_manager.PDFStorageManager",
                    return_value=mock_pdf_manager,
                ),
            ],
        ) as (client, ctx):
            resp = client.post(
                "/library/api/collections/coll-1/upload",
                data={"files": (BytesIO(b"%PDF-content"), "scan.pdf")},
                content_type="multipart/form-data",
            )
        assert resp.status_code == 200
        rdata = resp.get_json()
        assert rdata["success"] is True
        assert rdata["uploaded"][0]["status"] == "pdf_upgraded"
        assert rdata["uploaded"][0]["pdf_upgraded"] is True

    def test_upload_new_doc_unsupported_extension(self, app):
        """File with unsupported extension → error entry, not in uploaded list."""
        mock_coll = Mock()
        mock_coll.id = "coll-1"

        db_session = _make_db_session()
        call_count = {"n": 0}

        def query_side_effect(model):
            call_count["n"] += 1
            q = _build_mock_query()
            if call_count["n"] == 1:
                q.first.return_value = mock_coll  # Collection found
            elif call_count["n"] == 2:
                q.first.return_value = None  # No existing doc by hash
            return q

        db_session.query = Mock(side_effect=query_side_effect)

        mock_password_store = Mock()
        mock_password_store.get_session_password.return_value = None

        with _auth_client(
            app,
            mock_db_session=db_session,
            extra_patches=[
                patch(
                    f"{_DOC_LOADERS}.is_extension_supported", return_value=False
                ),
                patch(
                    f"{_DB_PASS}.session_password_store", mock_password_store
                ),
            ],
        ) as (client, ctx):
            resp = client.post(
                "/library/api/collections/coll-1/upload",
                data={"files": (BytesIO(b"data"), "file.xyz")},
                content_type="multipart/form-data",
            )
        assert resp.status_code == 200
        rdata = resp.get_json()
        assert rdata["success"] is True
        assert rdata["summary"]["successful"] == 0
        assert len(rdata["errors"]) == 1
        assert "Unsupported format" in rdata["errors"][0]["error"]

    def test_upload_new_doc_no_text_extracted(self, app):
        """File that produces empty extracted text → error entry."""
        mock_coll = Mock()
        mock_coll.id = "coll-1"

        db_session = _make_db_session()
        call_count = {"n": 0}

        def query_side_effect(model):
            call_count["n"] += 1
            q = _build_mock_query()
            if call_count["n"] == 1:
                q.first.return_value = mock_coll
            elif call_count["n"] == 2:
                q.first.return_value = None  # No existing doc
            return q

        db_session.query = Mock(side_effect=query_side_effect)

        mock_password_store = Mock()
        mock_password_store.get_session_password.return_value = None

        with _auth_client(
            app,
            mock_db_session=db_session,
            extra_patches=[
                patch(
                    f"{_DOC_LOADERS}.is_extension_supported", return_value=True
                ),
                patch(
                    f"{_DOC_LOADERS}.extract_text_from_bytes", return_value=""
                ),
                patch(
                    f"{_DB_PASS}.session_password_store", mock_password_store
                ),
            ],
        ) as (client, ctx):
            resp = client.post(
                "/library/api/collections/coll-1/upload",
                data={"files": (BytesIO(b"\x00\x01\x02"), "binary.pdf")},
                content_type="multipart/form-data",
            )
        assert resp.status_code == 200
        rdata = resp.get_json()
        assert rdata["success"] is True
        assert rdata["summary"]["successful"] == 0
        assert len(rdata["errors"]) == 1
        assert "Could not extract text" in rdata["errors"][0]["error"]

    def test_upload_new_doc_success_text_only(self, app):
        """New document upload with pdf_storage='none' succeeds; status is 'uploaded'."""
        mock_coll = Mock()
        mock_coll.id = "coll-1"

        mock_source = Mock()
        mock_source.id = "src-001"

        db_session = _make_db_session()
        call_count = {"n": 0}

        def query_side_effect(model):
            call_count["n"] += 1
            q = _build_mock_query()
            if call_count["n"] == 1:
                q.first.return_value = mock_coll  # Collection
            elif call_count["n"] == 2:
                q.first.return_value = None  # No existing doc
            elif call_count["n"] == 3:
                q.first.return_value = mock_source  # SourceType exists
            return q

        db_session.query = Mock(side_effect=query_side_effect)

        mock_password_store = Mock()
        mock_password_store.get_session_password.return_value = None

        with _auth_client(
            app,
            mock_db_session=db_session,
            settings_overrides={"research_library.upload_pdf_storage": "none"},
            extra_patches=[
                patch(
                    f"{_DOC_LOADERS}.is_extension_supported", return_value=True
                ),
                patch(
                    f"{_DOC_LOADERS}.extract_text_from_bytes",
                    return_value="Extracted document text",
                ),
                patch(
                    f"{_TEXT_PROC}.remove_surrogates", side_effect=lambda x: x
                ),
                patch(
                    f"{_DB_PASS}.session_password_store", mock_password_store
                ),
            ],
        ) as (client, ctx):
            resp = client.post(
                "/library/api/collections/coll-1/upload",
                data={"files": (BytesIO(b"some text content"), "doc.txt")},
                content_type="multipart/form-data",
            )
        assert resp.status_code == 200
        rdata = resp.get_json()
        assert rdata["success"] is True
        assert len(rdata["uploaded"]) == 1
        assert rdata["uploaded"][0]["status"] == "uploaded"
        assert rdata["uploaded"][0]["pdf_stored"] is False
        assert rdata["summary"]["successful"] == 1
        assert rdata["summary"]["failed"] == 0

    def test_upload_new_doc_success_with_pdf_db(self, app):
        """New PDF upload with pdf_storage='database' stores the PDF and reports pdf_stored=True."""
        mock_coll = Mock()
        mock_coll.id = "coll-1"

        mock_source = Mock()
        mock_source.id = "src-002"

        db_session = _make_db_session()
        call_count = {"n": 0}

        def query_side_effect(model):
            call_count["n"] += 1
            q = _build_mock_query()
            if call_count["n"] == 1:
                q.first.return_value = mock_coll
            elif call_count["n"] == 2:
                q.first.return_value = None  # No existing doc
            elif call_count["n"] == 3:
                q.first.return_value = mock_source  # SourceType
            return q

        db_session.query = Mock(side_effect=query_side_effect)

        mock_password_store = Mock()
        mock_password_store.get_session_password.return_value = None

        mock_pdf_manager = Mock()
        mock_pdf_manager.save_pdf = Mock()  # succeeds silently

        with _auth_client(
            app,
            mock_db_session=db_session,
            settings_overrides={
                "research_library.upload_pdf_storage": "database"
            },
            extra_patches=[
                patch(
                    f"{_DOC_LOADERS}.is_extension_supported", return_value=True
                ),
                patch(
                    f"{_DOC_LOADERS}.extract_text_from_bytes",
                    return_value="PDF extracted text",
                ),
                patch(
                    f"{_TEXT_PROC}.remove_surrogates", side_effect=lambda x: x
                ),
                patch(
                    f"{_DB_PASS}.session_password_store", mock_password_store
                ),
                patch(
                    "local_deep_research.research_library.services.pdf_storage_manager.PDFStorageManager",
                    return_value=mock_pdf_manager,
                ),
            ],
        ) as (client, ctx):
            resp = client.post(
                "/library/api/collections/coll-1/upload",
                data={
                    "files": (
                        BytesIO(b"%PDF-1.4 real pdf content"),
                        "report.pdf",
                    ),
                    "pdf_storage": "database",
                },
                content_type="multipart/form-data",
            )
        assert resp.status_code == 200
        rdata = resp.get_json()
        assert rdata["success"] is True
        assert len(rdata["uploaded"]) == 1
        assert rdata["uploaded"][0]["status"] == "uploaded"
        assert rdata["uploaded"][0]["pdf_stored"] is True
        mock_pdf_manager.save_pdf.assert_called_once()

    def test_upload_pdf_storage_failure_continues(self, app):
        """When pdf_storage_manager.save_pdf raises, text is still saved and pdf_stored=False."""
        mock_coll = Mock()
        mock_coll.id = "coll-1"

        mock_source = Mock()
        mock_source.id = "src-003"

        db_session = _make_db_session()
        call_count = {"n": 0}

        def query_side_effect(model):
            call_count["n"] += 1
            q = _build_mock_query()
            if call_count["n"] == 1:
                q.first.return_value = mock_coll
            elif call_count["n"] == 2:
                q.first.return_value = None
            elif call_count["n"] == 3:
                q.first.return_value = mock_source
            return q

        db_session.query = Mock(side_effect=query_side_effect)

        mock_password_store = Mock()
        mock_password_store.get_session_password.return_value = None

        mock_pdf_manager = Mock()
        mock_pdf_manager.save_pdf.side_effect = RuntimeError("Disk full")

        with _auth_client(
            app,
            mock_db_session=db_session,
            settings_overrides={
                "research_library.upload_pdf_storage": "database"
            },
            extra_patches=[
                patch(
                    f"{_DOC_LOADERS}.is_extension_supported", return_value=True
                ),
                patch(
                    f"{_DOC_LOADERS}.extract_text_from_bytes",
                    return_value="Some text",
                ),
                patch(
                    f"{_TEXT_PROC}.remove_surrogates", side_effect=lambda x: x
                ),
                patch(
                    f"{_DB_PASS}.session_password_store", mock_password_store
                ),
                patch(
                    "local_deep_research.research_library.services.pdf_storage_manager.PDFStorageManager",
                    return_value=mock_pdf_manager,
                ),
            ],
        ) as (client, ctx):
            resp = client.post(
                "/library/api/collections/coll-1/upload",
                data={
                    "files": (BytesIO(b"%PDF-broken"), "broken.pdf"),
                    "pdf_storage": "database",
                },
                content_type="multipart/form-data",
            )
        assert resp.status_code == 200
        rdata = resp.get_json()
        assert rdata["success"] is True
        # Document was uploaded (text saved) despite PDF storage failure
        assert len(rdata["uploaded"]) == 1
        assert rdata["uploaded"][0]["status"] == "uploaded"
        assert rdata["uploaded"][0]["pdf_stored"] is False

    def test_upload_auto_index_triggered(self, app):
        """Auto-index is triggered when a db_password exists for the session."""
        mock_coll = Mock()
        mock_coll.id = "coll-1"

        mock_source = Mock()
        mock_source.id = "src-004"

        db_session = _make_db_session()
        call_count = {"n": 0}

        def query_side_effect(model):
            call_count["n"] += 1
            q = _build_mock_query()
            if call_count["n"] == 1:
                q.first.return_value = mock_coll
            elif call_count["n"] == 2:
                q.first.return_value = None
            elif call_count["n"] == 3:
                q.first.return_value = mock_source
            return q

        db_session.query = Mock(side_effect=query_side_effect)

        mock_password_store = Mock()
        mock_password_store.get_session_password.return_value = "secret-db-pass"

        mock_trigger = Mock()

        with _auth_client(
            app,
            mock_db_session=db_session,
            extra_patches=[
                patch(
                    f"{_DOC_LOADERS}.is_extension_supported", return_value=True
                ),
                patch(
                    f"{_DOC_LOADERS}.extract_text_from_bytes",
                    return_value="Content for indexing",
                ),
                patch(
                    f"{_TEXT_PROC}.remove_surrogates", side_effect=lambda x: x
                ),
                patch(
                    f"{_DB_PASS}.session_password_store", mock_password_store
                ),
                patch(f"{MODULE}.trigger_auto_index", mock_trigger),
            ],
        ) as (client, ctx):
            resp = client.post(
                "/library/api/collections/coll-1/upload",
                data={"files": (BytesIO(b"indexable content"), "index_me.txt")},
                content_type="multipart/form-data",
            )
        assert resp.status_code == 200
        rdata = resp.get_json()
        assert rdata["success"] is True
        # trigger_auto_index must have been called
        mock_trigger.assert_called_once()
        call_args = mock_trigger.call_args
        assert call_args[0][1] == "coll-1"  # collection_id
        assert call_args[0][2] == "testuser"  # username
        assert call_args[0][3] == "secret-db-pass"  # db_password

    def test_upload_auto_index_no_password(self, app):
        """Auto-index is NOT triggered when db_password is None."""
        mock_coll = Mock()
        mock_coll.id = "coll-1"

        mock_source = Mock()
        mock_source.id = "src-005"

        db_session = _make_db_session()
        call_count = {"n": 0}

        def query_side_effect(model):
            call_count["n"] += 1
            q = _build_mock_query()
            if call_count["n"] == 1:
                q.first.return_value = mock_coll
            elif call_count["n"] == 2:
                q.first.return_value = None
            elif call_count["n"] == 3:
                q.first.return_value = mock_source
            return q

        db_session.query = Mock(side_effect=query_side_effect)

        mock_password_store = Mock()
        mock_password_store.get_session_password.return_value = (
            None  # No password
        )

        mock_trigger = Mock()

        with _auth_client(
            app,
            mock_db_session=db_session,
            extra_patches=[
                patch(
                    f"{_DOC_LOADERS}.is_extension_supported", return_value=True
                ),
                patch(
                    f"{_DOC_LOADERS}.extract_text_from_bytes",
                    return_value="Some indexable text",
                ),
                patch(
                    f"{_TEXT_PROC}.remove_surrogates", side_effect=lambda x: x
                ),
                patch(
                    f"{_DB_PASS}.session_password_store", mock_password_store
                ),
                patch(f"{MODULE}.trigger_auto_index", mock_trigger),
            ],
        ) as (client, ctx):
            resp = client.post(
                "/library/api/collections/coll-1/upload",
                data={"files": (BytesIO(b"plain text"), "nopass.txt")},
                content_type="multipart/form-data",
            )
        assert resp.status_code == 200
        rdata = resp.get_json()
        assert rdata["success"] is True
        # trigger_auto_index must NOT have been called
        mock_trigger.assert_not_called()


# ---------------------------------------------------------------------------
# get_collection_documents tests
# ---------------------------------------------------------------------------


class TestGetCollectionDocuments:
    """Tests for the get_collection_documents route."""

    def test_collection_documents_not_found(self, app):
        """Returns 404 when the collection is not found."""
        db_session = _make_db_session()
        q = _build_mock_query(first_result=None)
        db_session.query = Mock(return_value=q)

        with _auth_client(app, mock_db_session=db_session) as (client, ctx):
            resp = client.get("/library/api/collections/missing-id/documents")
        assert resp.status_code == 404
        data = resp.get_json()
        assert data["success"] is False
        assert "Collection not found" in data["error"]

    def test_collection_documents_with_index_size_formatting(self, app):
        """Index size is formatted as B, KB, or MB depending on the file size."""
        mock_coll = Mock()
        mock_coll.id = "coll-size"
        mock_coll.name = "Size Test Collection"
        mock_coll.description = "Testing size formatting"
        mock_coll.embedding_model = None
        mock_coll.embedding_model_type = None
        mock_coll.embedding_dimension = None
        mock_coll.chunk_size = None
        mock_coll.chunk_overlap = None
        mock_coll.splitter_type = None
        mock_coll.distance_metric = None
        mock_coll.index_type = None
        mock_coll.normalize_vectors = None
        mock_coll.collection_type = "user_uploads"

        # Create a temporary file to act as the index path
        with tempfile.NamedTemporaryFile(delete=False, suffix=".index") as tmp:
            # Write 500 bytes → should format as "500 B"
            tmp.write(b"x" * 500)
            tmp_path = tmp.name

        mock_rag_index = Mock()
        mock_rag_index.index_path = tmp_path

        db_session = _make_db_session()
        call_count = {"n": 0}

        def query_side_effect(*args):
            call_count["n"] += 1
            q = _build_mock_query()
            if call_count["n"] == 1:
                q.first.return_value = mock_coll  # Collection found
            elif call_count["n"] == 2:
                q.all.return_value = []  # No documents
            elif call_count["n"] == 3:
                q.first.return_value = mock_rag_index  # RAGIndex with path
            return q

        db_session.query = Mock(side_effect=query_side_effect)

        with _auth_client(app, mock_db_session=db_session) as (client, ctx):
            resp = client.get("/library/api/collections/coll-size/documents")
        assert resp.status_code == 200
        data = resp.get_json()
        assert data["success"] is True
        # 500 bytes → "500 B"
        assert data["collection"]["index_file_size"] == "500 B"
        assert data["collection"]["index_file_size_bytes"] == 500

        # --- KB branch: write 2048 bytes → "2.0 KB" ---
        with open(tmp_path, "wb") as f:
            f.write(b"k" * 2048)

        call_count["n"] = 0
        db_session.query = Mock(side_effect=query_side_effect)

        with _auth_client(app, mock_db_session=db_session) as (client, ctx):
            resp = client.get("/library/api/collections/coll-size/documents")
        assert resp.status_code == 200
        data = resp.get_json()
        assert data["collection"]["index_file_size"] == "2.0 KB"
        assert data["collection"]["index_file_size_bytes"] == 2048

        # --- MB branch: write 2 * 1024 * 1024 bytes → "2.0 MB" ---
        mb2 = 2 * 1024 * 1024
        with open(tmp_path, "wb") as f:
            f.write(b"m" * mb2)

        call_count["n"] = 0
        db_session.query = Mock(side_effect=query_side_effect)

        with _auth_client(app, mock_db_session=db_session) as (client, ctx):
            resp = client.get("/library/api/collections/coll-size/documents")
        assert resp.status_code == 200
        data = resp.get_json()
        assert data["collection"]["index_file_size"] == "2.0 MB"
        assert data["collection"]["index_file_size_bytes"] == mb2

        # Cleanup
        Path(tmp_path).unlink(missing_ok=True)
