"""
Extra coverage tests for library_routes.py.

Targets the ~111 statements not exercised by test_library_routes_coverage.py
and test_library_routes_deep_coverage.py:

- get_authenticated_user_password: session-store hit, g.user_password fallback,
  AuthenticationRequiredError raise
- download_all_text SSE: auth failure path, resources needing processing,
  txt_path exists pre-scan, download success/failure/exception branches
- download_bulk SSE: auth failure, queue items present (pdf mode, text_only mode),
  exception with paywall/server/generic phrase, queue empty then re-queued
- queue_all_undownloaded: resource with no URL, existing queue entry reset to
  pending, existing queue entry already pending, no filter result for resource
- download_source: existing queue entry reset to pending
- get_research_sources: malformed URL falls back to empty domain
"""

import json
from contextlib import contextmanager
from unittest.mock import MagicMock, Mock, patch

import pytest
from flask import Flask, jsonify

from local_deep_research.web.auth.routes import auth_bp
from local_deep_research.research_library.routes.library_routes import (
    library_bp,
)

# ---------------------------------------------------------------------------
# Constants (matching history_routes reference pattern)
# ---------------------------------------------------------------------------

MODULE = "local_deep_research.research_library.routes.library_routes"
AUTH_DB_MANAGER = "local_deep_research.web.auth.decorators.db_manager"

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _mock_auth():
    """Return a MagicMock that satisfies login_required db_manager check."""
    m = MagicMock()
    m.is_user_connected.return_value = True
    m.connections = {"testuser": True}
    m.has_encryption = False
    return m


def _make_db_ctx(mock_session):
    """Build a mock context-manager for get_user_db_session."""
    ctx = MagicMock()
    ctx.__enter__ = MagicMock(return_value=mock_session)
    ctx.__exit__ = MagicMock(return_value=None)
    return ctx


def _build_mock_query(
    all_result=None, first_result=None, count_result=0, get_result=None
):
    q = Mock()
    q.all.return_value = all_result if all_result is not None else []
    q.first.return_value = first_result
    q.count.return_value = count_result
    q.get.return_value = get_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.limit.return_value = q
    q.offset.return_value = q
    q.delete.return_value = 0
    q.is_.return_value = q
    return q


@contextmanager
def _auth_client(
    app,
    library_service=None,
    download_service=None,
    mock_db_session=None,
    settings_overrides=None,
    get_auth_password="mock_password",
    render_return="<html>ok</html>",
    extra_patches=None,
):
    """
    Authenticated test client with all external dependencies mocked.

    Yields (client, ctx_dict) where ctx_dict contains references to mocks.
    """
    mock_db = _mock_auth()
    lib_svc = library_service or Mock()
    lib_cls = Mock(return_value=lib_svc)
    dl_svc = download_service or Mock()
    dl_svc.__enter__ = Mock(return_value=dl_svc)
    dl_svc.__exit__ = Mock(return_value=False)
    dl_cls = Mock(return_value=dl_svc)
    db_session = mock_db_session or Mock()
    if not hasattr(db_session, "query") or not callable(
        getattr(db_session, "query", None)
    ):
        db_session = Mock()
        db_session.query = Mock(return_value=_build_mock_query())
    db_session.commit = Mock()
    db_session.add = Mock()

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

    mock_sm = Mock()
    defaults = {
        "research_library.pdf_storage_mode": "database",
        "research_library.shared_library": False,
        "research_library.storage_path": "/tmp/test_lib",
    }
    if settings_overrides:
        defaults.update(settings_overrides)
    mock_sm.get_setting.side_effect = lambda k, d=None: defaults.get(k, d)
    mock_render = Mock(return_value=render_return)

    patches = [
        patch(AUTH_DB_MANAGER, mock_db),
        patch(f"{MODULE}.LibraryService", lib_cls),
        patch(f"{MODULE}.DownloadService", dl_cls),
        patch(
            f"{MODULE}.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}.render_template_with_defaults", mock_render),
        patch(
            f"{MODULE}.get_authenticated_user_password",
            return_value=get_auth_password,
        ),
    ]
    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,
                {
                    "library_service": lib_svc,
                    "download_service": dl_svc,
                    "download_cls": dl_cls,
                    "db_session": db_session,
                    "settings": mock_sm,
                    "render": mock_render,
                },
            )
    finally:
        for p in patches:
            p.stop()


