"""
Deep coverage tests for DownloadService targeting ~65 missing statements.

Focuses on:
- download_resource: auto-indexing path, bool result, queue update, existing tracker
- _download_pdf: existing doc update, new doc creation errors, storage mode log branches,
  text extraction failure, outer exception path
- _download_pubmed: PMC article path, elink API, webpage scraping, rate-limiting
- _try_library_text_extraction: non-dict metadata edge cases
- _save_text_with_db: pdf_extraction (non-pdfplumber) quality branch
"""

from unittest.mock import MagicMock, patch

import pytest

from local_deep_research.research_library.services.download_service import (
    DownloadService,
)

MODULE = "local_deep_research.research_library.services.download_service"


@pytest.fixture
def svc():
    """Create a DownloadService with mocked __init__."""
    with patch.object(DownloadService, "__init__", lambda self, *a, **kw: None):
        service = DownloadService.__new__(DownloadService)
        service.username = "test_user"
        service.password = "test_pass"
        service._closed = False
        service.downloaders = []
        service.retry_manager = MagicMock()
        service.settings = MagicMock()
        service.library_root = "/tmp/test_library"
        service._pubmed_delay = 1.0
        service._last_pubmed_request = 0.0
        return service


def _make_ctx(session):
    ctx = MagicMock()
    ctx.__enter__ = MagicMock(return_value=session)
    ctx.__exit__ = MagicMock(return_value=False)
    return ctx


# ============================================================
# download_resource — additional paths
# ============================================================


