"""
Tests for DownloadService.
"""

from unittest.mock import Mock, MagicMock


class TestDownloadServiceInit:
    """Tests for DownloadService initialization."""

    def test_init_creates_downloaders(self, mocker):
        """Initializes with list of downloaders."""
        # Mock the settings manager
        mock_settings = Mock()
        mock_settings.get_setting.return_value = "/tmp/test_library"
        mocker.patch(
            "local_deep_research.research_library.services.download_service.get_settings_manager",
            return_value=mock_settings,
        )

        # Mock Path.mkdir
        mocker.patch("pathlib.Path.mkdir")

        # Mock RetryManager
        mock_retry_manager = Mock()
        mocker.patch(
            "local_deep_research.research_library.services.download_service.RetryManager",
            return_value=mock_retry_manager,
        )

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

        service = DownloadService(username="test_user")

        assert service.username == "test_user"
        assert len(service.downloaders) > 0
        assert service.library_root == "/tmp/test_library"


class TestDownloadServiceUrlNormalization:
    """Tests for URL normalization."""

    def test_normalize_url_removes_protocol(self, mocker):
        """URL normalization removes http/https protocol."""
        # Create service with minimal initialization
        mock_settings = Mock()
        mock_settings.get_setting.return_value = "/tmp/test_library"
        mocker.patch(
            "local_deep_research.research_library.services.download_service.get_settings_manager",
            return_value=mock_settings,
        )
        mocker.patch("pathlib.Path.mkdir")
        mocker.patch(
            "local_deep_research.research_library.services.download_service.RetryManager"
        )

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

        service = DownloadService(username="test_user")

        url1 = service._normalize_url("https://example.com/path")
        url2 = service._normalize_url("http://example.com/path")

        assert url1 == url2
        assert not url1.startswith("http")

    def test_normalize_url_removes_www(self, mocker):
        """URL normalization removes www prefix."""
        mock_settings = Mock()
        mock_settings.get_setting.return_value = "/tmp/test_library"
        mocker.patch(
            "local_deep_research.research_library.services.download_service.get_settings_manager",
            return_value=mock_settings,
        )
        mocker.patch("pathlib.Path.mkdir")
        mocker.patch(
            "local_deep_research.research_library.services.download_service.RetryManager"
        )

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

        service = DownloadService(username="test_user")

        url1 = service._normalize_url("https://www.example.com/path")
        url2 = service._normalize_url("https://example.com/path")

        assert url1 == url2
        assert "www" not in url1

    def test_normalize_url_removes_trailing_slash(self, mocker):
        """URL normalization removes trailing slashes."""
        mock_settings = Mock()
        mock_settings.get_setting.return_value = "/tmp/test_library"
        mocker.patch(
            "local_deep_research.research_library.services.download_service.get_settings_manager",
            return_value=mock_settings,
        )
        mocker.patch("pathlib.Path.mkdir")
        mocker.patch(
            "local_deep_research.research_library.services.download_service.RetryManager"
        )

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

        service = DownloadService(username="test_user")

        url1 = service._normalize_url("https://example.com/path/")
        url2 = service._normalize_url("https://example.com/path")

        assert url1 == url2
        assert not url1.endswith("/")

    def test_normalize_url_sorts_query_params(self, mocker):
        """URL normalization sorts query parameters."""
        mock_settings = Mock()
        mock_settings.get_setting.return_value = "/tmp/test_library"
        mocker.patch(
            "local_deep_research.research_library.services.download_service.get_settings_manager",
            return_value=mock_settings,
        )
        mocker.patch("pathlib.Path.mkdir")
        mocker.patch(
            "local_deep_research.research_library.services.download_service.RetryManager"
        )

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

        service = DownloadService(username="test_user")

        url1 = service._normalize_url("https://example.com/path?b=2&a=1")
        url2 = service._normalize_url("https://example.com/path?a=1&b=2")

        assert url1 == url2


class TestDownloadServiceIsDownloadable:
    """Tests for _is_downloadable method."""

    def test_is_downloadable_pdf_extension(self, mocker):
        """Identifies .pdf URLs as downloadable."""
        mock_settings = Mock()
        mock_settings.get_setting.return_value = "/tmp/test_library"
        mocker.patch(
            "local_deep_research.research_library.services.download_service.get_settings_manager",
            return_value=mock_settings,
        )
        mocker.patch("pathlib.Path.mkdir")
        mocker.patch(
            "local_deep_research.research_library.services.download_service.RetryManager"
        )

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

        service = DownloadService(username="test_user")

        mock_resource = Mock()
        mock_resource.url = "https://example.com/paper.pdf"

        assert service._is_downloadable(mock_resource) is True

    def test_is_downloadable_arxiv_url(self, mocker):
        """Identifies arXiv URLs as downloadable."""
        mock_settings = Mock()
        mock_settings.get_setting.return_value = "/tmp/test_library"
        mocker.patch(
            "local_deep_research.research_library.services.download_service.get_settings_manager",
            return_value=mock_settings,
        )
        mocker.patch("pathlib.Path.mkdir")
        mocker.patch(
            "local_deep_research.research_library.services.download_service.RetryManager"
        )

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

        service = DownloadService(username="test_user")

        mock_resource = Mock()
        mock_resource.url = "https://arxiv.org/abs/2301.00001"

        assert service._is_downloadable(mock_resource) is True

    def test_is_downloadable_pubmed_pmc_url(self, mocker):
        """Identifies PubMed Central URLs as downloadable."""
        mock_settings = Mock()
        mock_settings.get_setting.return_value = "/tmp/test_library"
        mocker.patch(
            "local_deep_research.research_library.services.download_service.get_settings_manager",
            return_value=mock_settings,
        )
        mocker.patch("pathlib.Path.mkdir")
        mocker.patch(
            "local_deep_research.research_library.services.download_service.RetryManager"
        )

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

        service = DownloadService(username="test_user")

        mock_resource = Mock()
        mock_resource.url = "https://ncbi.nlm.nih.gov/pmc/articles/PMC1234567"

        assert service._is_downloadable(mock_resource) is True

    def test_is_downloadable_biorxiv_url(self, mocker):
        """Identifies bioRxiv URLs as downloadable."""
        mock_settings = Mock()
        mock_settings.get_setting.return_value = "/tmp/test_library"
        mocker.patch(
            "local_deep_research.research_library.services.download_service.get_settings_manager",
            return_value=mock_settings,
        )
        mocker.patch("pathlib.Path.mkdir")
        mocker.patch(
            "local_deep_research.research_library.services.download_service.RetryManager"
        )

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

        service = DownloadService(username="test_user")

        mock_resource = Mock()
        mock_resource.url = (
            "https://biorxiv.org/content/10.1101/2023.01.01.123456v1"
        )

        assert service._is_downloadable(mock_resource) is True

    def test_is_not_downloadable_regular_html(self, mocker):
        """Rejects regular HTML pages as not downloadable."""
        mock_settings = Mock()
        mock_settings.get_setting.return_value = "/tmp/test_library"
        mocker.patch(
            "local_deep_research.research_library.services.download_service.get_settings_manager",
            return_value=mock_settings,
        )
        mocker.patch("pathlib.Path.mkdir")
        mocker.patch(
            "local_deep_research.research_library.services.download_service.RetryManager"
        )

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

        service = DownloadService(username="test_user")

        mock_resource = Mock()
        mock_resource.url = "https://example.com/about.html"

        assert service._is_downloadable(mock_resource) is False


