"""
Deep coverage tests for library_routes.py.

Focuses on edge cases and logic paths not fully covered by
test_library_routes.py and test_library_routes_coverage.py.
"""

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

_ROUTES = "local_deep_research.research_library.routes.library_routes"


def _create_app():
    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 = Mock()
    mock_db.is_user_connected.return_value = True
    mock_db.connections = {"testuser": True}
    mock_db.has_encryption = False
    return mock_db


def _build_mock_query(
    all_result=None, first_result=None, count_result=0, get_result=None
):
    q = Mock()
    q.all.return_value = all_result if all_result is not None else []
    q.first.return_value = first_result
    q.count.return_value = count_result
    q.get.return_value = get_result
    q.filter_by.return_value = q
    q.filter.return_value = q
    q.order_by.return_value = q
    q.outerjoin.return_value = q
    q.join.return_value = q
    q.limit.return_value = q
    q.offset.return_value = q
    q.delete.return_value = 0
    return q


@contextmanager
def _auth_client(
    app,
    library_service=None,
    download_service=None,
    mock_db_session=None,
    settings_overrides=None,
    get_auth_password="mock_password",
    render_return="<html>ok</html>",
    extra_patches=None,
):
    mock_db = _mock_db_manager()
    lib_svc = library_service or Mock()
    lib_cls = Mock(return_value=lib_svc)
    dl_svc = download_service or Mock()
    dl_svc.__enter__ = Mock(return_value=dl_svc)
    dl_svc.__exit__ = Mock(return_value=False)
    dl_cls = Mock(return_value=dl_svc)
    db_session = mock_db_session or Mock()
    if not hasattr(db_session, "query") or not callable(
        getattr(db_session, "query", None)
    ):
        db_session = Mock()
        db_session.query = Mock(return_value=_build_mock_query())
    db_session.commit = Mock()
    db_session.add = Mock()

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

    mock_sm = Mock()
    defaults = {
        "research_library.pdf_storage_mode": "database",
        "research_library.shared_library": False,
        "research_library.storage_path": "/tmp/test_lib",
    }
    if settings_overrides:
        defaults.update(settings_overrides)
    mock_sm.get_setting.side_effect = lambda k, d=None: defaults.get(k, d)
    mock_render = Mock(return_value=render_return)
    patches = [
        patch("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),
        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,
        ),
        patch(
            "local_deep_research.database.library_init.get_default_library_id",
            return_value=None,
        ),
    ]
    if extra_patches:
        patches.extend(extra_patches)
    started = []
    try:
        for p in patches:
            started.append(p.start())
        with app.test_client() as client:
            with client.session_transaction() as sess:
                sess["username"] = "testuser"
                sess["session_id"] = "test-session-id"
            yield (
                client,
                {
                    "library_service": lib_svc,
                    "download_service": dl_svc,
                    "download_cls": dl_cls,
                    "db_session": db_session,
                    "settings": mock_sm,
                    "render": mock_render,
                },
            )
    finally:
        for p in patches:
            p.stop()


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


class TestServeTextApiReturnsText:
    def test_returns_text_content_and_metadata(self, app):
        doc = Mock(
            text_content="Hello world",
            title="Test Doc",
            extraction_method="pdftotext",
            word_count=2,
        )
        db_session = Mock()
        db_session.query.return_value = _build_mock_query(first_result=doc)
        with _auth_client(app, mock_db_session=db_session) as (client, _):
            resp = client.get("/library/api/document/doc-123/text")
            assert resp.status_code == 200
            data = resp.get_json()
            assert data["text_content"] == "Hello world"
            assert data["title"] == "Test Doc"
            assert data["extraction_method"] == "pdftotext"
            assert data["word_count"] == 2

    def test_returns_404_when_document_not_found(self, app):
        db_session = Mock()
        db_session.query.return_value = _build_mock_query(first_result=None)
        with _auth_client(app, mock_db_session=db_session) as (client, _):
            resp = client.get("/library/api/document/nonexistent/text")
            assert resp.status_code == 404
            assert "not found" in resp.get_json()["error"].lower()

    def test_returns_404_when_text_content_is_none(self, app):
        doc = Mock(text_content=None)
        db_session = Mock()
        db_session.query.return_value = _build_mock_query(first_result=doc)
        with _auth_client(app, mock_db_session=db_session) as (client, _):
            resp = client.get("/library/api/document/doc-empty/text")
            assert resp.status_code == 404
            assert "not available" in resp.get_json()["error"].lower()

    def test_returns_404_when_text_content_is_empty_string(self, app):
        doc = Mock(text_content="")
        db_session = Mock()
        db_session.query.return_value = _build_mock_query(first_result=doc)
        with _auth_client(app, mock_db_session=db_session) as (client, _):
            resp = client.get("/library/api/document/doc-blank/text")
            assert resp.status_code == 404


