"""
Comprehensive coverage tests for library_routes.py.

Exercises every route handler with precise assertions on response bodies,
status codes, and service/mock interactions. Complements the existing
test_library_routes.py which focuses on route existence and utility functions.
"""

import json
from contextlib import contextmanager
from unittest.mock import 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,
)

# Module path shorthand for patching
_ROUTES = "local_deep_research.research_library.routes.library_routes"


# ---------------------------------------------------------------------------
# Test infrastructure
# ---------------------------------------------------------------------------


def _create_app():
    """Minimal Flask app with library blueprint registered."""
    app = Flask(__name__)
    app.config["SECRET_KEY"] = "test-secret"
    app.config["WTF_CSRF_ENABLED"] = False
    app.config["TESTING"] = True
    app.register_blueprint(auth_bp)
    app.register_blueprint(library_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."""
    mock_db = Mock()
    mock_db.is_user_connected.return_value = True
    mock_db.connections = {"testuser": True}
    mock_db.has_encryption = False
    return mock_db


@contextmanager
def _mock_user_db_session(mock_session=None):
    """Return a context-manager that yields mock_session."""
    yield mock_session or Mock()


def _build_mock_query(
    all_result=None, first_result=None, count_result=0, get_result=None
):
    """Build a chainable mock query."""
    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
    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>",
):
    """
    Context manager providing an authenticated test client with full mocking.

    Parameters control mock return values for different services.
    """
    mock_db = _mock_db_manager()

    # LibraryService
    lib_svc = library_service or Mock()
    lib_cls = Mock(return_value=lib_svc)

    # DownloadService (context manager)
    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
    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

    # Settings manager
    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("local_deep_research.web.auth.decorators.db_manager", mock_db),
        patch(f"{_ROUTES}.LibraryService", lib_cls),
        patch(f"{_ROUTES}.DownloadService", dl_cls),
        patch(
            f"{_ROUTES}.get_user_db_session",
            side_effect=fake_get_user_db_session,
        ),
        patch(f"{_ROUTES}.get_settings_manager", return_value=mock_sm),
        # Also patch the db_utils source so function-local imports pick it up
        patch(
            "local_deep_research.utilities.db_utils.get_settings_manager",
            return_value=mock_sm,
        ),
        patch(f"{_ROUTES}.render_template_with_defaults", mock_render),
        patch(
            f"{_ROUTES}.get_authenticated_user_password",
            return_value=get_auth_password,
        ),
    ]

    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()


# ---------------------------------------------------------------------------
# Unauthenticated access tests
# ---------------------------------------------------------------------------


class TestUnauthenticatedAccess:
    """Verify routes redirect or 401 when no session is set."""

    @pytest.fixture
    def app(self):
        return _create_app()

    def _unauthenticated_client(self, app):
        mock_db = Mock()
        mock_db.is_user_connected.return_value = False
        with patch(
            "local_deep_research.web.auth.decorators.db_manager", mock_db
        ):
            return app.test_client()

    def test_library_page_redirects(self, app):
        client = self._unauthenticated_client(app)
        resp = client.get("/library/")
        assert resp.status_code == 302

    def test_api_stats_redirects(self, app):
        """API routes under /library/api/ redirect (path starts with /library/ not /api/)."""
        client = self._unauthenticated_client(app)
        resp = client.get("/library/api/stats")
        assert resp.status_code == 302

    def test_download_manager_redirects(self, app):
        client = self._unauthenticated_client(app)
        resp = client.get("/library/download-manager")
        assert resp.status_code == 302

    def test_document_details_redirects(self, app):
        client = self._unauthenticated_client(app)
        resp = client.get("/library/document/some-id")
        assert resp.status_code == 302

    def test_download_single_resource_redirects(self, app):
        client = self._unauthenticated_client(app)
        resp = client.post("/library/api/download/1")
        assert resp.status_code == 302


# ---------------------------------------------------------------------------
# Library page route
# ---------------------------------------------------------------------------


class TestLibraryPage:
    @pytest.fixture
    def app(self):
        return _create_app()

    def test_renders_with_stats_and_documents(self, app):
        lib_svc = Mock()
        lib_svc.get_library_stats.return_value = {
            "total_documents": 5,
            "storage_path": "/tmp/lib",
        }
        lib_svc.get_documents.return_value = [{"id": "d1", "title": "Doc 1"}]
        lib_svc.get_unique_domains.return_value = ["arxiv.org"]
        lib_svc.get_research_list_with_stats.return_value = []
        lib_svc.get_all_collections.return_value = []

        with _auth_client(app, library_service=lib_svc) as (client, mocks):
            resp = client.get("/library/")
            assert resp.status_code == 200
            mocks["render"].assert_called_once()
            call_kwargs = mocks["render"].call_args
            # Template name is the first positional arg
            assert call_kwargs[0][0] == "pages/library.html"

    def test_passes_filter_params(self, app):
        lib_svc = Mock()
        lib_svc.get_library_stats.return_value = {"storage_path": ""}
        lib_svc.get_documents.return_value = []
        lib_svc.get_unique_domains.return_value = []
        lib_svc.get_research_list_with_stats.return_value = []
        lib_svc.get_all_collections.return_value = []

        with _auth_client(app, library_service=lib_svc) as (client, mocks):
            resp = client.get(
                "/library/?domain=arxiv.org&research=r1&collection=c1"
            )
            assert resp.status_code == 200
            lib_svc.get_documents.assert_called_once_with(
                research_id="r1",
                domain="arxiv.org",
                collection_id="c1",
                limit=100,
            )

    def test_pdf_storage_none_disables_button(self, app):
        lib_svc = Mock()
        lib_svc.get_library_stats.return_value = {"storage_path": ""}
        lib_svc.get_documents.return_value = []
        lib_svc.get_unique_domains.return_value = []
        lib_svc.get_research_list_with_stats.return_value = []
        lib_svc.get_all_collections.return_value = []

        with _auth_client(
            app,
            library_service=lib_svc,
            settings_overrides={"research_library.pdf_storage_mode": "none"},
        ) as (client, mocks):
            resp = client.get("/library/")
            assert resp.status_code == 200
            call_kw = mocks["render"].call_args[1]
            assert call_kw["enable_pdf_storage"] is False
            assert call_kw["pdf_storage_mode"] == "none"


# ---------------------------------------------------------------------------
# Document details page
# ---------------------------------------------------------------------------


class TestDocumentDetailsPage:
    @pytest.fixture
    def app(self):
        return _create_app()

    def test_found_document(self, app):
        lib_svc = Mock()
        lib_svc.get_document_by_id.return_value = {"id": "d1", "title": "Test"}

        with _auth_client(app, library_service=lib_svc) as (client, mocks):
            resp = client.get("/library/document/d1")
            assert resp.status_code == 200
            mocks["render"].assert_called_once()
            assert (
                mocks["render"].call_args[0][0] == "pages/document_details.html"
            )

    def test_not_found_document(self, app):
        lib_svc = Mock()
        lib_svc.get_document_by_id.return_value = None

        with _auth_client(app, library_service=lib_svc) as (client, _):
            resp = client.get("/library/document/nonexistent")
            assert resp.status_code == 404


# ---------------------------------------------------------------------------
# Download manager page
# ---------------------------------------------------------------------------


class TestDownloadManagerPage:
    @pytest.fixture
    def app(self):
        return _create_app()

    def test_renders_with_summary_stats(self, app):
        lib_svc = Mock()
        lib_svc.get_research_list_with_stats.return_value = [
            {
                "id": "r1",
                "total_resources": 10,
                "downloaded_count": 3,
                "downloadable_count": 8,
            }
        ]
        lib_svc.get_documents.return_value = [
            {
                "domain": "arxiv.org",
                "file_type": "pdf",
                "download_status": "completed",
            },
            {
                "domain": "arxiv.org",
                "file_type": "pdf",
                "download_status": "pending",
            },
        ]

        with _auth_client(app, library_service=lib_svc) as (client, mocks):
            resp = client.get("/library/download-manager")
            assert resp.status_code == 200
            call_kw = mocks["render"].call_args[1]
            assert call_kw["total_researches"] == 1
            assert call_kw["total_resources"] == 10
            assert call_kw["already_downloaded"] == 3
            assert call_kw["available_to_download"] == 5  # 8 - 3


# ---------------------------------------------------------------------------
# API: /api/stats
# ---------------------------------------------------------------------------


class TestGetLibraryStats:
    @pytest.fixture
    def app(self):
        return _create_app()

    def test_returns_stats_json(self, app):
        lib_svc = Mock()
        lib_svc.get_library_stats.return_value = {
            "total_documents": 42,
            "total_size": 1024,
        }

        with _auth_client(app, library_service=lib_svc) as (client, _):
            resp = client.get("/library/api/stats")
            assert resp.status_code == 200
            data = resp.get_json()
            assert data["total_documents"] == 42


# ---------------------------------------------------------------------------
# API: /api/collections/list
# ---------------------------------------------------------------------------


class TestGetCollectionsList:
    @pytest.fixture
    def app(self):
        return _create_app()

    def test_returns_collections(self, app):
        mock_col = Mock()
        mock_col.id = "c1"
        mock_col.name = "My Collection"
        mock_col.description = "Desc"

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

        with _auth_client(app, mock_db_session=db_session) as (client, _):
            resp = client.get("/library/api/collections/list")
            assert resp.status_code == 200
            data = resp.get_json()
            assert data["success"] is True
            assert len(data["collections"]) == 1
            assert data["collections"][0]["name"] == "My Collection"

    def test_returns_empty_list(self, app):
        query = _build_mock_query(all_result=[])
        db_session = Mock()
        db_session.query = Mock(return_value=query)

        with _auth_client(app, mock_db_session=db_session) as (client, _):
            resp = client.get("/library/api/collections/list")
            data = resp.get_json()
            assert data["success"] is True
            assert data["collections"] == []


# ---------------------------------------------------------------------------
# API: /api/documents
# ---------------------------------------------------------------------------


class TestGetDocuments:
    @pytest.fixture
    def app(self):
        return _create_app()

    def test_returns_documents_with_defaults(self, app):
        lib_svc = Mock()
        lib_svc.get_documents.return_value = [{"id": "d1"}]

        with _auth_client(app, library_service=lib_svc) as (client, _):
            resp = client.get("/library/api/documents")
            assert resp.status_code == 200
            data = resp.get_json()
            assert len(data["documents"]) == 1

    def test_passes_all_filter_params(self, app):
        lib_svc = Mock()
        lib_svc.get_documents.return_value = []

        with _auth_client(app, library_service=lib_svc) as (client, _):
            resp = client.get(
                "/library/api/documents?research_id=r1&domain=arxiv.org"
                "&file_type=pdf&favorites=true&search=quantum&limit=50&offset=10"
            )
            assert resp.status_code == 200
            lib_svc.get_documents.assert_called_once_with(
                research_id="r1",
                domain="arxiv.org",
                file_type="pdf",
                favorites_only=True,
                search_query="quantum",
                limit=50,
                offset=10,
            )

    def test_favorites_false_by_default(self, app):
        lib_svc = Mock()
        lib_svc.get_documents.return_value = []

        with _auth_client(app, library_service=lib_svc) as (client, _):
            resp = client.get("/library/api/documents")
            assert resp.status_code == 200
            call_kw = lib_svc.get_documents.call_args[1]
            assert call_kw["favorites_only"] is False


# ---------------------------------------------------------------------------
# API: toggle favorite
# ---------------------------------------------------------------------------


class TestToggleFavorite:
    @pytest.fixture
    def app(self):
        return _create_app()

    def test_returns_favorite_state(self, app):
        lib_svc = Mock()
        lib_svc.toggle_favorite.return_value = True

        with _auth_client(app, library_service=lib_svc) as (client, _):
            resp = client.post("/library/api/document/d1/favorite")
            assert resp.status_code == 200
            assert resp.get_json()["favorite"] is True

    def test_unfavorite(self, app):
        lib_svc = Mock()
        lib_svc.toggle_favorite.return_value = False

        with _auth_client(app, library_service=lib_svc) as (client, _):
            resp = client.post("/library/api/document/d1/favorite")
            assert resp.get_json()["favorite"] is False


# ---------------------------------------------------------------------------
# API: delete document
# ---------------------------------------------------------------------------


class TestDeleteDocument:
    @pytest.fixture
    def app(self):
        return _create_app()

    def test_successful_deletion(self, app):
        lib_svc = Mock()
        lib_svc.delete_document.return_value = True

        with _auth_client(app, library_service=lib_svc) as (client, _):
            resp = client.delete("/library/api/document/d1")
            assert resp.status_code == 200
            assert resp.get_json()["success"] is True

    def test_deletion_failure(self, app):
        lib_svc = Mock()
        lib_svc.delete_document.return_value = False

        with _auth_client(app, library_service=lib_svc) as (client, _):
            resp = client.delete("/library/api/document/d1")
            assert resp.status_code == 200
            assert resp.get_json()["success"] is False


# ---------------------------------------------------------------------------
# API: get PDF URL
# ---------------------------------------------------------------------------


class TestGetPdfUrl:
    @pytest.fixture
    def app(self):
        return _create_app()

    def test_returns_url(self, app):
        with _auth_client(app) as (client, _):
            resp = client.get("/library/api/document/abc123/pdf-url")
            assert resp.status_code == 200
            data = resp.get_json()
            assert data["url"] == "/library/api/document/abc123/pdf"
            assert data["title"] == "Document"


# ---------------------------------------------------------------------------
# View PDF page
# ---------------------------------------------------------------------------


class TestViewPdfPage:
    @pytest.fixture
    def app(self):
        return _create_app()

    def test_serves_pdf_bytes(self, app):
        mock_doc = Mock()
        mock_doc.title = "Test"
        mock_doc.file_path = "pdfs/test.pdf"
        mock_doc.filename = "test.pdf"

        query = _build_mock_query(first_result=mock_doc)
        db_session = Mock()
        db_session.query = Mock(return_value=query)

        pdf_bytes = b"%PDF-1.4 fake content"

        with _auth_client(app, mock_db_session=db_session) as (client, _):
            with patch(f"{_ROUTES}.PDFStorageManager") as mock_pdf_mgr_cls:
                mock_pdf_mgr = Mock()
                mock_pdf_mgr.load_pdf.return_value = pdf_bytes
                mock_pdf_mgr_cls.return_value = mock_pdf_mgr

                with patch(
                    f"{_ROUTES}.get_library_directory", return_value="/tmp/lib"
                ):
                    resp = client.get("/library/document/d1/pdf")
                    assert resp.status_code == 200
                    assert resp.content_type == "application/pdf"
                    assert resp.data == pdf_bytes

    def test_document_not_found(self, app):
        query = _build_mock_query(first_result=None)
        db_session = Mock()
        db_session.query = Mock(return_value=query)

        with _auth_client(app, mock_db_session=db_session) as (client, _):
            resp = client.get("/library/document/nonexistent/pdf")
            assert resp.status_code == 404

    def test_pdf_not_available(self, app):
        mock_doc = Mock()
        mock_doc.title = "Test"
        mock_doc.file_path = "pdfs/test.pdf"
        mock_doc.filename = "test.pdf"

        query = _build_mock_query(first_result=mock_doc)
        db_session = Mock()
        db_session.query = Mock(return_value=query)

        with _auth_client(app, mock_db_session=db_session) as (client, _):
            with patch(f"{_ROUTES}.PDFStorageManager") as mock_pdf_mgr_cls:
                mock_pdf_mgr = Mock()
                mock_pdf_mgr.load_pdf.return_value = None
                mock_pdf_mgr_cls.return_value = mock_pdf_mgr

                with patch(
                    f"{_ROUTES}.get_library_directory", return_value="/tmp/lib"
                ):
                    resp = client.get("/library/document/d1/pdf")
                    assert resp.status_code == 404


# ---------------------------------------------------------------------------
# View text page
# ---------------------------------------------------------------------------


class TestViewTextPage:
    @pytest.fixture
    def app(self):
        return _create_app()

    def test_renders_text_content(self, app):
        mock_doc = Mock()
        mock_doc.title = "Test Paper"
        mock_doc.text_content = "Full text content here."
        mock_doc.extraction_method = "pdftotext"
        mock_doc.word_count = 4

        query = _build_mock_query(first_result=mock_doc)
        db_session = Mock()
        db_session.query = Mock(return_value=query)

        with _auth_client(app, mock_db_session=db_session) as (client, mocks):
            resp = client.get("/library/document/d1/txt")
            assert resp.status_code == 200
            call_kw = mocks["render"].call_args[1]
            assert call_kw["title"] == "Test Paper"
            assert call_kw["text_content"] == "Full text content here."

    def test_document_not_found(self, app):
        query = _build_mock_query(first_result=None)
        db_session = Mock()
        db_session.query = Mock(return_value=query)

        with _auth_client(app, mock_db_session=db_session) as (client, _):
            resp = client.get("/library/document/missing/txt")
            assert resp.status_code == 404

    def test_no_text_content(self, app):
        mock_doc = Mock()
        mock_doc.text_content = None

        query = _build_mock_query(first_result=mock_doc)
        db_session = Mock()
        db_session.query = Mock(return_value=query)

        with _auth_client(app, mock_db_session=db_session) as (client, _):
            resp = client.get("/library/document/d1/txt")
            assert resp.status_code == 404

    def test_empty_text_content(self, app):
        mock_doc = Mock()
        mock_doc.text_content = ""

        query = _build_mock_query(first_result=mock_doc)
        db_session = Mock()
        db_session.query = Mock(return_value=query)

        with _auth_client(app, mock_db_session=db_session) as (client, _):
            resp = client.get("/library/document/d1/txt")
            # Empty string is falsy, so should return 404
            assert resp.status_code == 404

    def test_title_defaults_when_none(self, app):
        mock_doc = Mock()
        mock_doc.title = None
        mock_doc.text_content = "Some content"
        mock_doc.extraction_method = None
        mock_doc.word_count = 2

        query = _build_mock_query(first_result=mock_doc)
        db_session = Mock()
        db_session.query = Mock(return_value=query)

        with _auth_client(app, mock_db_session=db_session) as (client, mocks):
            resp = client.get("/library/document/d1/txt")
            assert resp.status_code == 200
            call_kw = mocks["render"].call_args[1]
            assert call_kw["title"] == "Document Text"


# ---------------------------------------------------------------------------
# API: serve text
# ---------------------------------------------------------------------------


class TestServeTextApi:
    @pytest.fixture
    def app(self):
        return _create_app()

    def test_returns_text_json(self, app):
        mock_doc = Mock()
        mock_doc.title = "Paper"
        mock_doc.text_content = "Hello world"
        mock_doc.extraction_method = "pdfminer"
        mock_doc.word_count = 2

        query = _build_mock_query(first_result=mock_doc)
        db_session = Mock()
        db_session.query = Mock(return_value=query)

        with _auth_client(app, mock_db_session=db_session) as (client, _):
            resp = client.get("/library/api/document/d1/text")
            assert resp.status_code == 200
            data = resp.get_json()
            assert data["text_content"] == "Hello world"
            assert data["title"] == "Paper"
            assert data["extraction_method"] == "pdfminer"
            assert data["word_count"] == 2

    def test_document_not_found(self, app):
        query = _build_mock_query(first_result=None)
        db_session = Mock()
        db_session.query = Mock(return_value=query)

        with _auth_client(app, mock_db_session=db_session) as (client, _):
            resp = client.get("/library/api/document/nope/text")
            assert resp.status_code == 404
            assert "not found" in resp.get_json()["error"].lower()

    def test_no_text_content(self, app):
        mock_doc = Mock()
        mock_doc.text_content = None

        query = _build_mock_query(first_result=mock_doc)
        db_session = Mock()
        db_session.query = Mock(return_value=query)

        with _auth_client(app, mock_db_session=db_session) as (client, _):
            resp = client.get("/library/api/document/d1/text")
            assert resp.status_code == 404
            assert "not available" in resp.get_json()["error"].lower()

    def test_title_defaults_when_none(self, app):
        mock_doc = Mock()
        mock_doc.title = None
        mock_doc.text_content = "content"
        mock_doc.extraction_method = None
        mock_doc.word_count = 1

        query = _build_mock_query(first_result=mock_doc)
        db_session = Mock()
        db_session.query = Mock(return_value=query)

        with _auth_client(app, mock_db_session=db_session) as (client, _):
            resp = client.get("/library/api/document/d1/text")
            assert resp.get_json()["title"] == "Document"


# ---------------------------------------------------------------------------
# API: open folder (disabled)
# ---------------------------------------------------------------------------


class TestOpenFolder:
    @pytest.fixture
    def app(self):
        return _create_app()

    def test_always_returns_403(self, app):
        with _auth_client(app) as (client, _):
            resp = client.post("/library/api/open-folder")
            assert resp.status_code == 403
            data = resp.get_json()
            assert data["status"] == "error"
            assert "disabled" in data["message"].lower()


# ---------------------------------------------------------------------------
# API: download single resource
# ---------------------------------------------------------------------------


class TestDownloadSingleResource:
    @pytest.fixture
    def app(self):
        return _create_app()

    def test_success(self, app):
        dl_svc = Mock()
        dl_svc.__enter__ = Mock(return_value=dl_svc)
        dl_svc.__exit__ = Mock(return_value=False)
        dl_svc.download_resource.return_value = (True, None)

        with _auth_client(app, download_service=dl_svc) as (client, _):
            resp = client.post("/library/api/download/42")
            assert resp.status_code == 200
            assert resp.get_json()["success"] is True

    def test_failure(self, app):
        dl_svc = Mock()
        dl_svc.__enter__ = Mock(return_value=dl_svc)
        dl_svc.__exit__ = Mock(return_value=False)
        dl_svc.download_resource.return_value = (False, "Network error")

        with _auth_client(app, download_service=dl_svc) as (client, _):
            resp = client.post("/library/api/download/42")
            assert resp.status_code == 500
            data = resp.get_json()
            assert data["success"] is False
            # Error message should be generic, not exposing internal details
            assert "Network error" not in data.get("error", "")


# ---------------------------------------------------------------------------
# API: download text single
# ---------------------------------------------------------------------------


class TestDownloadTextSingle:
    @pytest.fixture
    def app(self):
        return _create_app()

    def test_success(self, app):
        dl_svc = Mock()
        dl_svc.__enter__ = Mock(return_value=dl_svc)
        dl_svc.__exit__ = Mock(return_value=False)
        dl_svc.download_as_text.return_value = (True, None)

        with _auth_client(app, download_service=dl_svc) as (client, _):
            resp = client.post("/library/api/download-text/7")
            assert resp.status_code == 200
            data = resp.get_json()
            assert data["success"] is True
            assert data["error"] is None

    def test_failure(self, app):
        dl_svc = Mock()
        dl_svc.__enter__ = Mock(return_value=dl_svc)
        dl_svc.__exit__ = Mock(return_value=False)
        dl_svc.download_as_text.return_value = (False, "Extraction failed")

        with _auth_client(app, download_service=dl_svc) as (client, _):
            resp = client.post("/library/api/download-text/7")
            assert resp.status_code == 200
            data = resp.get_json()
            assert data["success"] is False
            # Error message should be generic
            assert data["error"] == "Failed to download resource"

    def test_exception_calls_handle_api_error(self, app):
        dl_svc = Mock()
        dl_svc.__enter__ = Mock(return_value=dl_svc)
        dl_svc.__exit__ = Mock(return_value=False)
        dl_svc.download_as_text.side_effect = RuntimeError("unexpected")

        with _auth_client(app, download_service=dl_svc) as (client, _):
            with patch(f"{_ROUTES}.handle_api_error") as mock_handle:
                with app.app_context():
                    from flask import make_response

                    error_resp = make_response(
                        json.dumps({"error": "An internal error occurred"}), 500
                    )
                    error_resp.content_type = "application/json"
                mock_handle.return_value = error_resp
                resp = client.post("/library/api/download-text/7")
                assert resp.status_code == 500
                mock_handle.assert_called_once()


# ---------------------------------------------------------------------------
# API: download research PDFs
# ---------------------------------------------------------------------------


class TestDownloadResearchPdfs:
    @pytest.fixture
    def app(self):
        return _create_app()

    def test_queues_downloads(self, app):
        dl_svc = Mock()
        dl_svc.__enter__ = Mock(return_value=dl_svc)
        dl_svc.__exit__ = Mock(return_value=False)
        dl_svc.queue_research_downloads.return_value = 5

        with _auth_client(app, download_service=dl_svc) as (client, _):
            resp = client.post(
                "/library/api/download-research/r1",
                json={},
                content_type="application/json",
            )
            assert resp.status_code == 200
            data = resp.get_json()
            assert data["success"] is True
            assert data["queued"] == 5

    def test_with_collection_id(self, app):
        dl_svc = Mock()
        dl_svc.__enter__ = Mock(return_value=dl_svc)
        dl_svc.__exit__ = Mock(return_value=False)
        dl_svc.queue_research_downloads.return_value = 2

        with _auth_client(app, download_service=dl_svc) as (client, _):
            resp = client.post(
                "/library/api/download-research/r1",
                json={"collection_id": "c1"},
                content_type="application/json",
            )
            assert resp.status_code == 200
            dl_svc.queue_research_downloads.assert_called_once_with("r1", "c1")

    def test_no_collection_id(self, app):
        dl_svc = Mock()
        dl_svc.__enter__ = Mock(return_value=dl_svc)
        dl_svc.__exit__ = Mock(return_value=False)
        dl_svc.queue_research_downloads.return_value = 0

        with _auth_client(app, download_service=dl_svc) as (client, _):
            resp = client.post(
                "/library/api/download-research/r1",
                json={},
                content_type="application/json",
            )
            assert resp.status_code == 200
            dl_svc.queue_research_downloads.assert_called_once_with("r1", None)


# ---------------------------------------------------------------------------
# API: download bulk
# ---------------------------------------------------------------------------


class TestDownloadBulk:
    @pytest.fixture
    def app(self):
        return _create_app()

    def test_no_json_body_returns_400(self, app):
        with _auth_client(app) as (client, _):
            resp = client.post(
                "/library/api/download-bulk",
                data="not json",
                content_type="text/plain",
            )
            assert resp.status_code == 400

    def test_empty_research_ids_returns_400(self, app):
        with _auth_client(app) as (client, _):
            resp = client.post(
                "/library/api/download-bulk",
                json={"research_ids": []},
                content_type="application/json",
            )
            assert resp.status_code == 400
            assert "No research IDs" in resp.get_json()["error"]

    def test_missing_research_ids_returns_400(self, app):
        with _auth_client(app) as (client, _):
            resp = client.post(
                "/library/api/download-bulk",
                json={"mode": "pdf"},
                content_type="application/json",
            )
            assert resp.status_code == 400

    def test_valid_request_returns_sse_stream(self, app):
        """Bulk download with valid IDs returns SSE stream."""
        query = _build_mock_query(all_result=[], count_result=0)
        db_session = Mock()
        db_session.query = Mock(return_value=query)
        db_session.commit = Mock()

        dl_svc = Mock()
        dl_svc.__enter__ = Mock(return_value=dl_svc)
        dl_svc.__exit__ = Mock(return_value=False)
        dl_svc.close = Mock()
        dl_svc.queue_research_downloads = Mock(return_value=0)

        with _auth_client(
            app, download_service=dl_svc, mock_db_session=db_session
        ) as (
            client,
            _,
        ):
            resp = client.post(
                "/library/api/download-bulk",
                json={"research_ids": ["r1"], "mode": "pdf"},
                content_type="application/json",
            )
            assert resp.status_code == 200
            assert "text/event-stream" in resp.content_type
            # Consume the stream fully to avoid context leaks
            _ = resp.data
            resp.close()


# ---------------------------------------------------------------------------
# API: research list
# ---------------------------------------------------------------------------


class TestGetResearchList:
    @pytest.fixture
    def app(self):
        return _create_app()

    def test_returns_research_list(self, app):
        lib_svc = Mock()
        lib_svc.get_research_list_with_stats.return_value = [
            {"id": "r1", "query": "quantum computing"}
        ]

        with _auth_client(app, library_service=lib_svc) as (client, _):
            resp = client.get("/library/api/research-list")
            assert resp.status_code == 200
            data = resp.get_json()
            assert len(data["research"]) == 1
            assert data["research"][0]["id"] == "r1"


# ---------------------------------------------------------------------------
# API: sync library
# ---------------------------------------------------------------------------


class TestSyncLibrary:
    @pytest.fixture
    def app(self):
        return _create_app()

    def test_returns_sync_stats(self, app):
        lib_svc = Mock()
        lib_svc.sync_library_with_filesystem.return_value = {
            "added": 2,
            "removed": 1,
        }

        with _auth_client(app, library_service=lib_svc) as (client, _):
            resp = client.post("/library/api/sync-library")
            assert resp.status_code == 200
            data = resp.get_json()
            assert data["added"] == 2
            assert data["removed"] == 1


# ---------------------------------------------------------------------------
# API: mark for redownload
# ---------------------------------------------------------------------------


class TestMarkForRedownload:
    @pytest.fixture
    def app(self):
        return _create_app()

    def test_no_json_body_returns_400(self, app):
        with _auth_client(app) as (client, _):
            resp = client.post(
                "/library/api/mark-redownload",
                data="not json",
                content_type="text/plain",
            )
            assert resp.status_code == 400

    def test_empty_document_ids_returns_400(self, app):
        with _auth_client(app) as (client, _):
            resp = client.post(
                "/library/api/mark-redownload",
                json={"document_ids": []},
                content_type="application/json",
            )
            assert resp.status_code == 400

    def test_marks_documents(self, app):
        lib_svc = Mock()
        lib_svc.mark_for_redownload.return_value = 3

        with _auth_client(app, library_service=lib_svc) as (client, _):
            resp = client.post(
                "/library/api/mark-redownload",
                json={"document_ids": ["d1", "d2", "d3"]},
                content_type="application/json",
            )
            assert resp.status_code == 200
            data = resp.get_json()
            assert data["success"] is True
            assert data["marked"] == 3


# ---------------------------------------------------------------------------
# API: get research sources
# ---------------------------------------------------------------------------


class TestGetResearchSources:
    @pytest.fixture
    def app(self):
        return _create_app()

    def test_returns_sources_with_metadata(self, app):
        mock_resource = Mock()
        mock_resource.id = 1
        mock_resource.url = "https://arxiv.org/abs/2301.00001"
        mock_resource.title = "Test Paper"
        mock_resource.content_preview = "Preview text"
        mock_resource.relevance_score = 0.95
        mock_resource.created_at = None

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

        with _auth_client(app, mock_db_session=db_session) as (client, _):
            with patch(
                f"{_ROUTES}.get_document_for_resource", return_value=None
            ):
                resp = client.get("/library/api/get-research-sources/r1")
                assert resp.status_code == 200
                data = resp.get_json()
                assert data["success"] is True
                assert data["total"] == 1
                src = data["sources"][0]
                assert src["resource_id"] == 1
                assert src["title"] == "Test Paper"
                assert src["domain"] == "arxiv.org"
                assert src["downloaded"] is False

    def test_source_with_completed_document(self, app):
        mock_resource = Mock()
        mock_resource.id = 1
        mock_resource.url = "https://arxiv.org/abs/2301.00001"
        mock_resource.title = "Paper"
        mock_resource.content_preview = ""
        mock_resource.relevance_score = None
        mock_resource.created_at = None

        mock_doc = Mock()
        mock_doc.id = "doc-1"
        mock_doc.status = "completed"
        mock_doc.file_type = "pdf"
        mock_doc.created_at = None

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

        with _auth_client(app, mock_db_session=db_session) as (client, _):
            with patch(
                f"{_ROUTES}.get_document_for_resource", return_value=mock_doc
            ):
                resp = client.get("/library/api/get-research-sources/r1")
                data = resp.get_json()
                src = data["sources"][0]
                assert src["downloaded"] is True
                assert src["document_id"] == "doc-1"
                assert src["file_type"] == "pdf"

    def test_empty_research(self, app):
        query = _build_mock_query(all_result=[])
        db_session = Mock()
        db_session.query = Mock(return_value=query)

        with _auth_client(app, mock_db_session=db_session) as (client, _):
            resp = client.get("/library/api/get-research-sources/empty")
            data = resp.get_json()
            assert data["total"] == 0
            assert data["sources"] == []

    def test_resource_with_no_url(self, app):
        mock_resource = Mock()
        mock_resource.id = 1
        mock_resource.url = None
        mock_resource.title = None
        mock_resource.content_preview = None
        mock_resource.relevance_score = None
        mock_resource.created_at = None

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

        with _auth_client(app, mock_db_session=db_session) as (client, _):
            with patch(
                f"{_ROUTES}.get_document_for_resource", return_value=None
            ):
                resp = client.get("/library/api/get-research-sources/r1")
                data = resp.get_json()
                src = data["sources"][0]
                assert src["domain"] == ""
                assert src["title"] == "Source 1"  # Default title


# ---------------------------------------------------------------------------
# API: check downloads
# ---------------------------------------------------------------------------


class TestCheckDownloads:
    @pytest.fixture
    def app(self):
        return _create_app()

    def test_no_json_body_returns_400(self, app):
        with _auth_client(app) as (client, _):
            resp = client.post(
                "/library/api/check-downloads",
                data="not json",
                content_type="text/plain",
            )
            assert resp.status_code == 400

    def test_missing_research_id_returns_400(self, app):
        with _auth_client(app) as (client, _):
            resp = client.post(
                "/library/api/check-downloads",
                json={"urls": ["https://arxiv.org/abs/1"]},
                content_type="application/json",
            )
            assert resp.status_code == 400

    def test_missing_urls_returns_400(self, app):
        with _auth_client(app) as (client, _):
            resp = client.post(
                "/library/api/check-downloads",
                json={"research_id": "r1"},
                content_type="application/json",
            )
            assert resp.status_code == 400

    def test_returns_download_status(self, app):
        mock_resource = Mock()
        mock_resource.id = 1
        mock_resource.url = "https://arxiv.org/abs/2301.00001"
        mock_resource.title = "Paper"

        mock_doc = Mock()
        mock_doc.id = "doc-1"
        mock_doc.status = "completed"
        mock_doc.file_path = "pdfs/test.pdf"
        mock_doc.file_type = "pdf"
        mock_doc.title = "Paper"

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

        with _auth_client(app, mock_db_session=db_session) as (client, _):
            with patch(
                f"{_ROUTES}.get_document_for_resource", return_value=mock_doc
            ):
                resp = client.post(
                    "/library/api/check-downloads",
                    json={
                        "research_id": "r1",
                        "urls": ["https://arxiv.org/abs/2301.00001"],
                    },
                    content_type="application/json",
                )
                assert resp.status_code == 200
                data = resp.get_json()
                status = data["download_status"]
                key = "https://arxiv.org/abs/2301.00001"
                assert status[key]["downloaded"] is True
                assert status[key]["document_id"] == "doc-1"

    def test_not_downloaded_resource(self, app):
        mock_resource = Mock()
        mock_resource.id = 1
        mock_resource.url = "https://arxiv.org/abs/2301.00001"

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

        with _auth_client(app, mock_db_session=db_session) as (client, _):
            with patch(
                f"{_ROUTES}.get_document_for_resource", return_value=None
            ):
                resp = client.post(
                    "/library/api/check-downloads",
                    json={
                        "research_id": "r1",
                        "urls": ["https://arxiv.org/abs/2301.00001"],
                    },
                    content_type="application/json",
                )
                data = resp.get_json()
                status = data["download_status"]
                key = "https://arxiv.org/abs/2301.00001"
                assert status[key]["downloaded"] is False
                assert status[key]["resource_id"] == 1


# ---------------------------------------------------------------------------
# API: download source
# ---------------------------------------------------------------------------


class TestDownloadSource:
    @pytest.fixture
    def app(self):
        return _create_app()

    def test_no_json_body_returns_400(self, app):
        with _auth_client(app) as (client, _):
            resp = client.post(
                "/library/api/download-source",
                data="not json",
                content_type="text/plain",
            )
            assert resp.status_code == 400

    def test_missing_research_id_returns_400(self, app):
        with _auth_client(app) as (client, _):
            resp = client.post(
                "/library/api/download-source",
                json={"url": "https://arxiv.org/abs/1"},
                content_type="application/json",
            )
            assert resp.status_code == 400

    def test_missing_url_returns_400(self, app):
        with _auth_client(app) as (client, _):
            resp = client.post(
                "/library/api/download-source",
                json={"research_id": "r1"},
                content_type="application/json",
            )
            assert resp.status_code == 400

    def test_non_downloadable_url_returns_400(self, app):
        with _auth_client(app) as (client, _):
            with patch(f"{_ROUTES}.is_downloadable_domain", return_value=False):
                resp = client.post(
                    "/library/api/download-source",
                    json={
                        "research_id": "r1",
                        "url": "https://google.com/search?q=test",
                    },
                    content_type="application/json",
                )
                assert resp.status_code == 400
                assert (
                    "not from a downloadable domain" in resp.get_json()["error"]
                )

    def test_resource_not_found(self, app):
        query = _build_mock_query(first_result=None)
        db_session = Mock()
        db_session.query = Mock(return_value=query)

        with _auth_client(app, mock_db_session=db_session) as (client, _):
            with patch(f"{_ROUTES}.is_downloadable_domain", return_value=True):
                resp = client.post(
                    "/library/api/download-source",
                    json={
                        "research_id": "r1",
                        "url": "https://arxiv.org/abs/2301.00001",
                    },
                    content_type="application/json",
                )
                assert resp.status_code == 404

    def test_already_downloaded(self, app):
        mock_resource = Mock()
        mock_resource.id = 1
        mock_resource.research_id = "r1"

        mock_doc = Mock()
        mock_doc.id = "doc-1"
        mock_doc.status = "completed"

        query = _build_mock_query(first_result=mock_resource)
        db_session = Mock()
        db_session.query = Mock(return_value=query)

        with _auth_client(app, mock_db_session=db_session) as (client, _):
            with patch(f"{_ROUTES}.is_downloadable_domain", return_value=True):
                with patch(
                    f"{_ROUTES}.get_document_for_resource",
                    return_value=mock_doc,
                ):
                    resp = client.post(
                        "/library/api/download-source",
                        json={
                            "research_id": "r1",
                            "url": "https://arxiv.org/abs/2301.00001",
                        },
                        content_type="application/json",
                    )
                    assert resp.status_code == 200
                    data = resp.get_json()
                    assert data["success"] is True
                    assert data["message"] == "Already downloaded"
                    assert data["document_id"] == "doc-1"

    def test_successful_download(self, app):
        mock_resource = Mock()
        mock_resource.id = 1
        mock_resource.research_id = "r1"

        query = _build_mock_query(first_result=mock_resource)
        db_session = Mock()
        db_session.query = Mock(return_value=query)
        db_session.commit = Mock()
        db_session.add = Mock()

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

        with _auth_client(
            app, download_service=dl_svc, mock_db_session=db_session
        ) as (client, _):
            with patch(f"{_ROUTES}.is_downloadable_domain", return_value=True):
                with patch(
                    f"{_ROUTES}.get_document_for_resource", return_value=None
                ):
                    resp = client.post(
                        "/library/api/download-source",
                        json={
                            "research_id": "r1",
                            "url": "https://arxiv.org/abs/2301.00001",
                        },
                        content_type="application/json",
                    )
                    assert resp.status_code == 200
                    assert resp.get_json()["success"] is True
                    assert resp.get_json()["message"] == "Download completed"

    def test_download_failure(self, app):
        mock_resource = Mock()
        mock_resource.id = 1
        mock_resource.research_id = "r1"

        query = _build_mock_query(first_result=mock_resource)
        db_session = Mock()
        db_session.query = Mock(return_value=query)
        db_session.commit = Mock()
        db_session.add = Mock()

        dl_svc = Mock()
        dl_svc.__enter__ = Mock(return_value=dl_svc)
        dl_svc.__exit__ = Mock(return_value=False)
        dl_svc.download_resource.return_value = (False, "Paywall detected")

        with _auth_client(
            app, download_service=dl_svc, mock_db_session=db_session
        ) as (client, _):
            with patch(f"{_ROUTES}.is_downloadable_domain", return_value=True):
                with patch(
                    f"{_ROUTES}.get_document_for_resource", return_value=None
                ):
                    resp = client.post(
                        "/library/api/download-source",
                        json={
                            "research_id": "r1",
                            "url": "https://arxiv.org/abs/2301.00001",
                        },
                        content_type="application/json",
                    )
                    data = resp.get_json()
                    assert data["success"] is False
                    # Should be generic, not exposing internal "Paywall detected"
                    assert data["message"] == "Download failed"


# ---------------------------------------------------------------------------
# API: download all text (SSE stream)
# ---------------------------------------------------------------------------


class TestDownloadAllText:
    @pytest.fixture
    def app(self):
        return _create_app()

    def test_returns_sse_stream(self, app):
        query = _build_mock_query(all_result=[])
        db_session = Mock()
        db_session.query = Mock(return_value=query)

        dl_svc = Mock()
        dl_svc.__enter__ = Mock(return_value=dl_svc)
        dl_svc.__exit__ = Mock(return_value=False)
        dl_svc.library_root = "/tmp/lib"
        dl_svc.close = Mock()

        with _auth_client(app, mock_db_session=db_session) as (client, _):
            with patch(f"{_ROUTES}.DownloadService", return_value=dl_svc):
                resp = client.post("/library/api/download-all-text")
                assert resp.status_code == 200
                assert "text/event-stream" in resp.content_type
                # Consume stream to avoid context leaks
                _ = resp.data
                resp.close()


# ---------------------------------------------------------------------------
# API: queue all undownloaded
# ---------------------------------------------------------------------------


class TestQueueAllUndownloaded:
    @pytest.fixture
    def app(self):
        return _create_app()

    def test_queues_downloadable_resources(self, app):
        mock_resource = Mock()
        mock_resource.id = 1
        mock_resource.url = "https://arxiv.org/abs/2301.00001"
        mock_resource.research_id = "r1"

        query = _build_mock_query(all_result=[mock_resource], first_result=None)
        db_session = Mock()
        db_session.query = Mock(return_value=query)
        db_session.commit = Mock()
        db_session.add = Mock()

        mock_filter_result = Mock()
        mock_filter_result.resource_id = 1
        mock_filter_result.can_retry = True
        mock_filter_result.reason = ""

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

        with _auth_client(app, mock_db_session=db_session) as (client, _):
            with patch(f"{_ROUTES}.ResourceFilter") as mock_rf_cls:
                mock_rf = Mock()
                mock_rf.filter_downloadable_resources.return_value = [
                    mock_filter_result
                ]
                mock_rf.get_filter_summary.return_value = mock_filter_summary
                mock_rf.get_skipped_resources_info.return_value = []
                mock_rf_cls.return_value = mock_rf

                with patch(
                    f"{_ROUTES}.is_downloadable_domain", return_value=True
                ):
                    resp = client.post("/library/api/queue-all-undownloaded")
                    assert resp.status_code == 200
                    data = resp.get_json()
                    assert data["success"] is True
                    assert data["queued"] >= 1


# ---------------------------------------------------------------------------
# Error handler
# ---------------------------------------------------------------------------


class TestWebApiExceptionHandler:
    """Test the blueprint-level error handler."""

    @pytest.fixture
    def app(self):
        return _create_app()

    def test_handles_web_api_exception(self, app):
        from local_deep_research.web.exceptions import WebAPIException

        with _auth_client(app) as (client, mocks):
            # Patch a route to raise WebAPIException
            with patch(
                f"{_ROUTES}.LibraryService",
                side_effect=WebAPIException("test error", status_code=422),
            ):
                resp = client.get("/library/api/stats")
                assert resp.status_code == 422
                data = resp.get_json()
                assert data["status"] == "error"

    def test_non_web_api_exception_reraises(self, app):
        """Non-WebAPIException errors are re-raised by the blueprint handler."""
        with _auth_client(app) as (client, mocks):
            lib_svc = Mock()
            lib_svc.get_library_stats.side_effect = ValueError("bad value")
            with patch(f"{_ROUTES}.LibraryService", return_value=lib_svc):
                # The blueprint handler re-raises non-WebAPIException errors.
                # With TESTING=True Flask propagates the exception.
                with pytest.raises(ValueError, match="bad value"):
                    client.get("/library/api/stats")


# ---------------------------------------------------------------------------
# Edge cases: serve_pdf_api delegates to view_pdf_page
# ---------------------------------------------------------------------------


class TestServePdfApiDelegation:
    @pytest.fixture
    def app(self):
        return _create_app()

    def test_api_pdf_endpoint_delegates(self, app):
        """The /api/document/<id>/pdf endpoint delegates to view_pdf_page."""
        mock_doc = Mock()
        mock_doc.title = "Test"
        mock_doc.file_path = "pdfs/test.pdf"
        mock_doc.filename = "test.pdf"

        query = _build_mock_query(first_result=mock_doc)
        db_session = Mock()
        db_session.query = Mock(return_value=query)

        pdf_bytes = b"%PDF-1.4 content"

        with _auth_client(app, mock_db_session=db_session) as (client, _):
            with patch(f"{_ROUTES}.PDFStorageManager") as mock_cls:
                mock_mgr = Mock()
                mock_mgr.load_pdf.return_value = pdf_bytes
                mock_cls.return_value = mock_mgr

                with patch(
                    f"{_ROUTES}.get_library_directory", return_value="/tmp/lib"
                ):
                    resp = client.get("/library/api/document/d1/pdf")
                    assert resp.status_code == 200
                    assert resp.data == pdf_bytes
