"""
Comprehensive tests for research_library/routes/library_routes.py

Tests cover:
- is_downloadable_domain function
- get_authenticated_user_password function
- API routes
- Resource ID extraction from filenames (N+1 fix)
- Filter results dict conversion (O(n²) → O(n) fix)
"""

import pytest
from contextlib import contextmanager
from unittest.mock import Mock, patch


def _create_test_app():
    """Create a Flask test app with auth_bp and library_bp registered,
    and db_manager mocked so login_required passes.

    A catch-all error handler is added so that unhandled exceptions
    (e.g. missing database) return a 500 JSON response instead of
    propagating to the test runner.
    """
    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,
    )

    app = Flask(__name__)
    app.config["SECRET_KEY"] = "test-secret"
    app.config["WTF_CSRF_ENABLED"] = False
    app.register_blueprint(auth_bp)
    app.register_blueprint(library_bp)

    # Register a 500-code handler on the *app* level.  Flask searches
    # code-specific handlers before class-based ones, so this will be
    # found *before* the blueprint's generic ``Exception`` handler and
    # will prevent that handler from re-raising the wrapped
    # ``InternalServerError``.
    @app.errorhandler(500)
    def _handle_500(error):  # noqa: ARG001
        return jsonify({"error": "Internal server error"}), 500

    return app


@contextmanager
def _authenticated_client(app):
    """Context manager that provides a test client with a mocked authenticated
    session and db_manager so that ``login_required`` passes.

    Mocks ``LibraryService``, ``DownloadService``, ``get_user_db_session``,
    ``get_settings_manager``, ``get_authenticated_user_password``, and
    ``render_template_with_defaults`` so route handlers never hit the real
    encrypted database (which would raise ``DatabaseSessionError`` in tests).
    """
    from contextlib import contextmanager as _cm

    mock_db = Mock()
    mock_db.connections = {"testuser": True}
    mock_db.has_encryption = False

    # --- Mock LibraryService ---
    mock_library_service = Mock()
    mock_library_service.get_library_stats.return_value = {
        "total_documents": 0,
        "total_collections": 0,
        "total_size": 0,
    }
    mock_library_service.get_documents.return_value = []
    mock_library_service.get_unique_domains.return_value = []
    mock_library_service.get_research_list_with_stats.return_value = []
    mock_library_service.get_all_collections.return_value = []
    mock_library_service.get_document.return_value = None
    mock_library_service.toggle_favorite.return_value = None
    mock_library_service.delete_document.return_value = True
    mock_library_service.sync_library_with_filesystem.return_value = {
        "synced": 0
    }
    mock_library_service.get_paginated_documents.return_value = {
        "documents": [],
        "total": 0,
        "page": 1,
        "per_page": 20,
        "pages": 0,
    }

    mock_library_cls = Mock(return_value=mock_library_service)

    # --- Mock DownloadService as a context manager ---
    mock_download_service = Mock()
    mock_download_service.__enter__ = Mock(return_value=mock_download_service)
    mock_download_service.__exit__ = Mock(return_value=False)
    mock_download_service.download_resource.return_value = {"status": "success"}
    mock_download_service.download_research_pdfs.return_value = {
        "status": "success"
    }
    mock_download_service.download_bulk.return_value = {"status": "success"}
    mock_download_service.check_downloads.return_value = []
    mock_download_service.download_source.return_value = {"status": "success"}

    mock_download_cls = Mock(return_value=mock_download_service)

    # --- Mock get_user_db_session as a context manager ---
    # Build a mock session whose query chains return sensible defaults
    # (empty lists for .all(), 0 for .count(), None for .first()).
    _mock_query = Mock()
    _mock_query.all.return_value = []
    _mock_query.first.return_value = None
    _mock_query.count.return_value = 0
    _mock_query.filter_by.return_value = _mock_query
    _mock_query.filter.return_value = _mock_query
    _mock_query.order_by.return_value = _mock_query
    _mock_query.outerjoin.return_value = _mock_query
    _mock_query.join.return_value = _mock_query
    _mock_query.limit.return_value = _mock_query
    _mock_query.offset.return_value = _mock_query
    _mock_query.delete.return_value = 0

    _mock_db_session = Mock()
    _mock_db_session.query.return_value = _mock_query

    @_cm
    def _fake_get_user_db_session(*args, **kwargs):
        yield _mock_db_session

    # --- Mock get_settings_manager ---
    mock_settings_mgr = Mock()
    mock_settings_mgr.get_setting.return_value = "database"

    # --- Mock render_template_with_defaults for page routes ---
    mock_render = Mock(return_value="<html>mocked</html>")

    _routes_mod = "local_deep_research.research_library.routes.library_routes"

    patches = [
        patch("local_deep_research.web.auth.decorators.db_manager", mock_db),
        patch(f"{_routes_mod}.LibraryService", mock_library_cls),
        patch(f"{_routes_mod}.DownloadService", mock_download_cls),
        patch(
            f"{_routes_mod}.get_user_db_session",
            side_effect=_fake_get_user_db_session,
        ),
        patch(
            f"{_routes_mod}.get_settings_manager",
            return_value=mock_settings_mgr,
        ),
        patch(
            f"{_routes_mod}.render_template_with_defaults",
            mock_render,
        ),
        patch(
            f"{_routes_mod}.get_authenticated_user_password",
            return_value="mock_password",
        ),
    ]

    # Start all 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
    finally:
        for p in patches:
            p.stop()