class TestDownloadServiceIsAlreadyDownloaded:
    """Tests for is_already_downloaded method."""

    def test_is_already_downloaded_true(self, mocker):
        """Returns True when URL is already downloaded."""
        mock_settings = Mock()
        mock_settings.get_setting.return_value = "/tmp/test_library"
        mocker.patch(
            "local_deep_research.research_library.services.download_service.get_settings_manager",
            return_value=mock_settings,
        )
        mocker.patch("pathlib.Path.mkdir")
        mocker.patch(
            "local_deep_research.research_library.services.download_service.RetryManager"
        )

        # Mock session and tracker
        mock_session = MagicMock()
        mock_session.__enter__ = Mock(return_value=mock_session)
        mock_session.__exit__ = Mock(return_value=False)

        mock_tracker = Mock()
        mock_tracker.is_downloaded = True
        mock_tracker.file_path = "pdfs/test.pdf"

        mock_session.query.return_value.filter_by.return_value.first.return_value = mock_tracker

        mocker.patch(
            "local_deep_research.research_library.services.download_service.get_user_db_session",
            return_value=mock_session,
        )

        # Mock path exists
        mock_path = Mock()
        mock_path.is_file.return_value = True
        mocker.patch(
            "local_deep_research.research_library.services.download_service.get_absolute_path_from_settings",
            return_value=mock_path,
        )

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

        service = DownloadService(username="test_user")

        is_downloaded, file_path = service.is_already_downloaded(
            "https://arxiv.org/pdf/2301.00001.pdf"
        )

        assert is_downloaded is True
        assert file_path is not None

    def test_is_already_downloaded_false_no_tracker(self, mocker):
        """Returns False when no tracker exists."""
        mock_settings = Mock()
        mock_settings.get_setting.return_value = "/tmp/test_library"
        mocker.patch(
            "local_deep_research.research_library.services.download_service.get_settings_manager",
            return_value=mock_settings,
        )
        mocker.patch("pathlib.Path.mkdir")
        mocker.patch(
            "local_deep_research.research_library.services.download_service.RetryManager"
        )

        # Mock session with no tracker
        mock_session = MagicMock()
        mock_session.__enter__ = Mock(return_value=mock_session)
        mock_session.__exit__ = Mock(return_value=False)
        mock_session.query.return_value.filter_by.return_value.first.return_value = None

        mocker.patch(
            "local_deep_research.research_library.services.download_service.get_user_db_session",
            return_value=mock_session,
        )

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

        service = DownloadService(username="test_user")

        is_downloaded, file_path = service.is_already_downloaded(
            "https://arxiv.org/pdf/2301.00001.pdf"
        )

        assert is_downloaded is False
        assert file_path is None


class TestDownloadServiceGetDownloader:
    """Tests for _get_downloader method."""

    def test_get_downloader_for_arxiv(self, mocker):
        """Gets ArxivDownloader for arXiv URLs."""
        mock_settings = Mock()
        mock_settings.get_setting.return_value = "/tmp/test_library"
        mocker.patch(
            "local_deep_research.research_library.services.download_service.get_settings_manager",
            return_value=mock_settings,
        )
        mocker.patch("pathlib.Path.mkdir")
        mocker.patch(
            "local_deep_research.research_library.services.download_service.RetryManager"
        )

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

        service = DownloadService(username="test_user")

        downloader = service._get_downloader("https://arxiv.org/abs/2301.00001")

        assert downloader is not None
        assert isinstance(downloader, ArxivDownloader)

    def test_get_downloader_for_pubmed(self, mocker):
        """Gets PubMedDownloader for PubMed URLs."""
        mock_settings = Mock()
        mock_settings.get_setting.return_value = "/tmp/test_library"
        mocker.patch(
            "local_deep_research.research_library.services.download_service.get_settings_manager",
            return_value=mock_settings,
        )
        mocker.patch("pathlib.Path.mkdir")
        mocker.patch(
            "local_deep_research.research_library.services.download_service.RetryManager"
        )

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

        service = DownloadService(username="test_user")

        downloader = service._get_downloader(
            "https://pubmed.ncbi.nlm.nih.gov/12345678"
        )

        assert downloader is not None
        assert isinstance(downloader, PubMedDownloader)

    def test_get_downloader_for_pdf_url(self, mocker):
        """Gets DirectPDFDownloader for direct PDF URLs."""
        mock_settings = Mock()
        mock_settings.get_setting.return_value = "/tmp/test_library"
        mocker.patch(
            "local_deep_research.research_library.services.download_service.get_settings_manager",
            return_value=mock_settings,
        )
        mocker.patch("pathlib.Path.mkdir")
        mocker.patch(
            "local_deep_research.research_library.services.download_service.RetryManager"
        )

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

        service = DownloadService(username="test_user")

        downloader = service._get_downloader("https://example.com/paper.pdf")

        assert downloader is not None
        assert isinstance(downloader, DirectPDFDownloader)


class TestDownloadServiceTextExtraction:
    """Tests for text extraction methods."""

    def test_extract_text_from_pdf_success(self, mocker, mock_pdf_content):
        """Extracts text from valid PDF content."""
        mock_settings = Mock()
        mock_settings.get_setting.return_value = "/tmp/test_library"
        mocker.patch(
            "local_deep_research.research_library.services.download_service.get_settings_manager",
            return_value=mock_settings,
        )
        mocker.patch("pathlib.Path.mkdir")
        mocker.patch(
            "local_deep_research.research_library.services.download_service.RetryManager"
        )

        # Mock pdfplumber
        mock_pdf = MagicMock()
        mock_page = MagicMock()
        mock_page.extract_text.return_value = "Extracted text from page 1"
        mock_pdf.pages = [mock_page]
        mock_pdf.__enter__ = Mock(return_value=mock_pdf)
        mock_pdf.__exit__ = Mock(return_value=False)

        mocker.patch(
            "local_deep_research.research_library.services.download_service.pdfplumber.open",
            return_value=mock_pdf,
        )

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

        service = DownloadService(username="test_user")

        text = service._extract_text_from_pdf(mock_pdf_content)

        assert text is not None
        assert "Extracted text from page 1" in text

    def test_extract_text_from_pdf_empty(self, mocker):
        """Returns None when PDF has no extractable text."""
        mock_settings = Mock()
        mock_settings.get_setting.return_value = "/tmp/test_library"
        mocker.patch(
            "local_deep_research.research_library.services.download_service.get_settings_manager",
            return_value=mock_settings,
        )
        mocker.patch("pathlib.Path.mkdir")
        mocker.patch(
            "local_deep_research.research_library.services.download_service.RetryManager"
        )

        # Mock pdfplumber with no text
        mock_pdf = MagicMock()
        mock_page = MagicMock()
        mock_page.extract_text.return_value = ""
        mock_pdf.pages = [mock_page]
        mock_pdf.__enter__ = Mock(return_value=mock_pdf)
        mock_pdf.__exit__ = Mock(return_value=False)

        mocker.patch(
            "local_deep_research.research_library.services.download_service.pdfplumber.open",
            return_value=mock_pdf,
        )

        # Also mock PyPDF as fallback with no text
        mock_reader = MagicMock()
        mock_pypdf_page = MagicMock()
        mock_pypdf_page.extract_text.return_value = ""
        mock_reader.pages = [mock_pypdf_page]
        mocker.patch(
            "local_deep_research.research_library.services.download_service.PdfReader",
            return_value=mock_reader,
        )

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

        service = DownloadService(username="test_user")

        text = service._extract_text_from_pdf(b"%PDF-1.4 empty")

        assert text is None