def _authed_post(app, path, **kwargs):
    """Issue an authenticated POST request and return the response."""
    with app.test_client() as c:
        with c.session_transaction() as sess:
            sess["username"] = "testuser"
            sess["session_id"] = "test-session-id"
        return c.post(path, **kwargs)


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------


@pytest.fixture()
def app():
    """Minimal Flask app with auth and library blueprints."""
    application = Flask(__name__)
    application.config["SECRET_KEY"] = "test-secret"
    application.config["TESTING"] = True
    application.register_blueprint(auth_bp)
    application.register_blueprint(library_bp)

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

    return application


# ---------------------------------------------------------------------------
# get_authenticated_user_password
# ---------------------------------------------------------------------------


# ---------------------------------------------------------------------------
# download_all_text SSE stream
# ---------------------------------------------------------------------------


class TestDownloadAllTextSseStream:
    """Exercise the SSE generator in download_all_text."""

    def test_auth_failure_yields_error_event(self, app):
        """When get_authenticated_user_password raises, SSE stream returns error."""
        from local_deep_research.web.exceptions import (
            AuthenticationRequiredError,
        )

        mock_db = _mock_auth()
        mock_sm = Mock()
        mock_sm.get_setting.return_value = "database"

        with (
            patch(AUTH_DB_MANAGER, mock_db),
            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}.LibraryService", Mock()),
            patch(
                f"{MODULE}.get_authenticated_user_password",
                side_effect=AuthenticationRequiredError("need auth"),
            ),
        ):
            with app.test_client() as c:
                with c.session_transaction() as sess:
                    sess["username"] = "testuser"
                    sess["session_id"] = "test-session-id"
                resp = c.post("/library/api/download-all-text")
            assert resp.status_code == 200
            assert "text/event-stream" in resp.content_type
            data = resp.data.decode()
            assert "Authentication required" in data

    def test_processes_resources_needing_extraction(self, app):
        """Resources not in txt_path are downloaded; success path emits SSE."""
        resource = Mock()
        resource.id = 5
        resource.url = "https://arxiv.org/abs/1234"
        resource.title = "My Paper"

        db_session = Mock()
        db_session.query.return_value = _build_mock_query(all_result=[resource])

        dl_svc = Mock()
        dl_svc.download_as_text.return_value = (True, None)
        dl_svc.library_root = "/tmp/lib_no_txt"

        with _auth_client(
            app,
            mock_db_session=db_session,
            extra_patches=[
                patch(f"{MODULE}.DownloadService", return_value=dl_svc),
                patch(f"{MODULE}.is_downloadable_url", return_value=True),
                patch(
                    "local_deep_research.utilities.resource_utils.safe_close",
                    return_value=None,
                ),
            ],
        ) as (client, _):
            resp = client.post("/library/api/download-all-text")
            assert resp.status_code == 200
            assert "text/event-stream" in resp.content_type
            body = resp.data.decode()
            assert "complete" in body

    def test_download_failure_emits_failed_status(self, app):
        """When download_as_text returns failure, status=failed is emitted."""
        resource = Mock()
        resource.id = 6
        resource.url = "https://arxiv.org/abs/5678"
        resource.title = "Failing Paper"

        db_session = Mock()
        db_session.query.return_value = _build_mock_query(all_result=[resource])

        dl_svc = Mock()
        dl_svc.download_as_text.return_value = (False, "Timeout error")
        dl_svc.library_root = "/tmp/lib_no_txt2"

        with _auth_client(
            app,
            mock_db_session=db_session,
            extra_patches=[
                patch(f"{MODULE}.DownloadService", return_value=dl_svc),
                patch(f"{MODULE}.is_downloadable_url", return_value=True),
                patch(
                    "local_deep_research.utilities.resource_utils.safe_close",
                    return_value=None,
                ),
            ],
        ) as (client, _):
            resp = client.post("/library/api/download-all-text")
            body = resp.data.decode()
            assert "failed" in body

    def test_exception_during_download_emits_failed_status(self, app):
        """When download_as_text raises, status=failed is emitted."""
        resource = Mock()
        resource.id = 7
        resource.url = "https://arxiv.org/abs/9999"
        resource.title = "Exception Paper"

        db_session = Mock()
        db_session.query.return_value = _build_mock_query(all_result=[resource])

        dl_svc = Mock()
        dl_svc.download_as_text.side_effect = RuntimeError("crash")
        dl_svc.library_root = "/tmp/lib_no_txt3"

        with _auth_client(
            app,
            mock_db_session=db_session,
            extra_patches=[
                patch(f"{MODULE}.DownloadService", return_value=dl_svc),
                patch(f"{MODULE}.is_downloadable_url", return_value=True),
                patch(
                    "local_deep_research.utilities.resource_utils.safe_close",
                    return_value=None,
                ),
            ],
        ) as (client, _):
            resp = client.post("/library/api/download-all-text")
            body = resp.data.decode()
            assert "failed" in body