class TestDownloadResourceDeep:
    def test_existing_tracker_not_created(self, svc):
        """When a tracker already exists, no new tracker is added to session."""
        session = MagicMock()
        resource = MagicMock()
        resource.id = 5
        resource.url = "https://example.com/paper.pdf"
        session.query.return_value.get.return_value = resource

        existing_tracker = MagicMock()
        existing_tracker.url_hash = "abc123"

        # existing_doc = None, queue_entry = None, tracker = existing_tracker
        session.query.return_value.filter_by.return_value.first.side_effect = [
            None,  # existing_doc (COMPLETED)
            None,  # queue_entry (LibraryDownloadQueue)
            existing_tracker,  # tracker (DownloadTracker)
            None,  # queue_entry after _download_pdf
        ]

        with (
            patch(
                f"{MODULE}.get_user_db_session",
                return_value=_make_ctx(session),
            ),
            patch.object(svc, "_get_url_hash", return_value="abc123"),
            patch.object(svc, "_download_pdf", return_value=(True, None, None)),
        ):
            success, reason = svc.download_resource(5)
            assert success is True
            # session.add should NOT have been called for a new tracker
            for call_args in session.add.call_args_list:
                arg = call_args[0][0]
                from local_deep_research.database.models.download_tracker import (
                    DownloadTracker,
                )

                assert not isinstance(arg, DownloadTracker)

    def test_queue_entry_updated_on_failure(self, svc):
        """Queue entry status is set to FAILED when download fails."""
        session = MagicMock()
        resource = MagicMock()
        resource.id = 2
        resource.url = "https://example.com/paper.pdf"
        session.query.return_value.get.return_value = resource

        tracker = MagicMock()
        tracker.url_hash = "hash2"
        queue_entry = MagicMock()

        session.query.return_value.filter_by.return_value.first.side_effect = [
            None,  # existing_doc
            None,  # initial queue_entry
            tracker,  # tracker
            queue_entry,  # queue_entry for status update
        ]

        with (
            patch(
                f"{MODULE}.get_user_db_session",
                return_value=_make_ctx(session),
            ),
            patch.object(svc, "_get_url_hash", return_value="hash2"),
            patch.object(
                svc, "_download_pdf", return_value=(False, "Timeout", None)
            ),
        ):
            success, reason = svc.download_resource(2)
            assert success is False
            from local_deep_research.database.models.library import (
                DocumentStatus,
            )

            assert queue_entry.status == DocumentStatus.FAILED

    def test_auto_index_triggered_with_password(self, svc):
        """Auto-indexing is triggered when success=True and password is set."""
        session = MagicMock()
        resource = MagicMock()
        resource.id = 3
        resource.url = "https://example.com/paper.pdf"
        session.query.return_value.get.return_value = resource

        tracker = MagicMock()
        tracker.url_hash = "hash3"
        doc = MagicMock()
        doc.id = "doc-auto"

        # Sequence: existing_doc=None, queue_entry=None (first check), tracker,
        # queue_entry=None (update), then doc query (order_by path)
        session.query.return_value.filter_by.return_value.first.side_effect = [
            None,  # existing_doc
            None,  # queue_entry (initial)
            tracker,  # tracker
            None,  # queue_entry (update)
        ]
        session.query.return_value.filter_by.return_value.order_by.return_value.first.return_value = doc

        mock_trigger = MagicMock()

        with (
            patch(
                f"{MODULE}.get_user_db_session",
                return_value=_make_ctx(session),
            ),
            patch.object(svc, "_get_url_hash", return_value="hash3"),
            patch.object(svc, "_download_pdf", return_value=(True, None, None)),
            patch(
                "local_deep_research.research_library.routes.rag_routes.trigger_auto_index",
                mock_trigger,
            ),
            patch(
                f"{MODULE}.get_default_library_id", return_value="lib-default"
            ),
        ):
            success, reason = svc.download_resource(3)
            assert success is True

    def test_auto_index_exception_does_not_propagate(self, svc):
        """Exception in auto-indexing is swallowed."""
        session = MagicMock()
        resource = MagicMock()
        resource.id = 4
        resource.url = "https://example.com/paper.pdf"
        session.query.return_value.get.return_value = resource

        tracker = MagicMock()
        tracker.url_hash = "hash4"

        session.query.return_value.filter_by.return_value.first.side_effect = [
            None,  # existing_doc
            None,  # queue_entry
            tracker,
            None,  # queue_entry update
        ]
        session.query.return_value.filter_by.return_value.order_by.return_value.first.side_effect = RuntimeError(
            "index error"
        )

        with (
            patch(
                f"{MODULE}.get_user_db_session",
                return_value=_make_ctx(session),
            ),
            patch.object(svc, "_get_url_hash", return_value="hash4"),
            patch.object(svc, "_download_pdf", return_value=(True, None, None)),
        ):
            # Should not raise despite the auto-index error
            success, reason = svc.download_resource(4)
            assert success is True

    def test_no_password_skips_auto_index(self, svc):
        """When password is None, auto-indexing block is skipped entirely."""
        svc.password = None
        session = MagicMock()
        resource = MagicMock()
        resource.id = 6
        resource.url = "https://example.com/paper.pdf"
        session.query.return_value.get.return_value = resource

        tracker = MagicMock()
        tracker.url_hash = "hash6"

        session.query.return_value.filter_by.return_value.first.side_effect = [
            None,  # existing_doc
            None,  # queue_entry
            tracker,
            None,  # queue_entry update
        ]

        with (
            patch(
                f"{MODULE}.get_user_db_session",
                return_value=_make_ctx(session),
            ),
            patch.object(svc, "_get_url_hash", return_value="hash6"),
            patch.object(svc, "_download_pdf", return_value=(True, None, None)),
        ):
            success, reason = svc.download_resource(6)
            assert success is True


# ============================================================
# _download_pdf — uncovered branches
# ============================================================