class TestIsDownloadableDomain:
    """Tests for is_downloadable_domain function."""

    def test_arxiv_url(self):
        """Test arxiv.org is recognized as downloadable."""
        from local_deep_research.research_library.routes.library_routes import (
            is_downloadable_domain,
        )

        assert (
            is_downloadable_domain("https://arxiv.org/abs/2301.00001") is True
        )
        assert (
            is_downloadable_domain("https://www.arxiv.org/pdf/2301.00001.pdf")
            is True
        )

    def test_pubmed_url(self):
        """Test PubMed URLs are recognized as downloadable."""
        from local_deep_research.research_library.routes.library_routes import (
            is_downloadable_domain,
        )

        assert (
            is_downloadable_domain("https://pubmed.ncbi.nlm.nih.gov/12345678")
            is True
        )
        assert (
            is_downloadable_domain(
                "https://ncbi.nlm.nih.gov/pmc/articles/PMC123"
            )
            is True
        )

    def test_biorxiv_url(self):
        """Test bioRxiv URLs are recognized as downloadable."""
        from local_deep_research.research_library.routes.library_routes import (
            is_downloadable_domain,
        )

        assert (
            is_downloadable_domain(
                "https://biorxiv.org/content/10.1101/2021.01.01"
            )
            is True
        )
        assert (
            is_downloadable_domain(
                "https://www.biorxiv.org/content/early/2021/01/01/2021.01.01.123456"
            )
            is True
        )

    def test_direct_pdf_url(self):
        """Test direct PDF URLs are recognized."""
        from local_deep_research.research_library.routes.library_routes import (
            is_downloadable_domain,
        )

        assert is_downloadable_domain("https://example.com/paper.pdf") is True
        assert (
            is_downloadable_domain("https://random.site/download.pdf?token=xyz")
            is True
        )

    def test_doi_url(self):
        """Test DOI URLs are recognized."""
        from local_deep_research.research_library.routes.library_routes import (
            is_downloadable_domain,
        )

        assert is_downloadable_domain("https://doi.org/10.1234/example") is True

    def test_major_publishers(self):
        """Test major publisher domains."""
        from local_deep_research.research_library.routes.library_routes import (
            is_downloadable_domain,
        )

        publisher_urls = [
            "https://nature.com/articles/s41586-021-01234-5",
            "https://www.sciencedirect.com/science/article/pii/S12345678",
            "https://springer.com/article/10.1007/s00123",
            "https://wiley.com/doi/abs/10.1002/example",
            "https://plos.org/article/12345",
            "https://frontiersin.org/articles/10.3389/fimmu.2021.12345",
        ]

        for url in publisher_urls:
            assert is_downloadable_domain(url) is True, (
                f"Expected {url} to be downloadable"
            )

    def test_pdf_in_path(self):
        """Test URLs with /pdf/ in path are recognized."""
        from local_deep_research.research_library.routes.library_routes import (
            is_downloadable_domain,
        )

        assert (
            is_downloadable_domain("https://example.com/pdf/document123")
            is True
        )

    def test_pdf_query_param(self):
        """Test URLs with PDF query parameters are recognized."""
        from local_deep_research.research_library.routes.library_routes import (
            is_downloadable_domain,
        )

        assert (
            is_downloadable_domain("https://example.com/doc?type=pdf") is True
        )
        assert (
            is_downloadable_domain("https://example.com/get?format=pdf") is True
        )

    def test_non_downloadable_url(self):
        """Test non-academic URLs are not recognized."""
        from local_deep_research.research_library.routes.library_routes import (
            is_downloadable_domain,
        )

        assert (
            is_downloadable_domain("https://google.com/search?q=test") is False
        )
        assert is_downloadable_domain("https://twitter.com/user") is False
        assert (
            is_downloadable_domain("https://youtube.com/watch?v=123") is False
        )

    def test_empty_url(self):
        """Test empty URL returns False."""
        from local_deep_research.research_library.routes.library_routes import (
            is_downloadable_domain,
        )

        assert is_downloadable_domain("") is False
        assert is_downloadable_domain(None) is False

    def test_invalid_url(self):
        """Test invalid URLs are handled gracefully."""
        from local_deep_research.research_library.routes.library_routes import (
            is_downloadable_domain,
        )

        # Should not raise, should return False
        result = is_downloadable_domain("not a valid url")
        assert (
            result is False or result is True
        )  # Either is acceptable for malformed URLs