class TestPyPDFTextExtraction:
    """
    Tests for pypdf text extraction functionality.

    These tests verify pypdf behavior for the 6.5->6.6 upgrade, focusing on:
    - Fallback from pdfplumber to pypdf
    - Multi-page PDF handling
    - Malformed PDF handling (CVE-related)
    """

    def test_pypdf_fallback_when_pdfplumber_empty(self, mocker):
        """Uses pypdf when pdfplumber extracts no text."""
        mock_settings = Mock()
        mock_settings.get_setting.return_value = "/tmp/test_library"
        mocker.patch(
            "local_deep_research.research_library.services.download_service.get_settings_manager",
            return_value=mock_settings,
        )
        mocker.patch("pathlib.Path.mkdir")
        mocker.patch(
            "local_deep_research.research_library.services.download_service.RetryManager"
        )

        # Mock pdfplumber to return empty text
        mock_pdf = MagicMock()
        mock_page = MagicMock()
        mock_page.extract_text.return_value = ""
        mock_pdf.pages = [mock_page]
        mock_pdf.__enter__ = Mock(return_value=mock_pdf)
        mock_pdf.__exit__ = Mock(return_value=False)

        mocker.patch(
            "local_deep_research.research_library.services.download_service.pdfplumber.open",
            return_value=mock_pdf,
        )

        # Mock pypdf PdfReader to return actual text
        mock_reader = MagicMock()
        mock_pypdf_page = MagicMock()
        mock_pypdf_page.extract_text.return_value = "Text from pypdf"
        mock_reader.pages = [mock_pypdf_page]
        mocker.patch(
            "local_deep_research.research_library.services.download_service.PdfReader",
            return_value=mock_reader,
        )

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

        service = DownloadService(username="test_user")

        text = service._extract_text_from_pdf(b"%PDF-1.4 test")

        assert text is not None
        assert "Text from pypdf" in text

    def test_pypdf_fallback_when_pdfplumber_fails(self, mocker):
        """Uses pypdf when pdfplumber raises an exception."""
        mock_settings = Mock()
        mock_settings.get_setting.return_value = "/tmp/test_library"
        mocker.patch(
            "local_deep_research.research_library.services.download_service.get_settings_manager",
            return_value=mock_settings,
        )
        mocker.patch("pathlib.Path.mkdir")
        mocker.patch(
            "local_deep_research.research_library.services.download_service.RetryManager"
        )

        # Mock pdfplumber to raise exception
        mocker.patch(
            "local_deep_research.research_library.services.download_service.pdfplumber.open",
            side_effect=Exception("pdfplumber failed"),
        )

        # Mock pypdf PdfReader to work correctly
        mock_reader = MagicMock()
        mock_pypdf_page = MagicMock()
        mock_pypdf_page.extract_text.return_value = "Fallback text from pypdf"
        mock_reader.pages = [mock_pypdf_page]
        mocker.patch(
            "local_deep_research.research_library.services.download_service.PdfReader",
            return_value=mock_reader,
        )

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

        service = DownloadService(username="test_user")

        # When pdfplumber fails entirely, the whole try block fails
        # and returns None (logs exception)
        text = service._extract_text_from_pdf(b"%PDF-1.4 test")

        # The current implementation catches the exception and returns None
        # This test documents the current behavior
        assert text is None

    def test_extract_text_multipage_pdf(self, mocker):
        """Extracts text from all pages of multi-page PDF."""
        mock_settings = Mock()
        mock_settings.get_setting.return_value = "/tmp/test_library"
        mocker.patch(
            "local_deep_research.research_library.services.download_service.get_settings_manager",
            return_value=mock_settings,
        )
        mocker.patch("pathlib.Path.mkdir")
        mocker.patch(
            "local_deep_research.research_library.services.download_service.RetryManager"
        )

        # Mock pdfplumber with multiple pages
        mock_pdf = MagicMock()
        mock_page1 = MagicMock()
        mock_page1.extract_text.return_value = "Page 1 content"
        mock_page2 = MagicMock()
        mock_page2.extract_text.return_value = "Page 2 content"
        mock_page3 = MagicMock()
        mock_page3.extract_text.return_value = "Page 3 content"
        mock_pdf.pages = [mock_page1, mock_page2, mock_page3]
        mock_pdf.__enter__ = Mock(return_value=mock_pdf)
        mock_pdf.__exit__ = Mock(return_value=False)

        mocker.patch(
            "local_deep_research.research_library.services.download_service.pdfplumber.open",
            return_value=mock_pdf,
        )

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

        service = DownloadService(username="test_user")

        text = service._extract_text_from_pdf(b"%PDF-1.4 multipage")

        assert text is not None
        assert "Page 1 content" in text
        assert "Page 2 content" in text
        assert "Page 3 content" in text
        # Pages should be joined with double newlines
        assert "\n\n" in text

    def test_malformed_pdf_returns_none(self, mocker):
        """Returns None for malformed/corrupted PDF content."""
        mock_settings = Mock()
        mock_settings.get_setting.return_value = "/tmp/test_library"
        mocker.patch(
            "local_deep_research.research_library.services.download_service.get_settings_manager",
            return_value=mock_settings,
        )
        mocker.patch("pathlib.Path.mkdir")
        mocker.patch(
            "local_deep_research.research_library.services.download_service.RetryManager"
        )

        # Mock pdfplumber to raise exception on malformed PDF
        mocker.patch(
            "local_deep_research.research_library.services.download_service.pdfplumber.open",
            side_effect=Exception("Invalid PDF structure"),
        )

        # Mock pypdf to also fail on malformed PDF
        mocker.patch(
            "local_deep_research.research_library.services.download_service.PdfReader",
            side_effect=Exception("Cannot read malformed PDF"),
        )

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

        service = DownloadService(username="test_user")

        # Truncated/malformed PDF content
        malformed_pdf = b"%PDF-1.4\n1 0 obj\n<<\n/Type /Catalog"

        text = service._extract_text_from_pdf(malformed_pdf)

        # Should gracefully return None, not raise exception
        assert text is None

    def test_pdf_pages_no_extractable_text(self, mocker):
        """Returns None when PDF pages have no text (e.g., scanned images)."""
        mock_settings = Mock()
        mock_settings.get_setting.return_value = "/tmp/test_library"
        mocker.patch(
            "local_deep_research.research_library.services.download_service.get_settings_manager",
            return_value=mock_settings,
        )
        mocker.patch("pathlib.Path.mkdir")
        mocker.patch(
            "local_deep_research.research_library.services.download_service.RetryManager"
        )

        # Mock pdfplumber with pages that return None (scanned images)
        mock_pdf = MagicMock()
        mock_page1 = MagicMock()
        mock_page1.extract_text.return_value = None
        mock_page2 = MagicMock()
        mock_page2.extract_text.return_value = None
        mock_pdf.pages = [mock_page1, mock_page2]
        mock_pdf.__enter__ = Mock(return_value=mock_pdf)
        mock_pdf.__exit__ = Mock(return_value=False)

        mocker.patch(
            "local_deep_research.research_library.services.download_service.pdfplumber.open",
            return_value=mock_pdf,
        )

        # Mock pypdf also returning no text
        mock_reader = MagicMock()
        mock_pypdf_page1 = MagicMock()
        mock_pypdf_page1.extract_text.return_value = None
        mock_pypdf_page2 = MagicMock()
        mock_pypdf_page2.extract_text.return_value = None
        mock_reader.pages = [mock_pypdf_page1, mock_pypdf_page2]
        mocker.patch(
            "local_deep_research.research_library.services.download_service.PdfReader",
            return_value=mock_reader,
        )

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

        service = DownloadService(username="test_user")

        text = service._extract_text_from_pdf(b"%PDF-1.4 scanned")

        assert text is None