class TestDocumentDetailsPage:
    def test_returns_rendered_page_for_valid_document(self, app):
        lib_svc = Mock()
        lib_svc.get_document_by_id.return_value = {
            "id": "abc",
            "title": "My Doc",
        }
        with _auth_client(
            app, library_service=lib_svc, render_return="<html>details</html>"
        ) as (client, ctx):
            resp = client.get("/library/document/abc")
            assert resp.status_code == 200
            ctx["render"].assert_called_once()
            assert (
                ctx["render"].call_args[0][0] == "pages/document_details.html"
            )
            assert ctx["render"].call_args[1]["document"]["id"] == "abc"

    def test_returns_404_when_document_not_found(self, app):
        lib_svc = Mock()
        lib_svc.get_document_by_id.return_value = None
        with _auth_client(app, library_service=lib_svc) as (client, _):
            assert client.get("/library/document/missing").status_code == 404


class TestViewPdfPage:
    def test_serves_pdf_bytes_when_available(self, app):
        doc = Mock(id="pdf-1", file_path="/p.pdf", filename="paper.pdf")
        pdf_bytes = b"%PDF-1.4 fake"
        db_session = Mock()
        db_session.query.return_value = _build_mock_query(first_result=doc)
        mgr = Mock()
        mgr.load_pdf.return_value = pdf_bytes
        with _auth_client(
            app,
            mock_db_session=db_session,
            extra_patches=[
                patch(f"{_ROUTES}.PDFStorageManager", return_value=mgr),
                patch(
                    f"{_ROUTES}.get_library_directory", return_value="/tmp/lib"
                ),
            ],
        ) as (client, _):
            resp = client.get("/library/document/pdf-1/pdf")
            assert resp.status_code == 200
            assert resp.content_type == "application/pdf"
            assert resp.data == pdf_bytes

    def test_returns_404_when_no_pdf_blob_available(self, app):
        doc = Mock(id="pdf-2", file_path=None, filename="x.pdf")
        db_session = Mock()
        db_session.query.return_value = _build_mock_query(first_result=doc)
        mgr = Mock()
        mgr.load_pdf.return_value = None
        with _auth_client(
            app,
            mock_db_session=db_session,
            extra_patches=[
                patch(f"{_ROUTES}.PDFStorageManager", return_value=mgr),
                patch(
                    f"{_ROUTES}.get_library_directory", return_value="/tmp/lib"
                ),
            ],
        ) as (client, _):
            resp = client.get("/library/document/pdf-2/pdf")
            assert resp.status_code == 404
            assert b"not available" in resp.data.lower()

    def test_returns_404_when_document_not_found(self, app):
        db_session = Mock()
        db_session.query.return_value = _build_mock_query(first_result=None)
        with _auth_client(app, mock_db_session=db_session) as (client, _):
            resp = client.get("/library/document/ghost/pdf")
            assert resp.status_code == 404
            assert b"not found" in resp.data.lower()


class TestCheckDownloads:
    def test_returns_download_status_for_urls(self, app):
        resource = Mock(id=10, url="https://arxiv.org/abs/1234")
        cdoc = Mock(
            id="doc-10",
            status="completed",
            file_path="/p.pdf",
            file_type="pdf",
            title="P",
        )
        db_session = Mock()
        db_session.query.return_value = _build_mock_query(all_result=[resource])
        with _auth_client(
            app,
            mock_db_session=db_session,
            extra_patches=[
                patch(
                    f"{_ROUTES}.get_document_for_resource", return_value=cdoc
                ),
            ],
        ) as (client, _):
            resp = client.post(
                "/library/api/check-downloads",
                json={
                    "research_id": "r1",
                    "urls": ["https://arxiv.org/abs/1234"],
                },
            )
            assert resp.status_code == 200
            s = resp.get_json()["download_status"]["https://arxiv.org/abs/1234"]
            assert s["downloaded"] is True
            assert s["document_id"] == "doc-10"

    def test_returns_400_when_missing_research_id(self, app):
        with _auth_client(app) as (client, _):
            assert (
                client.post(
                    "/library/api/check-downloads",
                    json={"urls": ["http://x.com"]},
                ).status_code
                == 400
            )

    def test_returns_400_when_urls_empty(self, app):
        with _auth_client(app) as (client, _):
            assert (
                client.post(
                    "/library/api/check-downloads",
                    json={"research_id": "r1", "urls": []},
                ).status_code
                == 400
            )