class TestGetAuthenticatedUserPassword:
    """Tests for get_authenticated_user_password function."""

    def test_get_password_from_session_store(self):
        """Test getting password from session store."""
        from local_deep_research.research_library.routes.library_routes import (
            get_authenticated_user_password,
        )

        mock_store = Mock()
        mock_store.get_session_password.return_value = "test_password"

        with patch(
            "local_deep_research.database.session_passwords.session_password_store",
            mock_store,
        ):
            with patch(
                "local_deep_research.research_library.routes.library_routes.session",
                {"session_id": "sess123"},
            ):
                password = get_authenticated_user_password("testuser")

                assert password == "test_password"
                mock_store.get_session_password.assert_called_once_with(
                    "testuser", "sess123"
                )

    def test_get_password_fallback_to_g(self):
        """Test fallback to g.user_password."""
        from flask import Flask

        app = Flask(__name__)

        with app.app_context():
            from local_deep_research.research_library.routes.library_routes import (
                get_authenticated_user_password,
                g,
            )

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

            with patch(
                "local_deep_research.database.session_passwords.session_password_store",
                mock_store,
            ):
                with patch(
                    "local_deep_research.research_library.routes.library_routes.session",
                    {"session_id": "sess123"},
                ):
                    # Set g.user_password directly
                    g.user_password = "fallback_password"

                    password = get_authenticated_user_password("testuser")

                    assert password == "fallback_password"

    def test_no_password_available(self):
        """Test error when no password is available."""
        from flask import Flask
        from local_deep_research.web.exceptions import (
            AuthenticationRequiredError,
        )

        app = Flask(__name__)

        with app.app_context():
            from local_deep_research.research_library.routes.library_routes import (
                get_authenticated_user_password,
                g,
            )

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

            with patch(
                "local_deep_research.database.session_passwords.session_password_store",
                mock_store,
            ):
                with patch(
                    "local_deep_research.research_library.routes.library_routes.session",
                    {"session_id": "sess123"},
                ):
                    # Don't set g.user_password (or set to None)
                    # Delete it if it exists to ensure it's not set
                    if hasattr(g, "user_password"):
                        delattr(g, "user_password")

                    with pytest.raises(AuthenticationRequiredError) as exc_info:
                        get_authenticated_user_password("testuser")

                    assert exc_info.value.status_code == 401

    def test_custom_session_id(self):
        """Test with custom flask_session_id parameter."""
        from local_deep_research.research_library.routes.library_routes import (
            get_authenticated_user_password,
        )

        mock_store = Mock()
        mock_store.get_session_password.return_value = "password123"

        with patch(
            "local_deep_research.database.session_passwords.session_password_store",
            mock_store,
        ):
            with patch(
                "local_deep_research.research_library.routes.library_routes.session",
                {"session_id": "default_sess"},
            ):
                password = get_authenticated_user_password(
                    "testuser", flask_session_id="custom_sess"
                )

                assert password == "password123"
                mock_store.get_session_password.assert_called_once_with(
                    "testuser", "custom_sess"
                )

    def test_exception_handling_in_session_store(self):
        """Test exception handling when session store fails."""
        from flask import Flask

        app = Flask(__name__)

        with app.app_context():
            from local_deep_research.research_library.routes.library_routes import (
                get_authenticated_user_password,
                g,
            )

            mock_store = Mock()
            mock_store.get_session_password.side_effect = Exception(
                "Store error"
            )

            with patch(
                "local_deep_research.database.session_passwords.session_password_store",
                mock_store,
            ):
                with patch(
                    "local_deep_research.research_library.routes.library_routes.session",
                    {"session_id": "sess123"},
                ):
                    g.user_password = "fallback"

                    password = get_authenticated_user_password("testuser")

                    # Should fall back to g.user_password
                    assert password == "fallback"


class TestLibraryBlueprintImport:
    """Tests for blueprint import and registration."""

    def test_blueprint_exists(self):
        """Test that library blueprint exists."""
        from local_deep_research.research_library.routes.library_routes import (
            library_bp,
        )

        assert library_bp is not None
        assert library_bp.name == "library"
        assert library_bp.url_prefix == "/library"


class TestMedRxivDomain:
    """Test medRxiv domain detection."""

    def test_medrxiv_recognized(self):
        """Test medRxiv URLs are recognized."""
        from local_deep_research.research_library.routes.library_routes import (
            is_downloadable_domain,
        )

        assert (
            is_downloadable_domain(
                "https://medrxiv.org/content/10.1101/2021.01.01"
            )
            is True
        )
        assert (
            is_downloadable_domain("https://www.medrxiv.org/content/something")
            is True
        )


class TestSemanticScholarDomain:
    """Test Semantic Scholar domain detection."""

    def test_semantic_scholar_recognized(self):
        """Test Semantic Scholar URLs are recognized."""
        from local_deep_research.research_library.routes.library_routes import (
            is_downloadable_domain,
        )

        assert (
            is_downloadable_domain("https://semanticscholar.org/paper/12345")
            is True
        )
        assert (
            is_downloadable_domain(
                "https://api.semanticscholar.org/paper/12345"
            )
            is True
        )