# ---------------------------------------------------------------------------
# download_bulk SSE stream
# ---------------------------------------------------------------------------


class TestDownloadBulkSseStream:
    """Exercise the SSE generator in download_bulk with queue items present."""

    @staticmethod
    def _collect_sse_events(body):
        """Parse 'data: <json>' SSE lines into a list of dicts."""
        return [
            json.loads(line[6:])
            for line in body.split("\n")
            if line.startswith("data: ")
        ]

    def test_auth_failure_in_bulk_yields_error_event(self, app):
        """When auth fails inside the generator, SSE error is emitted."""
        from local_deep_research.web.exceptions import (
            AuthenticationRequiredError,
        )

        mock_db = _mock_auth()
        mock_sm = Mock()
        mock_sm.get_setting.return_value = "database"

        with (
            patch(AUTH_DB_MANAGER, mock_db),
            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}.LibraryService", Mock()),
            patch(
                f"{MODULE}.get_authenticated_user_password",
                side_effect=AuthenticationRequiredError("need auth"),
            ),
        ):
            with app.test_client() as c:
                with c.session_transaction() as sess:
                    sess["username"] = "testuser"
                    sess["session_id"] = "test-session-id"
                resp = c.post(
                    "/library/api/download-bulk",
                    json={"research_ids": ["r1"], "mode": "pdf"},
                    content_type="application/json",
                )
            assert resp.status_code == 200
            body = resp.data.decode()
            assert "Authentication required" in body

    def test_pdf_mode_processes_queue_items(self, app):
        """With queue items present, pdf mode calls download_resource per item."""
        queue_item = Mock()
        queue_item.resource_id = 10
        resource = Mock()
        resource.title = "Queue Paper"

        db_session = Mock()
        q = _build_mock_query(all_result=[queue_item], count_result=1)
        q.get.return_value = resource
        db_session.query = Mock(return_value=q)
        db_session.commit = Mock()

        dl_svc = Mock()
        dl_svc.download_resource.return_value = (True, None)
        dl_svc.queue_research_downloads.return_value = 0

        with _auth_client(
            app,
            mock_db_session=db_session,
            download_service=dl_svc,
            extra_patches=[
                patch(
                    "local_deep_research.utilities.resource_utils.safe_close",
                    return_value=None,
                ),
            ],
        ) as (client, _):
            resp = client.post(
                "/library/api/download-bulk",
                json={"research_ids": ["r1"], "mode": "pdf"},
                content_type="application/json",
            )
            assert resp.status_code == 200
            body = resp.data.decode()
            assert "complete" in body

    def test_text_only_mode_calls_download_as_text(self, app):
        """text_only mode calls download_as_text rather than download_resource."""
        queue_item = Mock()
        queue_item.resource_id = 11
        resource = Mock()
        resource.title = "Text Paper"

        db_session = Mock()
        q = _build_mock_query(all_result=[queue_item], count_result=1)
        q.get.return_value = resource
        db_session.query = Mock(return_value=q)
        db_session.commit = Mock()

        dl_svc = Mock()
        dl_svc.download_as_text.return_value = (True, None)
        dl_svc.download_resource.return_value = (True, None)
        dl_svc.queue_research_downloads.return_value = 0

        with _auth_client(
            app,
            mock_db_session=db_session,
            download_service=dl_svc,
            extra_patches=[
                patch(
                    "local_deep_research.utilities.resource_utils.safe_close",
                    return_value=None,
                ),
            ],
        ) as (client, _):
            resp = client.post(
                "/library/api/download-bulk",
                json={"research_ids": ["r1"], "mode": "text_only"},
                content_type="application/json",
            )
            assert resp.status_code == 200
            body = resp.data.decode()
            assert "complete" in body

    def test_exception_with_paywall_phrase_emits_skipped(self, app):
        """Exception containing 'paywall' results in status=skipped."""
        queue_item = Mock()
        queue_item.resource_id = 12
        resource = Mock()
        resource.title = "Paywalled Paper"

        db_session = Mock()
        q = _build_mock_query(all_result=[queue_item], count_result=1)
        q.get.return_value = resource
        db_session.query = Mock(return_value=q)
        db_session.commit = Mock()

        dl_svc = Mock()
        dl_svc.download_resource.side_effect = Exception(
            "paywall detected - cannot access"
        )
        dl_svc.queue_research_downloads.return_value = 0

        with _auth_client(
            app,
            mock_db_session=db_session,
            download_service=dl_svc,
            extra_patches=[
                patch(
                    "local_deep_research.utilities.resource_utils.safe_close",
                    return_value=None,
                ),
            ],
        ) as (client, _):
            resp = client.post(
                "/library/api/download-bulk",
                json={"research_ids": ["r1"], "mode": "pdf"},
                content_type="application/json",
            )
            body = resp.data.decode()
            assert "skipped" in body

    def test_exception_with_server_phrase_emits_failed(self, app):
        """Exception containing 'server' results in status=failed."""
        queue_item = Mock()
        queue_item.resource_id = 13
        resource = Mock()
        resource.title = "Server Error Paper"

        db_session = Mock()
        q = _build_mock_query(all_result=[queue_item], count_result=1)
        q.get.return_value = resource
        db_session.query = Mock(return_value=q)
        db_session.commit = Mock()

        dl_svc = Mock()
        dl_svc.download_resource.side_effect = Exception("server returned 503")
        dl_svc.queue_research_downloads.return_value = 0

        with _auth_client(
            app,
            mock_db_session=db_session,
            download_service=dl_svc,
            extra_patches=[
                patch(
                    "local_deep_research.utilities.resource_utils.safe_close",
                    return_value=None,
                ),
            ],
        ) as (client, _):
            resp = client.post(
                "/library/api/download-bulk",
                json={"research_ids": ["r1"], "mode": "pdf"},
                content_type="application/json",
            )
            body = resp.data.decode()
            assert "failed" in body

    def test_exception_with_generic_phrase_emits_failed(self, app):
        """Generic exception (no known phrase) results in status=failed."""
        queue_item = Mock()
        queue_item.resource_id = 14
        resource = Mock()
        resource.title = "Generic Error Paper"

        db_session = Mock()
        q = _build_mock_query(all_result=[queue_item], count_result=1)
        q.get.return_value = resource
        db_session.query = Mock(return_value=q)
        db_session.commit = Mock()

        dl_svc = Mock()
        dl_svc.download_resource.side_effect = Exception("something went wrong")
        dl_svc.queue_research_downloads.return_value = 0

        with _auth_client(
            app,
            mock_db_session=db_session,
            download_service=dl_svc,
            extra_patches=[
                patch(
                    "local_deep_research.utilities.resource_utils.safe_close",
                    return_value=None,
                ),
            ],
        ) as (client, _):
            resp = client.post(
                "/library/api/download-bulk",
                json={"research_ids": ["r1"], "mode": "pdf"},
                content_type="application/json",
            )
            body = resp.data.decode()
            assert "failed" in body

    def test_empty_queue_auto_queues_then_processes(self, app):
        """When no queue items found initially, queue_research_downloads is called."""
        db_session = Mock()
        empty_q = _build_mock_query(all_result=[], count_result=0)
        db_session.query = Mock(return_value=empty_q)
        db_session.commit = Mock()

        dl_svc = Mock()
        dl_svc.queue_research_downloads.return_value = 0
        dl_svc.download_resource.return_value = (True, None)

        with _auth_client(
            app,
            mock_db_session=db_session,
            download_service=dl_svc,
            extra_patches=[
                patch(
                    "local_deep_research.utilities.resource_utils.safe_close",
                    return_value=None,
                ),
            ],
        ) as (client, _):
            resp = client.post(
                "/library/api/download-bulk",
                json={"research_ids": ["r1"], "mode": "pdf"},
                content_type="application/json",
            )
            assert resp.status_code == 200
            body = resp.data.decode()
            assert "complete" in body
            dl_svc.queue_research_downloads.assert_called_once_with("r1", None)

    def test_queue_research_exception_continues(self, app):
        """Exception in queue_research_downloads is swallowed; processing continues."""
        db_session = Mock()
        empty_q = _build_mock_query(all_result=[], count_result=0)
        db_session.query = Mock(return_value=empty_q)
        db_session.commit = Mock()

        dl_svc = Mock()
        dl_svc.queue_research_downloads.side_effect = RuntimeError("db locked")
        dl_svc.download_resource.return_value = (True, None)

        with _auth_client(
            app,
            mock_db_session=db_session,
            download_service=dl_svc,
            extra_patches=[
                patch(
                    "local_deep_research.utilities.resource_utils.safe_close",
                    return_value=None,
                ),
            ],
        ) as (client, _):
            resp = client.post(
                "/library/api/download-bulk",
                json={"research_ids": ["r1"], "mode": "pdf"},
                content_type="application/json",
            )
            assert resp.status_code == 200
            body = resp.data.decode()
            assert "complete" in body

    def test_initial_total_reflects_post_queue_count(self, app):
        """Regression test for issue #4660.

        Before the fix, `download_bulk` counted PENDING queue items BEFORE
        the pre-queue pass populated them, so the initial SSE event carried
        total=0 and the UI showed "X / 0 files". The fix moves the count
        after `queue_research_downloads`; this test verifies both that the
        initial event's total matches the post-queue count AND that
        `queue_research_downloads` actually runs before the count query
        (without the order check, a mock that always returns 3 from
        `.count()` would satisfy the assertions even under the old code).
        """
        call_order = []

        def _record_queue(*_args, **_kwargs):
            call_order.append("queue")
            return 3

        def _record_count():
            call_order.append("count")
            return 3

        queue_items = []
        resources = []
        for i in range(3):
            item = Mock()
            item.resource_id = 100 + i
            queue_items.append(item)
            resource = Mock()
            resource.title = f"Paper {i}"
            resources.append(resource)

        db_session = Mock()
        q = _build_mock_query(all_result=queue_items, count_result=3)
        q.get.side_effect = resources
        q.count.side_effect = _record_count
        db_session.query = Mock(return_value=q)
        db_session.commit = Mock()

        dl_svc = Mock()
        dl_svc.queue_research_downloads.side_effect = _record_queue
        dl_svc.download_resource.return_value = (True, None)

        with _auth_client(
            app,
            mock_db_session=db_session,
            download_service=dl_svc,
            extra_patches=[
                patch(
                    "local_deep_research.utilities.resource_utils.safe_close",
                    return_value=None,
                ),
            ],
        ) as (client, _):
            resp = client.post(
                "/library/api/download-bulk",
                json={"research_ids": ["r1"], "mode": "pdf"},
                content_type="application/json",
            )
            body = resp.data.decode()

        assert resp.status_code == 200, body
        events = self._collect_sse_events(body)
        assert events, f"No SSE events in body: {body[:500]}"

        # The FIRST event is the initial progress emission. Its total must
        # be the post-queue count (3), not 0 (which was the bug from
        # issue #4660 — emitting total=0 because the count ran before
        # queue_research_downloads populated the queue).
        initial = events[0]
        assert initial.get("total") == 3, (
            f"Initial SSE event total should be 3 (post-queue count), "
            f"got {initial.get('total')}. Initial event: {initial}"
        )
        assert initial.get("total") != 0, (
            "Initial total=0 was the bug from issue #4660"
        )
        assert call_order == ["queue", "count"], (
            f"queue_research_downloads must run BEFORE the count query "
            f"(otherwise the count sees an empty queue and the UI shows "
            f"'X / 0 files'). Got order: {call_order}"
        )

    def test_total_sums_across_multiple_research_ids(self, app):
        """For multiple research_ids, total is the sum of per-research
        PENDING counts (the `total += count` accumulator).
        """
        # r1 has 2 pending items, r2 has 3 pending items → total = 5.
        r1_items = [Mock(), Mock()]
        r2_items = [Mock(), Mock(), Mock()]
        for i, item in enumerate(r1_items + r2_items):
            item.resource_id = 100 + i
        resource = Mock()
        resource.title = "Paper"

        db_session = Mock()
        q = _build_mock_query()
        # count() is called once per research_id in the pre-pass loop.
        q.count.side_effect = [2, 3]
        # all() is called once per research_id in the processing loop.
        q.all.side_effect = [r1_items, r2_items]
        # get() is called once per item across both researches.
        q.get.return_value = resource
        db_session.query = Mock(return_value=q)
        db_session.commit = Mock()

        dl_svc = Mock()
        dl_svc.queue_research_downloads.return_value = 0
        dl_svc.download_resource.return_value = (True, None)

        with _auth_client(
            app,
            mock_db_session=db_session,
            download_service=dl_svc,
            extra_patches=[
                patch(
                    "local_deep_research.utilities.resource_utils.safe_close",
                    return_value=None,
                ),
            ],
        ) as (client, _):
            resp = client.post(
                "/library/api/download-bulk",
                json={"research_ids": ["r1", "r2"], "mode": "pdf"},
                content_type="application/json",
            )
            body = resp.data.decode()

        assert resp.status_code == 200, body
        events = self._collect_sse_events(body)
        # Initial event must carry the summed total (5), proving the
        # accumulator works across research_ids.
        assert events[0].get("total") == 5, (
            f"Initial total should be 5 (2+3 from r1+r2). Events: {events[:3]}"
        )
        # queue_research_downloads called once per research_id.
        assert dl_svc.queue_research_downloads.call_count == 2

    def test_emits_error_when_all_queue_calls_fail(self, app):
        """Regression for the silent-failure UX gap: if queue_research_downloads
        raises for every research_id and nothing was already queued, the SSE
        stream must include an error event so the UI alerts the user instead
        of silently completing with "0 / 0 files" success.
        """
        db_session = Mock()
        empty_q = _build_mock_query(all_result=[], count_result=0)
        db_session.query = Mock(return_value=empty_q)
        db_session.commit = Mock()

        dl_svc = Mock()
        dl_svc.queue_research_downloads.side_effect = RuntimeError("db locked")
        dl_svc.download_resource.return_value = (True, None)

        with _auth_client(
            app,
            mock_db_session=db_session,
            download_service=dl_svc,
            extra_patches=[
                patch(
                    "local_deep_research.utilities.resource_utils.safe_close",
                    return_value=None,
                ),
            ],
        ) as (client, _):
            resp = client.post(
                "/library/api/download-bulk",
                json={"research_ids": ["r1"], "mode": "pdf"},
                content_type="application/json",
            )
            body = resp.data.decode()

        assert resp.status_code == 200, body
        events = self._collect_sse_events(body)
        # The terminal event must carry `complete: True` AND `error`,
        # so handleSSECompletion in the frontend fires alert(error).
        terminal = events[-1]
        assert terminal.get("complete") is True, (
            f"Expected terminal event with complete=True. Events: {events}"
        )
        assert terminal.get("error"), (
            f"Expected error message on total-queue-failure. Terminal: {terminal}"
        )
        assert "queueing failed" in terminal["error"], (
            f"Error should mention queueing failure. Got: {terminal['error']}"
        )
        assert terminal.get("total") == 0

    def test_emits_nothing_to_download_message_when_queue_empty(self, app):
        """The legitimate "nothing left to download" path (e.g. all papers
        already downloaded): queue_research_downloads succeeds but the
        post-queue count stays 0. The terminal event must carry complete=True
        with the "No new papers" message — and must NOT be framed as a
        queueing failure (that wording is reserved for the exception path).
        """
        db_session = Mock()
        empty_q = _build_mock_query(all_result=[], count_result=0)
        db_session.query = Mock(return_value=empty_q)
        db_session.commit = Mock()

        dl_svc = Mock()
        # Succeeds (no exception) but nothing ends up PENDING.
        dl_svc.queue_research_downloads.return_value = 0
        dl_svc.download_resource.return_value = (True, None)

        with _auth_client(
            app,
            mock_db_session=db_session,
            download_service=dl_svc,
            extra_patches=[
                patch(
                    "local_deep_research.utilities.resource_utils.safe_close",
                    return_value=None,
                ),
            ],
        ) as (client, _):
            resp = client.post(
                "/library/api/download-bulk",
                json={"research_ids": ["r1"], "mode": "pdf"},
                content_type="application/json",
            )
            body = resp.data.decode()

        assert resp.status_code == 200, body
        events = self._collect_sse_events(body)
        terminal = events[-1]
        assert terminal.get("complete") is True, (
            f"Expected terminal event with complete=True. Events: {events}"
        )
        assert terminal.get("error"), (
            f"Expected a 'nothing to download' message. Terminal: {terminal}"
        )
        assert "No new papers" in terminal["error"], (
            f"Error should say no new papers. Got: {terminal['error']}"
        )
        # Must NOT be the queue-failure framing — this is a clean empty state.
        assert "queueing failed" not in terminal["error"], (
            f"Empty state misreported as a queue failure: {terminal['error']}"
        )
        assert terminal.get("total") == 0