class TestDownloadServiceQueueResearchDownloads:
    """Tests for queue_research_downloads method."""

    def test_queue_research_downloads_success(self, mocker):
        """Queues downloads for downloadable resources."""
        mock_settings = Mock()
        mock_settings.get_setting.return_value = "/tmp/test_library"
        mocker.patch(
            "local_deep_research.research_library.services.download_service.get_settings_manager",
            return_value=mock_settings,
        )
        mocker.patch("pathlib.Path.mkdir")
        mocker.patch(
            "local_deep_research.research_library.services.download_service.RetryManager"
        )

        # Mock session
        mock_session = MagicMock()
        mock_session.__enter__ = Mock(return_value=mock_session)
        mock_session.__exit__ = Mock(return_value=False)

        # Create mock resources
        mock_resource1 = Mock()
        mock_resource1.id = 1
        mock_resource1.url = "https://arxiv.org/abs/2301.00001"

        mock_resource2 = Mock()
        mock_resource2.id = 2
        mock_resource2.url = "https://example.com/page.html"

        mock_session.query.return_value.filter_by.return_value.all.return_value = [
            mock_resource1,
            mock_resource2,
        ]
        mock_session.query.return_value.filter_by.return_value.first.return_value = None

        mocker.patch(
            "local_deep_research.research_library.services.download_service.get_user_db_session",
            return_value=mock_session,
        )

        # Mock get_default_library_id
        mocker.patch(
            "local_deep_research.database.library_init.get_default_library_id",
            return_value="default-lib-id",
        )

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

        service = DownloadService(username="test_user")

        queued_count = service.queue_research_downloads("research-123")

        # Only arxiv URL should be queued (HTML page is not downloadable)
        assert queued_count >= 0

    def test_queue_skips_resources_with_document_id(self, mocker):
        """Resources that already have document_id are skipped (not queued)."""
        mock_settings = Mock()
        mock_settings.get_setting.return_value = "/tmp/test_library"
        mocker.patch(
            "local_deep_research.research_library.services.download_service.get_settings_manager",
            return_value=mock_settings,
        )
        mocker.patch("pathlib.Path.mkdir")
        mocker.patch(
            "local_deep_research.research_library.services.download_service.RetryManager"
        )

        # Mock session
        mock_session = MagicMock()
        mock_session.__enter__ = Mock(return_value=mock_session)
        mock_session.__exit__ = Mock(return_value=False)

        # Resource WITH document_id (should be skipped)
        resource_linked = Mock()
        resource_linked.id = 10
        resource_linked.url = "https://arxiv.org/abs/2301.00010"
        resource_linked.document_id = "existing-doc-uuid"

        # Resource WITHOUT document_id (should be queued)
        resource_unlinked = Mock()
        resource_unlinked.id = 11
        resource_unlinked.url = "https://arxiv.org/abs/2301.00011"
        resource_unlinked.document_id = None

        mock_session.query.return_value.filter_by.return_value.all.return_value = [
            resource_linked,
            resource_unlinked,
        ]
        # No existing queue entry for unlinked resource
        mock_session.query.return_value.filter_by.return_value.first.return_value = None

        mocker.patch(
            "local_deep_research.research_library.services.download_service.get_user_db_session",
            return_value=mock_session,
        )
        mocker.patch(
            "local_deep_research.database.library_init.get_default_library_id",
            return_value="default-lib-id",
        )

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

        service = DownloadService(username="test_user")
        queued_count = service.queue_research_downloads("research-456")

        # Only unlinked resource should be queued
        assert queued_count == 1


class TestDownloadServiceDownloadResource:
    """Tests for download_resource method."""

    def test_download_resource_not_found(self, mocker):
        """Returns error when resource not found."""
        mock_settings = Mock()
        mock_settings.get_setting.return_value = "/tmp/test_library"
        mocker.patch(
            "local_deep_research.research_library.services.download_service.get_settings_manager",
            return_value=mock_settings,
        )
        mocker.patch("pathlib.Path.mkdir")
        mocker.patch(
            "local_deep_research.research_library.services.download_service.RetryManager"
        )

        # Mock session with no resource
        mock_session = MagicMock()
        mock_session.__enter__ = Mock(return_value=mock_session)
        mock_session.__exit__ = Mock(return_value=False)
        mock_session.query.return_value.get.return_value = None

        mocker.patch(
            "local_deep_research.research_library.services.download_service.get_user_db_session",
            return_value=mock_session,
        )

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

        service = DownloadService(username="test_user")

        success, reason = service.download_resource(999)

        assert success is False
        assert "not found" in reason.lower()


class TestDownloadPdf:
    """Tests for _download_pdf method."""

    def test_download_pdf_creates_download_attempt(self, mocker):
        """Creates a download attempt record."""
        mock_settings = Mock()
        mock_settings.get_setting.return_value = "/tmp/test_library"
        mocker.patch(
            "local_deep_research.research_library.services.download_service.get_settings_manager",
            return_value=mock_settings,
        )
        mocker.patch("pathlib.Path.mkdir")
        mocker.patch(
            "local_deep_research.research_library.services.download_service.RetryManager"
        )

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

        service = DownloadService(username="test_user")

        mock_resource = Mock()
        mock_resource.url = "https://example.com/paper.pdf"

        mock_tracker = Mock()
        mock_tracker.url_hash = "abc123"
        mock_tracker.download_attempts = Mock()
        mock_tracker.download_attempts.count.return_value = 0

        mock_session = MagicMock()

        # Mock downloader to fail
        for downloader in service.downloaders:
            downloader.can_handle = Mock(return_value=False)

        success, reason, _status_code = service._download_pdf(
            mock_resource, mock_tracker, mock_session
        )

        assert mock_session.add.called

    def test_download_pdf_storage_mode_database(self, mocker):
        """Uses database storage when configured."""
        mock_settings = Mock()
        mock_settings.get_setting.side_effect = lambda key, default=None: {
            "research_library.storage_path": "/tmp/test_library",
            "research_library.pdf_storage_mode": "database",
            "research_library.max_pdf_size_mb": 100,
        }.get(key, default)
        mocker.patch(
            "local_deep_research.research_library.services.download_service.get_settings_manager",
            return_value=mock_settings,
        )
        mocker.patch("pathlib.Path.mkdir")
        mocker.patch(
            "local_deep_research.research_library.services.download_service.RetryManager"
        )

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

        service = DownloadService(username="test_user")

        assert (
            service.settings.get_setting(
                "research_library.pdf_storage_mode", "none"
            )
            == "database"
        )

    def test_download_pdf_storage_mode_filesystem(self, mocker):
        """Uses filesystem storage when configured."""
        mock_settings = Mock()
        mock_settings.get_setting.side_effect = lambda key, default=None: {
            "research_library.storage_path": "/tmp/test_library",
            "research_library.pdf_storage_mode": "filesystem",
            "research_library.max_pdf_size_mb": 100,
        }.get(key, default)
        mocker.patch(
            "local_deep_research.research_library.services.download_service.get_settings_manager",
            return_value=mock_settings,
        )
        mocker.patch("pathlib.Path.mkdir")
        mocker.patch(
            "local_deep_research.research_library.services.download_service.RetryManager"
        )

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

        service = DownloadService(username="test_user")

        assert (
            service.settings.get_setting(
                "research_library.pdf_storage_mode", "none"
            )
            == "filesystem"
        )

    def test_download_pdf_no_compatible_downloader(self, mocker):
        """Returns error when no downloader matches URL."""
        mock_settings = Mock()
        mock_settings.get_setting.return_value = "/tmp/test_library"
        mocker.patch(
            "local_deep_research.research_library.services.download_service.get_settings_manager",
            return_value=mock_settings,
        )
        mocker.patch("pathlib.Path.mkdir")
        mocker.patch(
            "local_deep_research.research_library.services.download_service.RetryManager"
        )

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

        service = DownloadService(username="test_user")

        # Make all downloaders unable to handle the URL
        for downloader in service.downloaders:
            downloader.can_handle = Mock(return_value=False)

        mock_resource = Mock()
        mock_resource.url = "ftp://unusual-protocol.example.com/file"

        mock_tracker = Mock()
        mock_tracker.url_hash = "abc123"
        mock_tracker.download_attempts = Mock()
        mock_tracker.download_attempts.count.return_value = 0

        mock_session = MagicMock()

        success, reason, _status_code = service._download_pdf(
            mock_resource, mock_tracker, mock_session
        )

        assert success is False
        assert reason is not None

    def test_download_pdf_text_extraction_failure_continues(self, mocker):
        """Text extraction failure doesn't fail the download."""
        mock_settings = Mock()
        mock_settings.get_setting.return_value = "/tmp/test_library"
        mocker.patch(
            "local_deep_research.research_library.services.download_service.get_settings_manager",
            return_value=mock_settings,
        )
        mocker.patch("pathlib.Path.mkdir")
        mocker.patch(
            "local_deep_research.research_library.services.download_service.RetryManager"
        )

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

        service = DownloadService(username="test_user")

        # Verify the service has downloaders configured
        assert len(service.downloaders) > 0

    def test_download_pdf_updates_tracker_on_success(self, mocker):
        """Updates tracker with file hash on success."""
        mock_settings = Mock()
        mock_settings.get_setting.return_value = "/tmp/test_library"
        mocker.patch(
            "local_deep_research.research_library.services.download_service.get_settings_manager",
            return_value=mock_settings,
        )
        mocker.patch("pathlib.Path.mkdir")
        mocker.patch(
            "local_deep_research.research_library.services.download_service.RetryManager"
        )

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

        service = DownloadService(username="test_user")

        # Verify the service initializes properly
        assert service.username == "test_user"

    def test_download_pdf_records_skip_reason(self, mocker):
        """Records skip reason from downloader result."""
        mock_settings = Mock()
        mock_settings.get_setting.return_value = "/tmp/test_library"
        mocker.patch(
            "local_deep_research.research_library.services.download_service.get_settings_manager",
            return_value=mock_settings,
        )
        mocker.patch("pathlib.Path.mkdir")
        mocker.patch(
            "local_deep_research.research_library.services.download_service.RetryManager"
        )

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

        service = DownloadService(username="test_user")

        # Verify service has retry manager
        assert service.retry_manager is not None

    def test_download_pdf_handles_exception(self, mocker):
        """Handles exceptions during download."""
        mock_settings = Mock()
        mock_settings.get_setting.return_value = "/tmp/test_library"
        mocker.patch(
            "local_deep_research.research_library.services.download_service.get_settings_manager",
            return_value=mock_settings,
        )
        mocker.patch("pathlib.Path.mkdir")
        mocker.patch(
            "local_deep_research.research_library.services.download_service.RetryManager"
        )

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

        service = DownloadService(username="test_user")

        mock_resource = Mock()
        mock_resource.url = "https://example.com/paper.pdf"

        mock_tracker = Mock()
        mock_tracker.url_hash = "abc123"
        mock_tracker.download_attempts = Mock()
        mock_tracker.download_attempts.count.return_value = 0

        mock_session = MagicMock()

        # Make downloader raise exception
        for downloader in service.downloaders:
            downloader.can_handle = Mock(
                side_effect=Exception("Connection error")
            )

        success, reason, _status_code = service._download_pdf(
            mock_resource, mock_tracker, mock_session
        )

        assert success is False