class TestAcademiaAndResearchGate:
    """Test Academia.edu and ResearchGate domain detection."""

    def test_academia_recognized(self):
        """Test Academia.edu URLs are recognized."""
        from local_deep_research.research_library.routes.library_routes import (
            is_downloadable_domain,
        )

        assert (
            is_downloadable_domain("https://academia.edu/12345/Paper_Title")
            is True
        )
        assert (
            is_downloadable_domain("https://www.academia.edu/attachments/12345")
            is True
        )

    def test_researchgate_recognized(self):
        """Test ResearchGate URLs are recognized."""
        from local_deep_research.research_library.routes.library_routes import (
            is_downloadable_domain,
        )

        assert (
            is_downloadable_domain("https://researchgate.net/publication/12345")
            is True
        )


class TestEuropePMC:
    """Test Europe PMC domain detection."""

    def test_europepmc_recognized(self):
        """Test Europe PMC URLs are recognized."""
        from local_deep_research.research_library.routes.library_routes import (
            is_downloadable_domain,
        )

        assert (
            is_downloadable_domain("https://europepmc.org/article/PMC/12345")
            is True
        )
        assert (
            is_downloadable_domain(
                "https://www.europepmc.org/articles/PMC12345"
            )
            is True
        )


class TestSubdomainHandling:
    """Test handling of subdomains."""

    def test_www_subdomain(self):
        """Test that www subdomains are recognized."""
        from local_deep_research.research_library.routes.library_routes import (
            is_downloadable_domain,
        )

        # www.domain.com should match domain.com
        assert is_downloadable_domain("https://www.arxiv.org/paper") is True
        assert is_downloadable_domain("https://www.nature.com/article") is True

    def test_other_subdomains(self):
        """Test that other subdomains are recognized."""
        from local_deep_research.research_library.routes.library_routes import (
            is_downloadable_domain,
        )

        # Subdomains should still be recognized
        assert (
            is_downloadable_domain("https://papers.arxiv.org/something") is True
        )
        assert (
            is_downloadable_domain("https://export.arxiv.org/abs/12345") is True
        )


class TestHandleWebApiException:
    """Tests for handle_web_api_exception function."""

    def test_web_api_exception_handler(self):
        """Test WebAPIException is handled correctly."""
        from flask import Flask
        from local_deep_research.research_library.routes.library_routes import (
            library_bp,
        )

        app = Flask(__name__)
        app.register_blueprint(library_bp)

        with app.test_request_context():
            from local_deep_research.web.exceptions import (
                WebAPIException,
            )
            from local_deep_research.research_library.routes.library_routes import (
                handle_web_api_exception,
            )

            error = WebAPIException("Test error", status_code=400)
            response = handle_web_api_exception(error)

            assert response[1] == 400
            resp_json = response[0].get_json()
            assert "Test error" in resp_json.get(
                "error", resp_json.get("message", "")
            )


class TestLibraryApiRoutes:
    """Tests for library API routes."""

    def test_get_library_stats_route(self):
        """Test /api/stats endpoint exists."""
        app = _create_test_app()

        with _authenticated_client(app) as client:
            response = client.get("/library/api/stats")
            assert response.status_code in [200, 302, 401, 403, 500]

    def test_get_collections_list_route(self):
        """Test /api/collections/list endpoint exists."""
        app = _create_test_app()

        with _authenticated_client(app) as client:
            response = client.get("/library/api/collections/list")
            assert response.status_code in [200, 302, 401, 403, 500]

    def test_get_documents_route(self):
        """Test /api/documents endpoint exists."""
        app = _create_test_app()

        with _authenticated_client(app) as client:
            response = client.get("/library/api/documents")
            assert response.status_code in [200, 302, 401, 403, 500]

    def test_toggle_favorite_route(self):
        """Test toggle favorite endpoint exists."""
        app = _create_test_app()

        with _authenticated_client(app) as client:
            response = client.post(
                "/library/api/document/test-doc/toggle-favorite"
            )
            assert response.status_code in [200, 302, 401, 403, 404, 500]

    def test_delete_document_route(self):
        """Test delete document endpoint exists."""
        app = _create_test_app()

        with _authenticated_client(app) as client:
            response = client.delete("/library/api/document/test-doc")
            assert response.status_code in [200, 302, 401, 403, 404, 500]


class TestLibraryPageRoutes:
    """Tests for library page routes."""

    def test_library_page_route_exists(self):
        """Test / page route exists."""
        app = _create_test_app()

        with _authenticated_client(app) as client:
            response = client.get("/library/")
            assert response.status_code in [200, 302, 401, 403, 500]

    def test_document_details_page_route_exists(self):
        """Test /document/<id> page route exists."""
        app = _create_test_app()

        with _authenticated_client(app) as client:
            response = client.get("/library/document/test-doc-id")
            assert response.status_code in [200, 302, 401, 403, 404, 500]

    def test_download_manager_page_route_exists(self):
        """Test /download-manager page route exists."""
        app = _create_test_app()

        with _authenticated_client(app) as client:
            response = client.get("/library/download-manager")
            assert response.status_code in [200, 302, 401, 403, 500]