# ---------------------------------------------------------------------------
# queue_all_undownloaded edge cases
# ---------------------------------------------------------------------------


class TestQueueAllUndownloadedEdgeCases:
    """Exercise the branches inside queue_all_undownloaded not yet covered."""

    def test_resource_with_no_url_is_skipped(self, app):
        """Resources where url is None/empty are counted as skipped."""
        resource = Mock()
        resource.id = 20
        resource.url = None  # no URL
        resource.research_id = "r1"

        filter_result = Mock()
        filter_result.resource_id = 20
        filter_result.can_retry = True

        filter_summary = Mock()
        filter_summary.to_dict.return_value = {"total": 1}
        filter_summary.permanently_failed_count = 0
        filter_summary.temporarily_failed_count = 0

        db_session = Mock()
        q = _build_mock_query(all_result=[resource])
        db_session.query = Mock(return_value=q)
        db_session.commit = Mock()
        db_session.add = Mock()

        mock_rf = Mock()
        mock_rf.filter_downloadable_resources.return_value = [filter_result]
        mock_rf.get_filter_summary.return_value = filter_summary
        mock_rf.get_skipped_resources_info.return_value = []

        with _auth_client(
            app,
            mock_db_session=db_session,
            extra_patches=[
                patch(f"{MODULE}.ResourceFilter", return_value=mock_rf),
            ],
        ) as (client, _):
            resp = client.post("/library/api/queue-all-undownloaded")
            assert resp.status_code == 200
            data = resp.get_json()
            assert data["skipped"] >= 1
            assert data["queued"] == 0

    def test_existing_queue_entry_not_pending_is_reset(self, app):
        """When a queue entry exists but is not PENDING, it is reset to PENDING."""
        resource = Mock()
        resource.id = 21
        resource.url = "https://arxiv.org/abs/2222"
        resource.research_id = "r1"

        from local_deep_research.database.models.library import DocumentStatus

        existing_entry = Mock()
        existing_entry.status = "failed"
        existing_entry.completed_at = "2024-01-01"

        filter_result = Mock()
        filter_result.resource_id = 21
        filter_result.can_retry = True

        filter_summary = Mock()
        filter_summary.to_dict.return_value = {"total": 1}
        filter_summary.permanently_failed_count = 0
        filter_summary.temporarily_failed_count = 0

        db_session = Mock()

        # main_q returned for the outerjoin query (all_result=[resource])
        # queue_q returned for the filter_by(resource_id=...) query
        main_q = _build_mock_query(all_result=[resource])
        queue_q = _build_mock_query(first_result=existing_entry)
        main_q.filter_by = Mock(return_value=queue_q)

        db_session.query = Mock(return_value=main_q)
        db_session.commit = Mock()
        db_session.add = Mock()

        mock_rf = Mock()
        mock_rf.filter_downloadable_resources.return_value = [filter_result]
        mock_rf.get_filter_summary.return_value = filter_summary
        mock_rf.get_skipped_resources_info.return_value = []

        with _auth_client(
            app,
            mock_db_session=db_session,
            extra_patches=[
                patch(f"{MODULE}.ResourceFilter", return_value=mock_rf),
                patch(f"{MODULE}.is_downloadable_domain", return_value=True),
            ],
        ) as (client, _):
            resp = client.post("/library/api/queue-all-undownloaded")
            assert resp.status_code == 200
            data = resp.get_json()
            assert data["queued"] >= 1
            # Entry should have been reset to PENDING
            assert existing_entry.status == DocumentStatus.PENDING
            assert existing_entry.completed_at is None

    def test_existing_queue_entry_already_pending_counted(self, app):
        """When a queue entry is already PENDING, it is still counted."""
        resource = Mock()
        resource.id = 22
        resource.url = "https://arxiv.org/abs/3333"
        resource.research_id = "r1"

        from local_deep_research.database.models.library import DocumentStatus

        existing_entry = Mock()
        existing_entry.status = DocumentStatus.PENDING

        filter_result = Mock()
        filter_result.resource_id = 22
        filter_result.can_retry = True

        filter_summary = Mock()
        filter_summary.to_dict.return_value = {"total": 1}
        filter_summary.permanently_failed_count = 0
        filter_summary.temporarily_failed_count = 0

        db_session = Mock()
        main_q = _build_mock_query(all_result=[resource])
        queue_q = _build_mock_query(first_result=existing_entry)
        main_q.filter_by = Mock(return_value=queue_q)
        db_session.query = Mock(return_value=main_q)
        db_session.commit = Mock()
        db_session.add = Mock()

        mock_rf = Mock()
        mock_rf.filter_downloadable_resources.return_value = [filter_result]
        mock_rf.get_filter_summary.return_value = filter_summary
        mock_rf.get_skipped_resources_info.return_value = []

        with _auth_client(
            app,
            mock_db_session=db_session,
            extra_patches=[
                patch(f"{MODULE}.ResourceFilter", return_value=mock_rf),
                patch(f"{MODULE}.is_downloadable_domain", return_value=True),
            ],
        ) as (client, _):
            resp = client.post("/library/api/queue-all-undownloaded")
            assert resp.status_code == 200
            data = resp.get_json()
            assert data["queued"] >= 1

    def test_no_filter_result_for_resource_skips(self, app):
        """Resources with no filter result at all are counted as skipped."""
        resource = Mock()
        resource.id = 24
        resource.url = "https://arxiv.org/abs/5555"
        resource.research_id = "r1"

        filter_summary = Mock()
        filter_summary.to_dict.return_value = {"total": 1}
        filter_summary.permanently_failed_count = 0
        filter_summary.temporarily_failed_count = 0

        db_session = Mock()
        q = _build_mock_query(all_result=[resource])
        db_session.query = Mock(return_value=q)
        db_session.commit = Mock()

        mock_rf = Mock()
        # Returns empty list — no filter result for resource id 24
        mock_rf.filter_downloadable_resources.return_value = []
        mock_rf.get_filter_summary.return_value = filter_summary
        mock_rf.get_skipped_resources_info.return_value = []

        with _auth_client(
            app,
            mock_db_session=db_session,
            extra_patches=[
                patch(f"{MODULE}.ResourceFilter", return_value=mock_rf),
            ],
        ) as (client, _):
            resp = client.post("/library/api/queue-all-undownloaded")
            assert resp.status_code == 200
            data = resp.get_json()
            assert data["queued"] == 0
            assert data["skipped"] >= 1