class TestMarkForRedownload:
    def test_marks_documents_successfully(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"]},
            )
            assert resp.status_code == 200
            assert resp.get_json()["marked"] == 3

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

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


class TestGetDocumentsPagination:
    def test_returns_documents_with_pagination_params(self, app):
        lib_svc = Mock()
        lib_svc.get_documents.return_value = [{"id": "d1"}, {"id": "d2"}]
        with _auth_client(app, library_service=lib_svc) as (client, _):
            resp = client.get(
                "/library/api/documents?limit=10&offset=5&domain=arxiv.org"
            )
            assert resp.status_code == 200
            assert len(resp.get_json()["documents"]) == 2
            lib_svc.get_documents.assert_called_once_with(
                research_id=None,
                domain="arxiv.org",
                file_type=None,
                favorites_only=False,
                search_query=None,
                limit=10,
                offset=5,
            )

    def test_negative_limit_is_clamped_not_unbounded(self, app):
        """?limit=-1 must never reach the service: SQLite treats LIMIT -1 as
        "no limit", which would load the whole collection into memory. It is
        clamped to >= 1 and offset to >= 0 (#4560)."""
        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?limit=-1&offset=-5")
            assert resp.status_code == 200
            _, kwargs = lib_svc.get_documents.call_args
            assert kwargs["limit"] == 1
            assert kwargs["offset"] == 0

    def test_oversized_limit_is_capped(self, app):
        """A huge limit is capped to the upper bound so it can't load an
        unbounded number of rows (#4560)."""
        lib_svc = Mock()
        lib_svc.get_documents.return_value = []
        with _auth_client(app, library_service=lib_svc) as (client, _):
            client.get("/library/api/documents?limit=999999")
            _, kwargs = lib_svc.get_documents.call_args
            assert kwargs["limit"] == 1000

    def test_returns_empty_list_when_no_documents(self, app):
        lib_svc = Mock()
        lib_svc.get_documents.return_value = []
        with _auth_client(app, library_service=lib_svc) as (client, _):
            assert (
                client.get("/library/api/documents").get_json()["documents"]
                == []
            )

    def test_collection_filter_via_library_page(self, app):
        lib_svc = Mock()
        lib_svc.get_library_stats.return_value = {"storage_path": "/tmp"}
        lib_svc.get_documents.return_value = []
        lib_svc.get_unique_domains.return_value = []
        lib_svc.get_research_list_for_dropdown.return_value = []
        lib_svc.get_all_collections.return_value = []
        lib_svc.count_documents.return_value = 0
        with _auth_client(app, library_service=lib_svc) as (client, _):
            client.get("/library/?collection=col-42")
            lib_svc.get_documents.assert_called_once_with(
                research_id=None,
                domain=None,
                collection_id="col-42",
                date_filter=None,
                limit=100,
                offset=0,
            )


class TestQueueAllUndownloaded:
    def _make_resource(self, rid):
        return Mock(id=rid, url="https://arxiv.org/abs/1234", research_id="r1")

    def test_queues_downloadable_resources_with_filter(self, app):
        resource = self._make_resource(1)
        fr = Mock(resource_id=1, can_retry=True)
        fs = Mock()
        fs.to_dict.return_value = {"total": 1}
        fs.permanently_failed_count = 0
        fs.temporarily_failed_count = 0
        db_session = Mock()
        main_q = _build_mock_query(all_result=[resource])
        queue_q = _build_mock_query(first_result=None)
        main_q.filter_by = Mock(
            side_effect=lambda **kw: queue_q if "resource_id" in kw else main_q
        )
        db_session.query = Mock(return_value=main_q)
        db_session.commit = Mock()
        db_session.add = Mock()
        mrf = Mock()
        mrf.filter_downloadable_resources.return_value = [fr]
        mrf.get_filter_summary.return_value = fs
        mrf.get_skipped_resources_info.return_value = []
        with _auth_client(
            app,
            mock_db_session=db_session,
            extra_patches=[
                patch(f"{_ROUTES}.ResourceFilter", return_value=mrf),
                patch(f"{_ROUTES}.is_downloadable_domain", return_value=True),
            ],
        ) as (client, _):
            resp = client.post("/library/api/queue-all-undownloaded")
            assert resp.status_code == 200
            assert resp.get_json()["queued"] >= 1

    def test_skips_resources_that_fail_filter(self, app):
        resource = self._make_resource(1)
        fs = Mock()
        fs.to_dict.return_value = {"total": 1}
        fs.permanently_failed_count = 1
        fs.temporarily_failed_count = 0
        db_session = Mock()
        db_session.query = Mock(
            return_value=_build_mock_query(all_result=[resource])
        )
        db_session.commit = Mock()
        db_session.add = Mock()
        mrf = Mock()
        mrf.filter_downloadable_resources.return_value = []
        mrf.get_filter_summary.return_value = fs
        mrf.get_skipped_resources_info.return_value = []
        with _auth_client(
            app,
            mock_db_session=db_session,
            extra_patches=[
                patch(f"{_ROUTES}.ResourceFilter", return_value=mrf),
                patch(f"{_ROUTES}.is_downloadable_domain", return_value=True),
            ],
        ) as (client, _):
            resp = client.post("/library/api/queue-all-undownloaded")
            assert (
                resp.get_json()["queued"] == 0
                and resp.get_json()["skipped"] >= 1
            )