class TestDownloadApiRoutes:
    """Tests for download API routes."""

    def test_download_single_resource_route(self):
        """Test /api/download/<id> endpoint exists."""
        app = _create_test_app()

        with _authenticated_client(app) as client:
            response = client.post("/library/api/download/123")
            assert response.status_code in [200, 302, 401, 403, 404, 500]

    def test_download_research_pdfs_route(self):
        """Test /api/download-research/<id> endpoint exists."""
        app = _create_test_app()

        with _authenticated_client(app) as client:
            response = client.post(
                "/library/api/download-research/research-123"
            )
            assert response.status_code in [200, 302, 401, 403, 404, 500]

    def test_download_bulk_route(self):
        """Test /api/download-bulk endpoint exists."""
        app = _create_test_app()

        with _authenticated_client(app) as client:
            response = client.post(
                "/library/api/download-bulk",
                json={"research_ids": []},
                content_type="application/json",
            )
            assert response.status_code in [200, 302, 400, 401, 403, 500]

    def test_sync_library_route(self):
        """Test /api/sync-library endpoint exists."""
        app = _create_test_app()

        with _authenticated_client(app) as client:
            response = client.post("/library/api/sync-library")
            assert response.status_code in [200, 302, 401, 403, 500]

    def test_mark_for_redownload_route(self):
        """Test /api/mark-redownload endpoint exists."""
        app = _create_test_app()

        with _authenticated_client(app) as client:
            response = client.post(
                "/library/api/mark-redownload",
                json={"document_ids": []},
                content_type="application/json",
            )
            assert response.status_code in [200, 302, 400, 401, 403, 500]


class TestResearchSourcesRoute:
    """Tests for research sources API route."""

    def test_get_research_sources_route(self):
        """Test /api/get-research-sources/<id> endpoint exists."""
        app = _create_test_app()

        with _authenticated_client(app) as client:
            response = client.get(
                "/library/api/get-research-sources/research-123"
            )
            assert response.status_code in [200, 302, 401, 403, 404, 500]


class TestCheckDownloadsRoute:
    """Tests for check downloads API route."""

    def test_check_downloads_route(self):
        """Test /api/check-downloads endpoint exists."""
        app = _create_test_app()

        with _authenticated_client(app) as client:
            response = client.post(
                "/library/api/check-downloads",
                json={"urls": ["https://arxiv.org/abs/2301.00001"]},
                content_type="application/json",
            )
            assert response.status_code in [200, 302, 400, 401, 403, 500]


class TestDownloadSourceRoute:
    """Tests for download source API route."""

    def test_download_source_route(self):
        """Test /api/download-source endpoint exists."""
        app = _create_test_app()

        with _authenticated_client(app) as client:
            response = client.post(
                "/library/api/download-source",
                json={"url": "https://arxiv.org/abs/2301.00001"},
                content_type="application/json",
            )
            assert response.status_code in [200, 302, 400, 401, 403, 500]


# ============= Extended Tests for Phase 3.3 Coverage =============


class TestServePdfApi:
    """Tests for PDF serving API endpoints."""

    def test_serve_pdf_api_route(self):
        """Test /api/pdf/<document_id> endpoint exists."""
        app = _create_test_app()

        with _authenticated_client(app) as client:
            response = client.get("/library/api/pdf/doc123")
            assert response.status_code in [200, 302, 401, 403, 404, 500]

    def test_serve_pdf_api_nonexistent_doc(self):
        """Test serving PDF for nonexistent document."""
        app = _create_test_app()

        with _authenticated_client(app) as client:
            response = client.get("/library/api/pdf/nonexistent-doc-id-12345")
            assert response.status_code in [302, 401, 403, 404, 500]


class TestGetPdfUrl:
    """Tests for get PDF URL endpoint."""

    def test_get_pdf_url_route(self):
        """Test /api/document/<id>/pdf-url endpoint exists."""
        app = _create_test_app()

        with _authenticated_client(app) as client:
            response = client.get("/library/api/document/doc123/pdf-url")
            assert response.status_code in [200, 302, 401, 403, 404, 500]


class TestDownloadSingleResource:
    """Extended tests for download single resource endpoint."""

    def test_download_single_resource_missing_doc(self):
        """Test download with nonexistent document."""
        app = _create_test_app()

        with _authenticated_client(app) as client:
            response = client.post("/library/api/download/nonexistent-doc-999")
            assert response.status_code in [302, 401, 403, 404, 500]

    def test_download_single_resource_with_options(self):
        """Test download with options in request body."""
        app = _create_test_app()

        with _authenticated_client(app) as client:
            response = client.post(
                "/library/api/download/doc123",
                json={"force_download": True, "storage_type": "database"},
                content_type="application/json",
            )
            assert response.status_code in [200, 302, 401, 403, 404, 500]


class TestDownloadBulk:
    """Extended tests for bulk download endpoint."""

    def test_download_bulk_empty_list(self):
        """Test bulk download with empty list."""
        app = _create_test_app()

        with _authenticated_client(app) as client:
            response = client.post(
                "/library/api/download-bulk",
                json={"research_ids": []},
                content_type="application/json",
            )
            assert response.status_code in [200, 302, 400, 401, 403, 500]

    def test_download_bulk_with_ids(self):
        """Test bulk download with research IDs."""
        app = _create_test_app()

        with _authenticated_client(app) as client:
            response = client.post(
                "/library/api/download-bulk",
                json={"research_ids": ["research1", "research2"]},
                content_type="application/json",
            )
            assert response.status_code in [200, 302, 401, 403, 500]

    def test_download_bulk_missing_research_ids(self):
        """Test bulk download without research_ids field."""
        app = _create_test_app()

        with _authenticated_client(app) as client:
            response = client.post(
                "/library/api/download-bulk",
                json={},
                content_type="application/json",
            )
            assert response.status_code in [302, 400, 401, 403, 500]