class TestDownloadPdfDeep:
    def _make_tracker(self):
        tracker = MagicMock()
        tracker.url_hash = "uhash"
        tracker.download_attempts = MagicMock()
        tracker.download_attempts.count.return_value = 0
        return tracker

    def test_existing_doc_updated_filesystem_mode(self, svc):
        """When existing doc is found and storage mode is filesystem, logs filesystem path."""
        session = MagicMock()
        resource = MagicMock()
        resource.id = 10
        resource.url = "https://example.com/p.pdf"
        resource.title = "Paper Title for Extraction"
        resource.research_id = "res-10"

        tracker = self._make_tracker()
        existing_doc = MagicMock()
        existing_doc.id = "doc-existing"

        svc.settings.get_setting.side_effect = lambda key, default=None: {
            "research_library.pdf_storage_mode": "filesystem",
            "research_library.max_pdf_size_mb": 100,
        }.get(key, default)

        downloader = MagicMock()
        result_obj = MagicMock()
        result_obj.is_success = True
        result_obj.content = b"%PDF-1.4 fake"
        result_obj.skip_reason = None
        downloader.can_handle.return_value = True
        downloader.download_with_result.return_value = result_obj
        svc.downloaders = [downloader]

        mock_storage = MagicMock()
        mock_storage.save_pdf.return_value = ("/lib/pdfs/10.pdf", None)

        with (
            patch(
                f"{MODULE}.get_document_for_resource", return_value=existing_doc
            ),
            patch(f"{MODULE}.PDFStorageManager", return_value=mock_storage),
            patch.object(svc, "_extract_text_from_pdf", return_value="text"),
            patch.object(svc, "_save_text_with_db"),
        ):
            success, reason, _status_code = svc._download_pdf(
                resource, tracker, session
            )
            assert success is True
            assert reason is None

    def test_new_doc_created_database_mode(self, svc):
        """No existing doc: new Document created with database storage mode."""
        session = MagicMock()
        resource = MagicMock()
        resource.id = 11
        resource.url = "https://arxiv.org/abs/1234"
        resource.title = "New Paper Title Here"
        resource.research_id = "res-11"

        tracker = self._make_tracker()

        svc.settings.get_setting.side_effect = lambda key, default=None: {
            "research_library.pdf_storage_mode": "database",
            "research_library.max_pdf_size_mb": 50,
        }.get(key, default)

        downloader = MagicMock()
        result_obj = MagicMock()
        result_obj.is_success = True
        result_obj.content = b"%PDF database"
        result_obj.skip_reason = None
        downloader.can_handle.return_value = True
        downloader.download_with_result.return_value = result_obj
        svc.downloaders = [downloader]

        mock_storage = MagicMock()
        mock_storage.save_pdf.return_value = ("database", None)

        with (
            patch(f"{MODULE}.get_document_for_resource", return_value=None),
            patch(f"{MODULE}.get_source_type_id", return_value="src-type-1"),
            patch(f"{MODULE}.get_default_library_id", return_value="lib-col-1"),
            patch(f"{MODULE}.PDFStorageManager", return_value=mock_storage),
            patch(f"{MODULE}.uuid") as mock_uuid,
            patch.object(svc, "_extract_text_from_pdf", return_value=None),
        ):
            mock_uuid.uuid4.return_value = "new-doc-id"
            success, reason, _status_code = svc._download_pdf(
                resource, tracker, session
            )
            assert success is True

    def test_source_type_exception_propagates(self, svc):
        """Exception from get_source_type_id is re-raised."""
        session = MagicMock()
        resource = MagicMock()
        resource.id = 12
        resource.url = "https://example.com/p.pdf"
        resource.title = "Paper"
        resource.research_id = "res-12"

        tracker = self._make_tracker()

        svc.settings.get_setting.side_effect = lambda key, default=None: {
            "research_library.pdf_storage_mode": "none",
            "research_library.max_pdf_size_mb": 100,
        }.get(key, default)

        downloader = MagicMock()
        result_obj = MagicMock()
        result_obj.is_success = True
        result_obj.content = b"%PDF-1.4"
        result_obj.skip_reason = None
        downloader.can_handle.return_value = True
        downloader.download_with_result.return_value = result_obj
        svc.downloaders = [downloader]

        mock_storage = MagicMock()
        mock_storage.save_pdf.return_value = (None, None)

        with (
            patch(f"{MODULE}.get_document_for_resource", return_value=None),
            patch(
                f"{MODULE}.get_source_type_id",
                side_effect=RuntimeError("db unavailable"),
            ),
            patch(f"{MODULE}.PDFStorageManager", return_value=mock_storage),
            patch(f"{MODULE}.sanitize_for_log", return_value="safe error"),
        ):
            success, reason, _status_code = svc._download_pdf(
                resource, tracker, session
            )
            # Outer except catches the re-raised error
            assert success is False

    def test_no_downloader_for_url(self, svc):
        """No downloader can handle URL: skip_reason set to no downloader."""
        session = MagicMock()
        resource = MagicMock()
        resource.id = 13
        resource.url = "ftp://weird.ftp/file"

        tracker = self._make_tracker()

        d = MagicMock()
        d.can_handle.return_value = False
        svc.downloaders = [d]

        success, reason, _status_code = svc._download_pdf(
            resource, tracker, session
        )
        assert success is False
        assert "No compatible downloader" in reason

    def test_generic_downloader_breaks_loop(self, svc):
        """GenericDownloader with skip_reason breaks the downloader loop."""
        from local_deep_research.research_library.downloaders import (
            GenericDownloader,
        )

        session = MagicMock()
        resource = MagicMock()
        resource.id = 14
        resource.url = "https://example.com/doc"

        tracker = self._make_tracker()

        generic_dl = MagicMock(spec=GenericDownloader)
        generic_dl.can_handle.return_value = True
        result_obj = MagicMock()
        result_obj.is_success = False
        result_obj.content = None
        result_obj.skip_reason = "Not a PDF"
        generic_dl.download_with_result.return_value = result_obj
        svc.downloaders = [generic_dl]

        success, reason, _status_code = svc._download_pdf(
            resource, tracker, session
        )
        assert success is False
        assert reason == "Not a PDF"

    def test_text_extraction_exception_swallowed(self, svc):
        """Exception during text extraction does not fail the download."""
        session = MagicMock()
        resource = MagicMock()
        resource.id = 15
        resource.url = "https://example.com/p.pdf"
        resource.title = "A Title Here"
        resource.research_id = "res-15"

        tracker = self._make_tracker()
        existing_doc = MagicMock()
        existing_doc.id = "doc-15"

        svc.settings.get_setting.side_effect = lambda key, default=None: {
            "research_library.pdf_storage_mode": "none",
            "research_library.max_pdf_size_mb": 100,
        }.get(key, default)

        downloader = MagicMock()
        result_obj = MagicMock()
        result_obj.is_success = True
        result_obj.content = b"%PDF-1.4 ok"
        result_obj.skip_reason = None
        downloader.can_handle.return_value = True
        downloader.download_with_result.return_value = result_obj
        svc.downloaders = [downloader]

        mock_storage = MagicMock()
        mock_storage.save_pdf.return_value = (None, None)

        with (
            patch(
                f"{MODULE}.get_document_for_resource", return_value=existing_doc
            ),
            patch(f"{MODULE}.PDFStorageManager", return_value=mock_storage),
            patch.object(
                svc,
                "_extract_text_from_pdf",
                side_effect=RuntimeError("corrupt PDF"),
            ),
        ):
            success, reason, _status_code = svc._download_pdf(
                resource, tracker, session
            )
            assert success is True  # text extraction failure does not fail

    def test_outer_exception_caught_and_sanitized(self, svc):
        """Exceptions inside the try block are caught and sanitized via sanitize_for_log."""
        session = MagicMock()
        resource = MagicMock()
        resource.id = 16
        resource.url = "https://example.com/fail.pdf"

        tracker = self._make_tracker()

        d = MagicMock()
        d.can_handle.return_value = True
        result_obj = MagicMock()
        result_obj.is_success = True
        result_obj.content = b"%PDF"
        result_obj.skip_reason = None
        d.download_with_result.return_value = result_obj
        svc.downloaders = [d]

        # Raise inside the try block (get_document_for_resource is called after
        # downloader succeeds, so it triggers the outer except at line ~763)
        with (
            patch(
                f"{MODULE}.get_document_for_resource",
                side_effect=RuntimeError("DB exploded secret_token"),
            ),
            patch(f"{MODULE}.sanitize_for_log", return_value="redacted error"),
        ):
            success, reason, _status_code = svc._download_pdf(
                resource, tracker, session
            )
            assert success is False
            assert reason == "redacted error"

    def test_none_storage_mode_logs_text_extraction(self, svc):
        """Storage mode 'none' logs text extraction success message."""
        session = MagicMock()
        resource = MagicMock()
        resource.id = 17
        resource.url = "https://example.com/p.pdf"
        resource.title = "Paper With None Mode"
        resource.research_id = "res-17"

        tracker = self._make_tracker()
        existing_doc = MagicMock()
        existing_doc.id = "doc-17"

        svc.settings.get_setting.side_effect = lambda key, default=None: {
            "research_library.pdf_storage_mode": "none",
            "research_library.max_pdf_size_mb": 100,
        }.get(key, default)

        downloader = MagicMock()
        result_obj = MagicMock()
        result_obj.is_success = True
        result_obj.content = b"%PDF-1.4 none mode"
        result_obj.skip_reason = None
        downloader.can_handle.return_value = True
        downloader.download_with_result.return_value = result_obj
        svc.downloaders = [downloader]

        mock_storage = MagicMock()
        mock_storage.save_pdf.return_value = (None, None)

        with (
            patch(
                f"{MODULE}.get_document_for_resource", return_value=existing_doc
            ),
            patch(f"{MODULE}.PDFStorageManager", return_value=mock_storage),
            patch.object(svc, "_extract_text_from_pdf", return_value="text"),
            patch.object(svc, "_save_text_with_db"),
        ):
            success, reason, _status_code = svc._download_pdf(
                resource, tracker, session
            )
            assert success is True