class TestDownloadSingleResource:
    def test_successful_download(self, app):
        dl_svc = Mock()
        dl_svc.download_resource.return_value = (True, None)
        dl_svc.__enter__ = Mock(return_value=dl_svc)
        dl_svc.__exit__ = Mock(return_value=False)
        with _auth_client(app, download_service=dl_svc) as (client, _):
            assert (
                client.post("/library/api/download/42").get_json()["success"]
                is True
            )

    def test_failed_download_returns_500(self, app):
        dl_svc = Mock()
        dl_svc.download_resource.return_value = (False, "timeout")
        dl_svc.__enter__ = Mock(return_value=dl_svc)
        dl_svc.__exit__ = Mock(return_value=False)
        with _auth_client(app, download_service=dl_svc) as (client, _):
            resp = client.post("/library/api/download/42")
            assert resp.status_code == 500
            assert resp.get_json()["success"] is False


class TestDownloadSource:
    def test_successful_download_of_source(self, app):
        resource = Mock(id=7, research_id="r1")
        db_session = Mock()
        db_session.query = Mock(
            return_value=_build_mock_query(first_result=resource)
        )
        db_session.commit = Mock()
        db_session.add = Mock()
        dl_svc = Mock()
        dl_svc.download_resource.return_value = (True, None)
        dl_svc.__enter__ = Mock(return_value=dl_svc)
        dl_svc.__exit__ = Mock(return_value=False)
        with _auth_client(
            app,
            mock_db_session=db_session,
            download_service=dl_svc,
            extra_patches=[
                patch(f"{_ROUTES}.is_downloadable_domain", return_value=True),
                patch(
                    f"{_ROUTES}.get_document_for_resource", return_value=None
                ),
            ],
        ) as (client, _):
            resp = client.post(
                "/library/api/download-source",
                json={"research_id": "r1", "url": "https://arxiv.org/abs/1234"},
            )
            assert resp.get_json()["success"] is True

    def test_returns_400_for_non_downloadable_url(self, app):
        with _auth_client(
            app,
            extra_patches=[
                patch(f"{_ROUTES}.is_downloadable_domain", return_value=False),
            ],
        ) as (client, _):
            resp = client.post(
                "/library/api/download-source",
                json={"research_id": "r1", "url": "https://example.com"},
            )
            assert resp.status_code == 400

    def test_returns_404_when_resource_not_found(self, app):
        db_session = Mock()
        db_session.query.return_value = _build_mock_query(first_result=None)
        with _auth_client(
            app,
            mock_db_session=db_session,
            extra_patches=[
                patch(f"{_ROUTES}.is_downloadable_domain", return_value=True),
            ],
        ) as (client, _):
            resp = client.post(
                "/library/api/download-source",
                json={"research_id": "r1", "url": "https://arxiv.org/abs/9999"},
            )
            assert resp.status_code == 404

    def test_returns_success_when_already_downloaded(self, app):
        resource = Mock(id=7)
        existing_doc = Mock(id="doc-7", status="completed")
        db_session = Mock()
        db_session.query.return_value = _build_mock_query(first_result=resource)
        with _auth_client(
            app,
            mock_db_session=db_session,
            extra_patches=[
                patch(f"{_ROUTES}.is_downloadable_domain", return_value=True),
                patch(
                    f"{_ROUTES}.get_document_for_resource",
                    return_value=existing_doc,
                ),
            ],
        ) as (client, _):
            resp = client.post(
                "/library/api/download-source",
                json={"research_id": "r1", "url": "https://arxiv.org/abs/1234"},
            )
            data = resp.get_json()
            assert data["message"] == "Already downloaded"
            assert data["document_id"] == "doc-7"

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

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