class TestCheckDownloads:
    """Extended tests for check downloads endpoint."""

    def test_check_downloads_empty_urls(self):
        """Test check downloads with empty URLs list."""
        app = _create_test_app()

        with _authenticated_client(app) as client:
            response = client.post(
                "/library/api/check-downloads",
                json={"urls": []},
                content_type="application/json",
            )
            assert response.status_code in [200, 302, 400, 401, 403, 500]

    def test_check_downloads_multiple_urls(self):
        """Test check downloads with multiple URLs."""
        app = _create_test_app()

        with _authenticated_client(app) as client:
            response = client.post(
                "/library/api/check-downloads",
                json={
                    "urls": [
                        "https://arxiv.org/abs/2301.00001",
                        "https://nature.com/articles/test",
                        "https://random.site.com/page",
                    ]
                },
                content_type="application/json",
            )
            # 400 is expected when research_id is missing from the request
            assert response.status_code in [200, 302, 400, 401, 403, 500]


class TestMarkForRedownload:
    """Extended tests for mark for redownload endpoint."""

    def test_mark_redownload_empty_list(self):
        """Test mark redownload with empty list."""
        app = _create_test_app()

        with _authenticated_client(app) as client:
            response = client.post(
                "/library/api/mark-redownload",
                json={"document_ids": []},
                content_type="application/json",
            )
            assert response.status_code in [200, 302, 400, 401, 403, 500]

    def test_mark_redownload_with_ids(self):
        """Test mark redownload with document IDs."""
        app = _create_test_app()

        with _authenticated_client(app) as client:
            response = client.post(
                "/library/api/mark-redownload",
                json={"document_ids": ["doc1", "doc2", "doc3"]},
                content_type="application/json",
            )
            assert response.status_code in [200, 302, 401, 403, 500]


class TestGetDocuments:
    """Extended tests for get documents endpoint."""

    def test_get_documents_with_pagination(self):
        """Test get documents with pagination parameters."""
        app = _create_test_app()

        with _authenticated_client(app) as client:
            response = client.get("/library/api/documents?page=2&per_page=20")
            assert response.status_code in [200, 302, 401, 403, 500]

    def test_get_documents_with_search(self):
        """Test get documents with search query."""
        app = _create_test_app()

        with _authenticated_client(app) as client:
            response = client.get(
                "/library/api/documents?search=machine+learning"
            )
            assert response.status_code in [200, 302, 401, 403, 500]

    def test_get_documents_with_filters(self):
        """Test get documents with filters."""
        app = _create_test_app()

        with _authenticated_client(app) as client:
            response = client.get(
                "/library/api/documents?collection_id=coll123&favorite=true"
            )
            assert response.status_code in [200, 302, 401, 403, 500]


class TestGetSingleDocument:
    """Tests for getting single document endpoint."""

    def test_get_single_document(self):
        """Test /api/document/<id> endpoint."""
        app = _create_test_app()

        with _authenticated_client(app) as client:
            response = client.get("/library/api/document/doc123")
            # 405 is expected because /api/document/<id> only supports DELETE
            assert response.status_code in [
                200,
                302,
                401,
                403,
                404,
                405,
                500,
            ]


class TestUpdateDocument:
    """Tests for updating document endpoint."""

    def test_update_document_title(self):
        """Test updating document title."""
        app = _create_test_app()

        with _authenticated_client(app) as client:
            response = client.put(
                "/library/api/document/doc123",
                json={"title": "Updated Title"},
                content_type="application/json",
            )
            assert response.status_code in [
                200,
                302,
                400,
                401,
                403,
                404,
                405,
                500,
            ]


class TestDeleteDocument:
    """Extended tests for delete document endpoint."""

    def test_delete_document_nonexistent(self):
        """Test deleting nonexistent document."""
        app = _create_test_app()

        with _authenticated_client(app) as client:
            response = client.delete(
                "/library/api/document/nonexistent-doc-999"
            )
            assert response.status_code in [200, 302, 401, 403, 404, 500]


class TestToggleFavorite:
    """Extended tests for toggle favorite endpoint."""

    def test_toggle_favorite_nonexistent_doc(self):
        """Test toggling favorite for nonexistent document."""
        app = _create_test_app()

        with _authenticated_client(app) as client:
            response = client.post(
                "/library/api/document/nonexistent-doc-999/toggle-favorite"
            )
            assert response.status_code in [200, 302, 401, 403, 404, 500]