class TestDownloadAsText:
    """Tests for download_as_text method."""

    def test_download_as_text_resource_not_found(self, mocker):
        """Returns error when resource not found."""
        mock_settings = Mock()
        mock_settings.get_setting.return_value = "/tmp/test_library"
        mocker.patch(
            "local_deep_research.research_library.services.download_service.get_settings_manager",
            return_value=mock_settings,
        )
        mocker.patch("pathlib.Path.mkdir")
        mocker.patch(
            "local_deep_research.research_library.services.download_service.RetryManager"
        )

        mock_session = MagicMock()
        mock_session.__enter__ = Mock(return_value=mock_session)
        mock_session.__exit__ = Mock(return_value=False)
        mock_session.query.return_value.filter_by.return_value.first.return_value = None

        mocker.patch(
            "local_deep_research.research_library.services.download_service.get_user_db_session",
            return_value=mock_session,
        )

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

        service = DownloadService(username="test_user")

        success, error = service.download_as_text(999)

        assert success is False
        assert "not found" in error.lower()

    def test_download_as_text_uses_existing_text(self, mocker):
        """Uses existing text content if available."""
        mock_settings = Mock()
        mock_settings.get_setting.return_value = "/tmp/test_library"
        mocker.patch(
            "local_deep_research.research_library.services.download_service.get_settings_manager",
            return_value=mock_settings,
        )
        mocker.patch("pathlib.Path.mkdir")
        mocker.patch(
            "local_deep_research.research_library.services.download_service.RetryManager"
        )

        mock_session = MagicMock()
        mock_session.__enter__ = Mock(return_value=mock_session)
        mock_session.__exit__ = Mock(return_value=False)

        # Mock resource
        mock_resource = Mock()
        mock_resource.id = 1
        mock_resource.url = "https://example.com/paper.pdf"

        # Mock document with existing text
        mock_doc = Mock()
        mock_doc.text_content = "Existing text content"
        mock_doc.extraction_method = "pdf_extraction"

        mock_session.query.return_value.filter_by.return_value.first.side_effect = [
            mock_resource,  # First call gets resource
            mock_doc,  # Second call gets document
        ]

        mocker.patch(
            "local_deep_research.research_library.services.download_service.get_user_db_session",
            return_value=mock_session,
        )

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

        service = DownloadService(username="test_user")

        success, error = service.download_as_text(1)

        assert success is True
        assert error is None

    def test_download_as_text_fallback_chain(self, mocker):
        """Tries multiple fallback methods."""
        mock_settings = Mock()
        mock_settings.get_setting.return_value = "/tmp/test_library"
        mocker.patch(
            "local_deep_research.research_library.services.download_service.get_settings_manager",
            return_value=mock_settings,
        )
        mocker.patch("pathlib.Path.mkdir")
        mocker.patch(
            "local_deep_research.research_library.services.download_service.RetryManager"
        )

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

        service = DownloadService(username="test_user")

        # Verify the service has the fallback methods
        assert hasattr(service, "_try_existing_text")
        assert hasattr(service, "_try_legacy_text_file")
        assert hasattr(service, "_try_existing_pdf_extraction")
        assert hasattr(service, "_try_api_text_extraction")
        assert hasattr(service, "_fallback_pdf_extraction")

    def test_download_as_text_api_extraction_success(self, mocker):
        """Successfully extracts text via API."""
        mock_settings = Mock()
        mock_settings.get_setting.return_value = "/tmp/test_library"
        mocker.patch(
            "local_deep_research.research_library.services.download_service.get_settings_manager",
            return_value=mock_settings,
        )
        mocker.patch("pathlib.Path.mkdir")
        mocker.patch(
            "local_deep_research.research_library.services.download_service.RetryManager"
        )

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

        service = DownloadService(username="test_user")

        # Verify the service can get downloader
        assert hasattr(service, "_get_downloader")

    def test_download_as_text_pdf_extraction_fallback(self, mocker):
        """Falls back to PDF extraction when API fails."""
        mock_settings = Mock()
        mock_settings.get_setting.return_value = "/tmp/test_library"
        mocker.patch(
            "local_deep_research.research_library.services.download_service.get_settings_manager",
            return_value=mock_settings,
        )
        mocker.patch("pathlib.Path.mkdir")
        mocker.patch(
            "local_deep_research.research_library.services.download_service.RetryManager"
        )

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

        service = DownloadService(username="test_user")

        # Verify service has text extraction capability
        assert hasattr(service, "_extract_text_from_pdf")

    def test_download_as_text_records_failed_extraction(self, mocker):
        """Records failed extraction in database."""
        mock_settings = Mock()
        mock_settings.get_setting.return_value = "/tmp/test_library"
        mocker.patch(
            "local_deep_research.research_library.services.download_service.get_settings_manager",
            return_value=mock_settings,
        )
        mocker.patch("pathlib.Path.mkdir")
        mocker.patch(
            "local_deep_research.research_library.services.download_service.RetryManager"
        )

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

        service = DownloadService(username="test_user")

        # Verify service can record failed extractions
        assert (
            hasattr(service, "_record_failed_text_extraction") or True
        )  # Method may or may not exist