# ============================================================
# _download_pubmed — uncovered branches
# ============================================================


class TestDownloadPubmedDeep:
    def test_pmc_article_url_success(self, svc):
        """Direct PMC article URL downloads via Europe PMC."""
        pmc_resp = MagicMock()
        pmc_resp.status_code = 200
        pmc_resp.headers = {"content-type": "application/pdf"}
        pmc_resp.content = b"%PDF-pmc"

        with (
            patch(f"{MODULE}.time") as mock_time,
            patch(f"{MODULE}.safe_get", return_value=pmc_resp),
        ):
            mock_time.time.return_value = 100.0
            result = svc._download_pubmed(
                "https://pmc.ncbi.nlm.nih.gov/articles/PMC1234567/"
            )
            assert result == b"%PDF-pmc"

    def test_pmc_article_url_exception(self, svc):
        """Direct PMC article: inner safe_get exception returns None."""
        with (
            patch(f"{MODULE}.time") as mock_time,
            patch(f"{MODULE}.safe_get", side_effect=RuntimeError("net error")),
        ):
            mock_time.time.return_value = 100.0
            result = svc._download_pubmed(
                "https://pmc.ncbi.nlm.nih.gov/articles/PMC9999999/"
            )
            assert result is None

    def test_pubmed_elink_api_success(self, svc):
        """PubMed PMID resolved via elink → esummary → Europe PMC PDF."""
        svc._last_pubmed_request = 0.0
        svc._pubmed_delay = 0.0

        # elink API response
        elink_resp = MagicMock()
        elink_resp.status_code = 200
        elink_resp.json.return_value = {
            "linksets": [{"linksetdbs": [{"dbto": "pmc", "links": [7654321]}]}]
        }

        # esummary response
        esummary_resp = MagicMock()
        esummary_resp.status_code = 200
        esummary_resp.json.return_value = {
            "result": {"7654321": {"uid": "7654321"}}
        }

        # Europe PMC PDF response
        pdf_resp = MagicMock()
        pdf_resp.status_code = 200
        pdf_resp.headers = {"content-type": "application/pdf"}
        pdf_resp.content = b"%PDF-elink"

        with (
            patch(f"{MODULE}.time") as mock_time,
            patch(
                f"{MODULE}.safe_get",
                side_effect=[elink_resp, esummary_resp, pdf_resp],
            ),
            patch(f"{MODULE}.urlparse") as mock_urlparse,
            # _try_europe_pmc is called first; return None so elink path runs
            patch.object(svc, "_try_europe_pmc", return_value=None),
        ):
            mock_time.time.return_value = 5.0
            parsed = MagicMock()
            parsed.hostname = "pubmed.ncbi.nlm.nih.gov"
            mock_urlparse.return_value = parsed

            result = svc._download_pubmed(
                "https://pubmed.ncbi.nlm.nih.gov/12345/"
            )
            assert result == b"%PDF-elink"

    def test_pubmed_webpage_scraping_finds_pmc(self, svc):
        """When Europe PMC and elink fail, webpage scraping finds PMC ID."""
        svc._last_pubmed_request = 0.0
        svc._pubmed_delay = 0.0

        # _try_europe_pmc returns None
        # elink API returns no links
        elink_resp = MagicMock()
        elink_resp.status_code = 200
        elink_resp.json.return_value = {"linksets": []}

        # Webpage response with PMC ID
        page_resp = MagicMock()
        page_resp.status_code = 200
        page_resp.text = "See also PMC1122334 for full text"

        # Europe PMC PDF
        pdf_resp = MagicMock()
        pdf_resp.status_code = 200
        pdf_resp.headers = {"content-type": "application/pdf"}
        pdf_resp.content = b"%PDF-scraped"

        with (
            patch(f"{MODULE}.time") as mock_time,
            patch(
                f"{MODULE}.safe_get",
                side_effect=[elink_resp, page_resp, pdf_resp],
            ),
            patch(f"{MODULE}.urlparse") as mock_urlparse,
            patch.object(svc, "_try_europe_pmc", return_value=None),
        ):
            mock_time.time.return_value = 5.0
            parsed = MagicMock()
            parsed.hostname = "pubmed.ncbi.nlm.nih.gov"
            mock_urlparse.return_value = parsed

            result = svc._download_pubmed(
                "https://pubmed.ncbi.nlm.nih.gov/9876/"
            )
            assert result == b"%PDF-scraped"

    def test_pubmed_rate_limiting_on_429(self, svc):
        """HTTP 429 from PubMed doubles _pubmed_delay and re-raises."""
        import requests

        svc._last_pubmed_request = 0.0
        svc._pubmed_delay = 1.0

        http_err = requests.exceptions.HTTPError()
        resp_mock = MagicMock()
        resp_mock.status_code = 429
        http_err.response = resp_mock

        with (
            patch(f"{MODULE}.time") as mock_time,
            patch(f"{MODULE}.safe_get", side_effect=http_err),
            patch(f"{MODULE}.urlparse") as mock_urlparse,
            patch.object(svc, "_try_europe_pmc", return_value=None),
        ):
            mock_time.time.return_value = 5.0
            parsed = MagicMock()
            parsed.hostname = "pubmed.ncbi.nlm.nih.gov"
            mock_urlparse.return_value = parsed

            # elink returns nothing so we hit the scraping path that raises
            elink_resp = MagicMock()
            elink_resp.status_code = 200
            elink_resp.json.return_value = {"linksets": []}

            with patch(
                f"{MODULE}.safe_get", side_effect=[elink_resp, http_err]
            ):
                # The rate-limit exception is re-raised then caught by outer try
                result = svc._download_pubmed(
                    "https://pubmed.ncbi.nlm.nih.gov/55555/"
                )
                # Outer except catches and returns None
                assert result is None
                assert svc._pubmed_delay == 2.0

    def test_pubmed_no_pmid_in_url_falls_to_generic(self, svc):
        """PubMed URL without PMID falls through to _download_generic."""
        svc._last_pubmed_request = 0.0
        svc._pubmed_delay = 0.0

        with (
            patch(f"{MODULE}.time") as mock_time,
            patch(f"{MODULE}.urlparse") as mock_urlparse,
            patch.object(svc, "_download_generic", return_value=b"generic"),
        ):
            mock_time.time.return_value = 5.0
            parsed = MagicMock()
            parsed.hostname = "pubmed.ncbi.nlm.nih.gov"
            mock_urlparse.return_value = parsed

            result = svc._download_pubmed(
                "https://pubmed.ncbi.nlm.nih.gov/search?term=cancer"
            )
            assert result == b"generic"

    def test_pubmed_rate_limit_sleep_applied(self, svc):
        """When last request was recent, time.sleep is called."""
        svc._pubmed_delay = 1.0
        svc._last_pubmed_request = 999.5  # only 0.5 s ago → sleep needed

        with (
            patch(f"{MODULE}.time") as mock_time,
            patch.object(svc, "_download_generic", return_value=None),
            patch(f"{MODULE}.urlparse") as mock_urlparse,
        ):
            mock_time.time.side_effect = [
                1000.0,
                1000.5,
            ]  # current_time, then set
            parsed = MagicMock()
            parsed.hostname = "other.host.com"
            mock_urlparse.return_value = parsed

            svc._download_pubmed("https://other.host.com/paper")
            mock_time.sleep.assert_called_once()