class TestLibraryEdgeCases:
    """Edge case tests for library routes."""

    def test_sql_injection_in_document_id(self):
        """Test SQL injection attempt in document ID."""
        app = _create_test_app()

        with _authenticated_client(app) as client:
            response = client.get(
                "/library/api/document/'; DROP TABLE documents; --"
            )
            # 405 is expected because /api/document/<id> only supports DELETE
            assert response.status_code in [
                200,
                302,
                400,
                401,
                403,
                404,
                405,
                500,
            ]

    def test_path_traversal_in_pdf_endpoint(self):
        """Test path traversal attempt in PDF endpoint."""
        app = _create_test_app()

        with _authenticated_client(app) as client:
            response = client.get("/library/api/pdf/../../etc/passwd")
            assert response.status_code in [302, 400, 401, 403, 404, 500]

    def test_special_characters_in_search(self):
        """Test special characters in search query."""
        app = _create_test_app()

        with _authenticated_client(app) as client:
            response = client.get(
                "/library/api/documents?search=<script>alert('xss')</script>"
            )
            assert response.status_code in [200, 302, 401, 403, 500]

    def test_unicode_in_search(self):
        """Test unicode in search query."""
        app = _create_test_app()

        with _authenticated_client(app) as client:
            response = client.get("/library/api/documents?search=机器学习")
            assert response.status_code in [200, 302, 401, 403, 500]

    def test_negative_page_number(self):
        """Test negative page number in pagination."""
        app = _create_test_app()

        with _authenticated_client(app) as client:
            response = client.get("/library/api/documents?page=-1")
            assert response.status_code in [200, 302, 400, 401, 403, 500]

    def test_very_large_page_number(self):
        """Test very large page number."""
        app = _create_test_app()

        with _authenticated_client(app) as client:
            response = client.get("/library/api/documents?page=999999")
            assert response.status_code in [200, 302, 401, 403, 500]


class TestAdditionalDomains:
    """Additional tests for domain detection."""

    def test_ieee_domain(self):
        """Test IEEE domain recognition."""
        from local_deep_research.research_library.routes.library_routes import (
            is_downloadable_domain,
        )

        assert (
            is_downloadable_domain("https://ieeexplore.ieee.org/document/12345")
            is True
        )

    def test_acm_domain(self):
        """Test ACM domain recognition."""
        from local_deep_research.research_library.routes.library_routes import (
            is_downloadable_domain,
        )

        assert (
            is_downloadable_domain("https://dl.acm.org/doi/10.1145/12345")
            is True
        )

    def test_ssrn_domain(self):
        """Test SSRN domain recognition."""
        from local_deep_research.research_library.routes.library_routes import (
            is_downloadable_domain,
        )

        assert (
            is_downloadable_domain("https://ssrn.com/abstract=12345") is True
            or is_downloadable_domain("https://papers.ssrn.com/sol3/12345")
            is True
        )

    def test_openreview_domain(self):
        """Test OpenReview domain recognition."""
        from local_deep_research.research_library.routes.library_routes import (
            is_downloadable_domain,
        )

        assert (
            is_downloadable_domain("https://openreview.net/forum?id=abc123")
            is True
        )

    def test_url_with_pdf_fragment(self):
        """Test URL with PDF in fragment."""
        from local_deep_research.research_library.routes.library_routes import (
            is_downloadable_domain,
        )

        # Fragment shouldn't affect detection
        result = is_downloadable_domain("https://arxiv.org/abs/2301.00001#pdf")
        assert result is True

    def test_file_protocol_url(self):
        """Test file:// protocol URL."""
        from local_deep_research.research_library.routes.library_routes import (
            is_downloadable_domain,
        )

        result = is_downloadable_domain("file:///home/user/document.pdf")
        # Should either be True (for .pdf extension) or False (not a web domain)
        assert result is True or result is False

    def test_ftp_protocol_url(self):
        """Test ftp:// protocol URL."""
        from local_deep_research.research_library.routes.library_routes import (
            is_downloadable_domain,
        )

        result = is_downloadable_domain("ftp://ftp.example.com/paper.pdf")
        # Should recognize .pdf extension
        assert result is True or result is False


class TestDownloadResearchPdfs:
    """Extended tests for download research PDFs endpoint."""

    def test_download_research_pdfs_valid(self):
        """Test download research PDFs with valid research ID."""
        app = _create_test_app()

        with _authenticated_client(app) as client:
            response = client.post(
                "/library/api/download-research/research-123"
            )
            assert response.status_code in [200, 302, 401, 403, 404, 500]

    def test_download_research_pdfs_nonexistent(self):
        """Test download research PDFs with nonexistent research ID."""
        app = _create_test_app()

        with _authenticated_client(app) as client:
            response = client.post(
                "/library/api/download-research/nonexistent-research-999"
            )
            assert response.status_code in [302, 401, 403, 404, 500]


class TestGetResearchSources:
    """Extended tests for get research sources endpoint."""

    def test_get_research_sources_valid(self):
        """Test getting research sources with valid ID."""
        app = _create_test_app()

        with _authenticated_client(app) as client:
            response = client.get(
                "/library/api/get-research-sources/research-123"
            )
            assert response.status_code in [200, 302, 401, 403, 404, 500]

    def test_get_research_sources_nonexistent(self):
        """Test getting research sources with nonexistent ID."""
        app = _create_test_app()

        with _authenticated_client(app) as client:
            response = client.get(
                "/library/api/get-research-sources/nonexistent-research-999"
            )
            # 200 is returned when no resources are found (empty list)
            assert response.status_code in [200, 302, 401, 403, 404, 500]