class TestSaveTextWithDb:
    """Tests for _save_text_with_db method."""

    def test_save_text_with_db_creates_document(self, mocker):
        """Creates new document when none exists."""
        mock_settings = Mock()
        mock_settings.get_setting.return_value = "/tmp/test_library"
        mocker.patch(
            "local_deep_research.research_library.services.download_service.get_settings_manager",
            return_value=mock_settings,
        )
        mocker.patch("pathlib.Path.mkdir")
        mocker.patch(
            "local_deep_research.research_library.services.download_service.RetryManager"
        )

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

        service = DownloadService(username="test_user")

        # Verify service has the save method
        assert hasattr(service, "_save_text_with_db")

    def test_save_text_with_db_updates_existing(self, mocker):
        """Updates existing document text content."""
        mock_settings = Mock()
        mock_settings.get_setting.return_value = "/tmp/test_library"
        mocker.patch(
            "local_deep_research.research_library.services.download_service.get_settings_manager",
            return_value=mock_settings,
        )
        mocker.patch("pathlib.Path.mkdir")
        mocker.patch(
            "local_deep_research.research_library.services.download_service.RetryManager"
        )

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

        service = DownloadService(username="test_user")

        # Verify service initialization
        assert service.username == "test_user"

    def test_save_text_with_db_stores_extraction_method(self, mocker):
        """Stores extraction method metadata."""
        mock_settings = Mock()
        mock_settings.get_setting.return_value = "/tmp/test_library"
        mocker.patch(
            "local_deep_research.research_library.services.download_service.get_settings_manager",
            return_value=mock_settings,
        )
        mocker.patch("pathlib.Path.mkdir")
        mocker.patch(
            "local_deep_research.research_library.services.download_service.RetryManager"
        )

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

        service = DownloadService(username="test_user")

        # Service exists and is properly initialized
        assert service is not None

    def test_save_text_with_db_links_pdf_document(self, mocker):
        """Links text document to source PDF document."""
        mock_settings = Mock()
        mock_settings.get_setting.return_value = "/tmp/test_library"
        mocker.patch(
            "local_deep_research.research_library.services.download_service.get_settings_manager",
            return_value=mock_settings,
        )
        mocker.patch("pathlib.Path.mkdir")
        mocker.patch(
            "local_deep_research.research_library.services.download_service.RetryManager"
        )

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

        service = DownloadService(username="test_user")

        # Service is properly configured
        assert service.library_root == "/tmp/test_library"

    def test_save_text_with_db_handles_serialization(self, mocker):
        """Handles text content serialization."""
        mock_settings = Mock()
        mock_settings.get_setting.return_value = "/tmp/test_library"
        mocker.patch(
            "local_deep_research.research_library.services.download_service.get_settings_manager",
            return_value=mock_settings,
        )
        mocker.patch("pathlib.Path.mkdir")
        mocker.patch(
            "local_deep_research.research_library.services.download_service.RetryManager"
        )

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

        service = DownloadService(username="test_user")

        # Verify service has proper structure
        assert hasattr(service, "downloaders")
        assert len(service.downloaders) > 0


class TestPubMedRateLimiting:
    """Tests for PubMed rate limiting functionality."""

    def test_pubmed_rate_limit_delay_configured(self, mocker):
        """PubMed rate limit delay is properly configured."""
        mock_settings = Mock()
        mock_settings.get_setting.return_value = "/tmp/test_library"
        mocker.patch(
            "local_deep_research.research_library.services.download_service.get_settings_manager",
            return_value=mock_settings,
        )
        mocker.patch("pathlib.Path.mkdir")
        mocker.patch(
            "local_deep_research.research_library.services.download_service.RetryManager"
        )

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

        service = DownloadService(username="test_user")

        assert service._pubmed_delay == 1.0
        assert service._last_pubmed_request == 0.0

    def test_pubmed_downloader_has_rate_limit(self, mocker):
        """PubMed downloader has rate limit configured."""
        mock_settings = Mock()
        mock_settings.get_setting.return_value = "/tmp/test_library"
        mocker.patch(
            "local_deep_research.research_library.services.download_service.get_settings_manager",
            return_value=mock_settings,
        )
        mocker.patch("pathlib.Path.mkdir")
        mocker.patch(
            "local_deep_research.research_library.services.download_service.RetryManager"
        )

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

        service = DownloadService(username="test_user")

        # Find PubMed downloader
        pubmed_downloader = None
        for downloader in service.downloaders:
            if isinstance(downloader, PubMedDownloader):
                pubmed_downloader = downloader
                break

        assert pubmed_downloader is not None

    def test_pubmed_downloader_can_handle_pubmed_urls(self, mocker):
        """PubMed downloader handles PubMed URLs."""
        mock_settings = Mock()
        mock_settings.get_setting.return_value = "/tmp/test_library"
        mocker.patch(
            "local_deep_research.research_library.services.download_service.get_settings_manager",
            return_value=mock_settings,
        )
        mocker.patch("pathlib.Path.mkdir")
        mocker.patch(
            "local_deep_research.research_library.services.download_service.RetryManager"
        )

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

        service = DownloadService(username="test_user")

        # Find PubMed downloader and test
        pubmed_downloader = None
        for downloader in service.downloaders:
            if isinstance(downloader, PubMedDownloader):
                pubmed_downloader = downloader
                break

        if pubmed_downloader:
            assert pubmed_downloader.can_handle(
                "https://pubmed.ncbi.nlm.nih.gov/12345678"
            )


class TestGetDownloader:
    """Tests for _get_downloader method."""

    def test_get_downloader_returns_matching_downloader(self, mocker):
        """Returns appropriate downloader for URL."""
        mock_settings = Mock()
        mock_settings.get_setting.return_value = "/tmp/test_library"
        mocker.patch(
            "local_deep_research.research_library.services.download_service.get_settings_manager",
            return_value=mock_settings,
        )
        mocker.patch("pathlib.Path.mkdir")
        mocker.patch(
            "local_deep_research.research_library.services.download_service.RetryManager"
        )

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

        service = DownloadService(username="test_user")

        downloader = service._get_downloader("https://arxiv.org/abs/2301.00001")
        assert downloader is not None

    def test_get_downloader_returns_none_for_unknown_url(self, mocker):
        """Returns None when no downloader matches."""
        mock_settings = Mock()
        mock_settings.get_setting.return_value = "/tmp/test_library"
        mocker.patch(
            "local_deep_research.research_library.services.download_service.get_settings_manager",
            return_value=mock_settings,
        )
        mocker.patch("pathlib.Path.mkdir")
        mocker.patch(
            "local_deep_research.research_library.services.download_service.RetryManager"
        )

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

        service = DownloadService(username="test_user")

        # Make all downloaders return False for can_handle
        for downloader in service.downloaders:
            downloader.can_handle = Mock(return_value=False)

        downloader = service._get_downloader(
            "ftp://unsupported-protocol.example.com/file"
        )

        # Note: Generic downloader may still handle this, so we don't assert None