# ---------------------------------------------------------------------------
# download_source: existing queue entry reset to pending
# ---------------------------------------------------------------------------


class TestDownloadSourceExistingQueueEntry:
    """Covers the branch where a queue entry already exists and is reset."""

    def test_existing_queue_entry_is_reset_to_pending(self, app):
        """When an existing queue entry is found, its status/priority are updated."""
        resource = Mock()
        resource.id = 30
        resource.research_id = "r1"

        from local_deep_research.database.models.library import DocumentStatus

        existing_entry = Mock()
        existing_entry.status = "failed"
        existing_entry.priority = 0

        db_session = Mock()

        # query().filter_by() for the resource lookup returns resource
        # query().filter_by() for the queue lookup returns existing_entry
        resource_q = _build_mock_query(first_result=resource)
        queue_q = _build_mock_query(first_result=existing_entry)

        call_n = [0]

        def mock_filter_by(**kw):
            call_n[0] += 1
            if call_n[0] <= 1:
                return resource_q
            return queue_q

        resource_q.filter_by = Mock(side_effect=mock_filter_by)
        queue_q.filter_by = Mock(return_value=queue_q)
        db_session.query = Mock(return_value=resource_q)
        db_session.commit = Mock()
        db_session.add = Mock()

        dl_svc = Mock()
        dl_svc.download_resource.return_value = (True, None)
        dl_svc.__enter__ = Mock(return_value=dl_svc)
        dl_svc.__exit__ = Mock(return_value=False)

        with _auth_client(
            app,
            mock_db_session=db_session,
            download_service=dl_svc,
            extra_patches=[
                patch(f"{MODULE}.is_downloadable_domain", return_value=True),
                patch(f"{MODULE}.get_document_for_resource", return_value=None),
            ],
        ) as (client, _):
            resp = client.post(
                "/library/api/download-source",
                json={
                    "research_id": "r1",
                    "url": "https://arxiv.org/abs/1234",
                },
                content_type="application/json",
            )
            assert resp.status_code == 200
            # Entry should have been reset
            assert existing_entry.status == DocumentStatus.PENDING
            assert existing_entry.priority == 1