class TestSyncLibrary:
    """Extended tests for sync library endpoint."""

    def test_sync_library(self):
        """Test syncing library."""
        app = _create_test_app()

        with _authenticated_client(app) as client:
            response = client.post("/library/api/sync-library")
            assert response.status_code in [200, 302, 401, 403, 500]


class TestDownloadSource:
    """Extended tests for download source endpoint."""

    def test_download_source_missing_url(self):
        """Test download source without URL."""
        app = _create_test_app()

        with _authenticated_client(app) as client:
            response = client.post(
                "/library/api/download-source",
                json={},
                content_type="application/json",
            )
            assert response.status_code in [302, 400, 401, 403, 500]

    def test_download_source_with_options(self):
        """Test download source with options."""
        app = _create_test_app()

        with _authenticated_client(app) as client:
            response = client.post(
                "/library/api/download-source",
                json={
                    "url": "https://arxiv.org/abs/2301.00001",
                    "collection_id": "coll123",
                    "storage_type": "database",
                },
                content_type="application/json",
            )
            assert response.status_code in [200, 302, 400, 401, 403, 500]


class TestResourceIdExtractionFromFilenames:
    """Tests for pre-scan directory logic that extracts resource IDs from filenames.

    The pattern is *_{id}.txt where id is an integer.
    This replaces per-resource glob calls with a single directory scan.
    """

    def test_extracts_id_from_standard_filename(self, tmp_path):
        """Should extract resource ID from filename like 'article_42.txt'."""
        txt_dir = tmp_path / "txt"
        txt_dir.mkdir()
        (txt_dir / "article_42.txt").touch()
        (txt_dir / "paper_100.txt").touch()

        existing_resource_ids = set()
        for txt_file in txt_dir.glob("*.txt"):
            parts = txt_file.stem.rsplit("_", 1)
            if len(parts) == 2:
                try:
                    existing_resource_ids.add(int(parts[1]))
                except ValueError:
                    pass

        assert existing_resource_ids == {42, 100}

    def test_ignores_non_numeric_suffix(self, tmp_path):
        """Should skip filenames without numeric ID suffix."""
        txt_dir = tmp_path / "txt"
        txt_dir.mkdir()
        (txt_dir / "readme.txt").touch()
        (txt_dir / "notes_abc.txt").touch()

        existing_resource_ids = set()
        for txt_file in txt_dir.glob("*.txt"):
            parts = txt_file.stem.rsplit("_", 1)
            if len(parts) == 2:
                try:
                    existing_resource_ids.add(int(parts[1]))
                except ValueError:
                    pass

        assert existing_resource_ids == set()

    def test_handles_empty_directory(self, tmp_path):
        """Should return empty set for empty directory."""
        txt_dir = tmp_path / "txt"
        txt_dir.mkdir()

        existing_resource_ids = set()
        if txt_dir.exists():
            for txt_file in txt_dir.glob("*.txt"):
                parts = txt_file.stem.rsplit("_", 1)
                if len(parts) == 2:
                    try:
                        existing_resource_ids.add(int(parts[1]))
                    except ValueError:
                        pass

        assert existing_resource_ids == set()

    def test_handles_nonexistent_directory(self, tmp_path):
        """Should return empty set when directory doesn't exist."""
        txt_dir = tmp_path / "txt"

        existing_resource_ids = set()
        if txt_dir.exists():
            for txt_file in txt_dir.glob("*.txt"):
                parts = txt_file.stem.rsplit("_", 1)
                if len(parts) == 2:
                    try:
                        existing_resource_ids.add(int(parts[1]))
                    except ValueError:
                        pass

        assert existing_resource_ids == set()


class TestFilterResultsDictConversion:
    """Tests for O(1) dict lookup replacing O(n²) list scan in queue_all_undownloaded."""

    def test_dict_lookup_matches_list_scan(self):
        """Dict-based lookup should find same results as linear scan."""
        filter_results = []
        for i in range(5):
            fr = Mock()
            fr.resource_id = i + 1
            fr.can_retry = i % 2 == 0
            filter_results.append(fr)

        # New approach: dict lookup
        filter_results_by_id = {r.resource_id: r for r in filter_results}

        for resource_id in [1, 2, 3, 4, 5]:
            # Old approach: linear scan
            old_result = next(
                (r for r in filter_results if r.resource_id == resource_id),
                None,
            )
            # New approach: dict lookup
            new_result = filter_results_by_id.get(resource_id)
            assert old_result is new_result

    def test_missing_resource_returns_none(self):
        """Dict lookup returns None for missing resource IDs."""
        filter_results = [Mock(resource_id=1), Mock(resource_id=3)]
        filter_results_by_id = {r.resource_id: r for r in filter_results}

        assert filter_results_by_id.get(2) is None
        assert filter_results_by_id.get(99) is None