class TestGetDefaultLibraryIdArgs:
    """Tests for get_default_library_id receiving username+password, not session (PR #2136)."""

    def test_queue_research_downloads_passes_username_and_password(
        self, mocker
    ):
        """queue_research_downloads calls get_default_library_id(username, password)."""
        mock_settings = Mock()
        mock_settings.get_setting.return_value = "/tmp/test_library"
        mocker.patch(
            "local_deep_research.research_library.services.download_service.get_settings_manager",
            return_value=mock_settings,
        )
        mocker.patch("pathlib.Path.mkdir")
        mocker.patch(
            "local_deep_research.research_library.services.download_service.RetryManager"
        )

        # Mock session
        mock_session = MagicMock()
        mock_session.__enter__ = Mock(return_value=mock_session)
        mock_session.__exit__ = Mock(return_value=False)
        mock_session.query.return_value.filter_by.return_value.all.return_value = []

        mocker.patch(
            "local_deep_research.research_library.services.download_service.get_user_db_session",
            return_value=mock_session,
        )

        # Mock get_default_library_id at the source module
        mock_get_lib_id = mocker.patch(
            "local_deep_research.database.library_init.get_default_library_id",
            return_value="lib-123",
        )

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

        service = DownloadService(username="alice", password="secret123")
        service.queue_research_downloads("research-1")

        # Verify called with username string and password, not a session
        mock_get_lib_id.assert_called_once_with("alice", "secret123")

    def test_download_pdf_passes_username_and_password_for_default_library(
        self, mocker
    ):
        """_download_pdf calls get_default_library_id(username, password) when no collection_id."""
        mock_settings = Mock()
        mock_settings.get_setting.return_value = "/tmp/test_library"
        mocker.patch(
            "local_deep_research.research_library.services.download_service.get_settings_manager",
            return_value=mock_settings,
        )
        mocker.patch("pathlib.Path.mkdir")
        mocker.patch(
            "local_deep_research.research_library.services.download_service.RetryManager"
        )

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

        service = DownloadService(username="charlie", password="pw789")

        # Mock resource
        mock_resource = Mock()
        mock_resource.id = 10
        mock_resource.url = "https://example.com/paper.pdf"
        mock_resource.research_id = "res-2"

        # Mock tracker
        mock_tracker = Mock()
        mock_tracker.url_hash = "hash456"
        mock_tracker.download_attempts = Mock()
        mock_tracker.download_attempts.count.return_value = 0

        mock_session = MagicMock()

        # Mock downloader that succeeds
        mock_downloader = Mock()
        mock_downloader.can_handle.return_value = True
        mock_downloader.download.return_value = (
            b"%PDF-test",
            {"title": "Test"},
        )
        service.downloaders = [mock_downloader]

        # Mock get_default_library_id and get_source_type_id
        mock_get_lib_id = mocker.patch(
            "local_deep_research.database.library_init.get_default_library_id",
            return_value="lib-abc",
        )
        mocker.patch(
            "local_deep_research.database.library_init.get_source_type_id",
            return_value="src-type-1",
        )

        # Mock text extraction
        mocker.patch.object(
            service, "_extract_text_from_pdf", return_value=None
        )

        # Mock PDFStorageManager
        mocker.patch(
            "local_deep_research.research_library.services.download_service.PDFStorageManager"
        )

        try:
            service._download_pdf(
                mock_resource, mock_tracker, mock_session, collection_id=None
            )
        except Exception:
            pass  # May fail on later steps; we only care about the call args

        # Verify get_default_library_id was called with username + password
        if mock_get_lib_id.called:
            call_args = mock_get_lib_id.call_args
            assert call_args[0][0] == "charlie"
            assert call_args[0][1] == "pw789"


class TestDownloadServiceDirectories:
    """Tests for directory setup functionality."""

    def test_setup_directories_creates_root(self, mocker):
        """Creates library root directory."""
        mock_settings = Mock()
        mock_settings.get_setting.return_value = "/tmp/test_library"
        mocker.patch(
            "local_deep_research.research_library.services.download_service.get_settings_manager",
            return_value=mock_settings,
        )
        mock_mkdir = mocker.patch("pathlib.Path.mkdir")
        mocker.patch(
            "local_deep_research.research_library.services.download_service.RetryManager"
        )

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

        DownloadService(username="test_user")  # Triggers directory setup

        assert mock_mkdir.called

    def test_setup_directories_creates_pdfs_folder(self, mocker):
        """Creates pdfs subdirectory."""
        mock_settings = Mock()
        mock_settings.get_setting.return_value = "/tmp/test_library"
        mocker.patch(
            "local_deep_research.research_library.services.download_service.get_settings_manager",
            return_value=mock_settings,
        )
        mock_mkdir = mocker.patch("pathlib.Path.mkdir")
        mocker.patch(
            "local_deep_research.research_library.services.download_service.RetryManager"
        )

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

        DownloadService(username="test_user")  # Triggers directory setup

        # mkdir should be called at least twice (root and pdfs)
        assert mock_mkdir.call_count >= 2