# ============================================================
# _try_library_text_extraction — non-dict metadata
# ============================================================


class TestTryLibraryTextExtractionDeep:
    def test_original_data_not_dict_falls_to_url(self, svc):
        """original_data value is not a dict: no doc_id from metadata."""
        session = MagicMock()
        resource = MagicMock()
        resource.resource_metadata = {"original_data": "string value"}
        resource.url = "/library/document/uuid-fallback"

        doc = MagicMock()
        doc.text_content = "present"
        doc.extraction_method = "pdf_extraction"
        doc.id = "uuid-fallback"
        session.query.return_value.filter_by.return_value.first.return_value = (
            doc
        )

        result = svc._try_library_text_extraction(session, resource)
        assert result == (True, None)

    def test_meta_inner_not_dict(self, svc):
        """metadata inner value is not a dict."""
        session = MagicMock()
        resource = MagicMock()
        resource.resource_metadata = {
            "original_data": {"metadata": ["list", "not", "dict"]}
        }
        resource.url = "/library/document/inner-not-dict"

        doc = MagicMock()
        doc.text_content = "ok"
        doc.extraction_method = "good"
        doc.id = "inner-not-dict"
        session.query.return_value.filter_by.return_value.first.return_value = (
            doc
        )

        result = svc._try_library_text_extraction(session, resource)
        assert result == (True, None)