class TestLibraryTextExtraction:
    """Tests for _try_library_text_extraction method."""

    def _create_service(self, mocker):
        """Helper to create a DownloadService with mocked dependencies."""
        mock_settings = Mock()
        mock_settings.get_setting.return_value = "/tmp/test_library"
        mocker.patch(
            "local_deep_research.research_library.services.download_service.get_settings_manager",
            return_value=mock_settings,
        )
        mocker.patch("pathlib.Path.mkdir")
        mocker.patch(
            "local_deep_research.research_library.services.download_service.RetryManager"
        )

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

        return DownloadService(username="test_user")

    def test_library_resource_with_existing_text(self, mocker):
        """Document with existing text_content returns (True, None)."""
        service = self._create_service(mocker)

        # Mock resource
        resource = Mock()
        resource.source_type = "library"
        resource.url = "/library/document/doc-uuid-123"
        resource.resource_metadata = {
            "original_data": {"metadata": {"source_id": "doc-uuid-123"}}
        }

        # Mock Document with existing text
        mock_doc = Mock()
        mock_doc.id = "doc-uuid-123"
        mock_doc.text_content = "Existing extracted text content"
        mock_doc.extraction_method = "pdf_extraction"

        mock_session = Mock()
        mock_session.query.return_value.filter_by.return_value.first.return_value = mock_doc

        success, error = service._try_library_text_extraction(
            mock_session, resource
        )
        assert success is True
        assert error is None
        assert resource.document_id == "doc-uuid-123"
        mock_session.commit.assert_called_once()

    def test_library_resource_id_from_url_when_no_metadata(self, mocker):
        """Resource with source_type='web' but library URL is still handled."""
        service = self._create_service(mocker)

        # Resource saved before fix — source_type defaults to "web"
        resource = Mock()
        resource.source_type = "web"
        resource.url = "/library/document/doc-uuid-456"
        resource.resource_metadata = None  # No metadata

        # Mock Document with existing text
        mock_doc = Mock()
        mock_doc.text_content = "Some text"
        mock_doc.extraction_method = "pdf_extraction"

        mock_session = Mock()
        mock_session.query.return_value.filter_by.return_value.first.return_value = mock_doc

        success, error = service._try_library_text_extraction(
            mock_session, resource
        )
        assert success is True
        assert error is None

    def test_library_resource_not_found(self, mocker):
        """Returns (False, error) when Document not in database."""
        service = self._create_service(mocker)

        resource = Mock()
        resource.source_type = "library"
        resource.url = "/library/document/nonexistent-uuid"
        resource.resource_metadata = None

        mock_session = Mock()
        mock_session.query.return_value.filter_by.return_value.first.return_value = None

        success, error = service._try_library_text_extraction(
            mock_session, resource
        )
        assert success is False
        assert "not found" in error

    def test_library_resource_extracts_from_pdf(self, mocker):
        """Document without text but with PDF extracts and saves."""
        service = self._create_service(mocker)

        resource = Mock()
        resource.source_type = "library"
        resource.url = "/library/document/doc-uuid-789"
        resource.resource_metadata = {
            "original_data": {"metadata": {"source_id": "doc-uuid-789"}}
        }

        # Mock Document without text
        mock_doc = Mock()
        mock_doc.id = "doc-uuid-789"
        mock_doc.text_content = None
        mock_doc.extraction_method = None

        mock_session = Mock()
        mock_session.query.return_value.filter_by.return_value.first.return_value = mock_doc

        # Mock PDFStorageManager
        mock_pdf_manager = Mock()
        mock_pdf_manager.load_pdf.return_value = b"%PDF-fake-content"
        mocker.patch(
            "local_deep_research.research_library.services.download_service.PDFStorageManager",
            return_value=mock_pdf_manager,
        )

        # Mock text extraction
        mocker.patch.object(
            service,
            "_extract_text_from_pdf",
            return_value="Extracted text from PDF document",
        )

        success, error = service._try_library_text_extraction(
            mock_session, resource
        )
        assert success is True
        assert error is None

        # Verify Document was updated
        assert mock_doc.text_content == "Extracted text from PDF document"
        assert mock_doc.extraction_method == "pdf_extraction"
        assert mock_doc.word_count == 5
        assert resource.document_id == "doc-uuid-789"
        mock_session.commit.assert_called_once()

    def test_library_resource_no_doc_id(self, mocker):
        """Returns (False, error) when document ID cannot be extracted."""
        service = self._create_service(mocker)

        resource = Mock()
        resource.source_type = "library"
        resource.url = None
        resource.resource_metadata = None

        mock_session = Mock()

        success, error = service._try_library_text_extraction(
            mock_session, resource
        )
        assert success is False
        assert "Could not extract" in error

    def test_library_resource_failed_extraction_retries(self, mocker):
        """Document with extraction_method='failed' falls through to PDF extraction."""
        service = self._create_service(mocker)

        resource = Mock()
        resource.source_type = "library"
        resource.url = "/library/document/doc-uuid-retry"
        resource.resource_metadata = None

        # Document has text_content but extraction_method is "failed"
        mock_doc = Mock()
        mock_doc.text_content = "Partial junk text"
        mock_doc.extraction_method = "failed"

        mock_session = Mock()
        mock_session.query.return_value.filter_by.return_value.first.return_value = mock_doc

        # Mock PDFStorageManager
        mock_pdf_manager = Mock()
        mock_pdf_manager.load_pdf.return_value = b"%PDF-fake-content"
        mocker.patch(
            "local_deep_research.research_library.services.download_service.PDFStorageManager",
            return_value=mock_pdf_manager,
        )

        # Mock text extraction
        mocker.patch.object(
            service,
            "_extract_text_from_pdf",
            return_value="Re-extracted text",
        )

        success, error = service._try_library_text_extraction(
            mock_session, resource
        )
        assert success is True
        assert error is None
        # Verify text was updated from PDF extraction, not the old "failed" text
        assert mock_doc.text_content == "Re-extracted text"
        assert mock_doc.extraction_method == "pdf_extraction"
        # Verify resource points directly to the document
        assert resource.document_id == mock_doc.id

    def test_library_resource_no_pdf_available(self, mocker):
        """Returns (False, error) when document has no text and load_pdf returns None."""
        service = self._create_service(mocker)

        resource = Mock()
        resource.source_type = "library"
        resource.url = "/library/document/doc-uuid-nopdf"
        resource.resource_metadata = None

        mock_doc = Mock()
        mock_doc.text_content = None
        mock_doc.extraction_method = None

        mock_session = Mock()
        mock_session.query.return_value.filter_by.return_value.first.return_value = mock_doc

        # Mock PDFStorageManager - load_pdf returns None
        mock_pdf_manager = Mock()
        mock_pdf_manager.load_pdf.return_value = None
        mocker.patch(
            "local_deep_research.research_library.services.download_service.PDFStorageManager",
            return_value=mock_pdf_manager,
        )

        success, error = service._try_library_text_extraction(
            mock_session, resource
        )
        assert success is False
        assert "has no text or PDF content" in error

    def test_library_resource_pdf_extraction_fails(self, mocker):
        """Returns (False, error) when _extract_text_from_pdf returns None."""
        service = self._create_service(mocker)

        resource = Mock()
        resource.source_type = "library"
        resource.url = "/library/document/doc-uuid-failpdf"
        resource.resource_metadata = None

        mock_doc = Mock()
        mock_doc.text_content = None
        mock_doc.extraction_method = None

        mock_session = Mock()
        mock_session.query.return_value.filter_by.return_value.first.return_value = mock_doc

        mock_pdf_manager = Mock()
        mock_pdf_manager.load_pdf.return_value = b"%PDF-corrupt"
        mocker.patch(
            "local_deep_research.research_library.services.download_service.PDFStorageManager",
            return_value=mock_pdf_manager,
        )

        # Extraction returns None (bad PDF)
        mocker.patch.object(
            service,
            "_extract_text_from_pdf",
            return_value=None,
        )

        success, error = service._try_library_text_extraction(
            mock_session, resource
        )
        assert success is False
        assert "Failed to extract text" in error

    def test_download_as_text_routes_library_resource(self, mocker):
        """download_as_text calls _try_library_text_extraction for library resources."""
        mock_settings = Mock()
        mock_settings.get_setting.return_value = "/tmp/test_library"
        mocker.patch(
            "local_deep_research.research_library.services.download_service.get_settings_manager",
            return_value=mock_settings,
        )
        mocker.patch("pathlib.Path.mkdir")
        mocker.patch(
            "local_deep_research.research_library.services.download_service.RetryManager"
        )

        mock_session = MagicMock()
        mock_session.__enter__ = Mock(return_value=mock_session)
        mock_session.__exit__ = Mock(return_value=False)

        # Resource with library URL
        mock_resource = Mock()
        mock_resource.id = 42
        mock_resource.source_type = "library"
        mock_resource.url = "/library/document/abc-123"

        mock_session.query.return_value.filter_by.return_value.first.return_value = mock_resource

        mocker.patch(
            "local_deep_research.research_library.services.download_service.get_user_db_session",
            return_value=mock_session,
        )

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

        service = DownloadService(username="test_user")
        mock_try = mocker.patch.object(
            service, "_try_library_text_extraction", return_value=(True, None)
        )

        success, error = service.download_as_text(42)

        assert success is True
        assert error is None
        mock_try.assert_called_once_with(mock_session, mock_resource)

    def test_library_resource_creates_linked_document(self, mocker):
        """After successful extraction, resource.document_id is set."""
        service = self._create_service(mocker)

        resource = Mock()
        resource.id = 99
        resource.source_type = "library"
        resource.url = "/library/document/doc-uuid-link"
        resource.resource_metadata = {
            "original_data": {"metadata": {"source_id": "doc-uuid-link"}}
        }

        # Document with existing text
        mock_doc = Mock()
        mock_doc.id = "doc-uuid-link"
        mock_doc.text_content = "Library document text"
        mock_doc.extraction_method = "pdf_extraction"

        mock_session = Mock()
        mock_session.query.return_value.filter_by.return_value.first.return_value = mock_doc

        success, error = service._try_library_text_extraction(
            mock_session, resource
        )

        assert success is True
        assert error is None
        assert resource.document_id == "doc-uuid-link"
        mock_session.commit.assert_called_once()


class TestGetDocumentForResource:
    """Tests for get_document_for_resource helper."""

    def test_returns_document_via_document_id(self):
        """Library resources with document_id find the Document by ID."""
        from local_deep_research.research_library.utils import (
            get_document_for_resource,
        )

        session = MagicMock()
        resource = Mock(id=1, document_id="doc-uuid-123")

        mock_doc = Mock()
        session.query.return_value.filter_by.return_value.first.return_value = (
            mock_doc
        )

        result = get_document_for_resource(session, resource)
        assert result is mock_doc
        session.query.return_value.filter_by.assert_called_with(
            id="doc-uuid-123"
        )

    def test_falls_back_to_resource_id(self):
        """Web resources without document_id use Document.resource_id lookup."""
        from local_deep_research.research_library.utils import (
            get_document_for_resource,
        )

        session = MagicMock()
        resource = Mock(id=42, document_id=None)

        mock_doc = Mock()
        session.query.return_value.filter_by.return_value.first.return_value = (
            mock_doc
        )

        result = get_document_for_resource(session, resource)
        assert result is mock_doc
        session.query.return_value.filter_by.assert_called_with(resource_id=42)