# ============================================================
# _save_text_with_db — low quality non-pdfplumber path
# ============================================================


class TestSaveTextWithDbDeep:
    def test_pdf_extraction_non_pdfplumber_quality_low(self, svc):
        """pdf_extraction with source != pdfplumber gets 'low' quality."""
        session = MagicMock()
        doc = MagicMock()
        resource = MagicMock()

        with patch(f"{MODULE}.get_document_for_resource", return_value=doc):
            svc._save_text_with_db(
                resource,
                "text content",
                session,
                extraction_method="pdf_extraction",
                extraction_source="pypdf",  # not pdfplumber
            )
            assert doc.extraction_quality == "low"

    def test_create_new_doc_medium_quality_for_non_native_api(self, svc):
        """New doc with non-native_api extraction gets 'medium' quality."""
        session = MagicMock()
        resource = MagicMock()
        resource.id = 20
        resource.research_id = "res-20"
        resource.url = "https://example.com/p.pdf"
        resource.title = "Paper Title"

        library_col = MagicMock()
        library_col.id = "lib-col-2"

        with (
            patch(f"{MODULE}.get_document_for_resource", return_value=None),
            patch(f"{MODULE}.get_source_type_id", return_value="src-2"),
            patch(f"{MODULE}.uuid") as mock_uuid,
        ):
            mock_uuid.uuid4.return_value = "new-uuid-2"
            session.query.return_value.filter_by.return_value.first.return_value = library_col
            svc._save_text_with_db(
                resource,
                "text",
                session,
                extraction_method="pdf_extraction",
                extraction_source="pdfplumber",
            )
            # extraction_quality should be "medium" for pdf_extraction
            added_doc = session.add.call_args_list[0][0][0]
            assert added_doc.extraction_quality == "medium"
