"""Comprehensive tests for DirectPDFDownloader to improve coverage.

Covers can_handle URL patterns, download with TEXT/PDF content types,
download_with_result success/failure paths, and error handling.
"""

from unittest.mock import patch

import pytest
import requests

from local_deep_research.research_library.downloaders.base import (
    ContentType,
    DownloadResult,
)
from local_deep_research.research_library.downloaders.direct_pdf import (
    DirectPDFDownloader,
)


# ============== Fixtures ==============


@pytest.fixture
def downloader(create_downloader_with_mock_session):
    """Create a DirectPDFDownloader with mocked session and rate tracker."""
    return create_downloader_with_mock_session(DirectPDFDownloader)


@pytest.fixture
def mock_session(downloader):
    """Shortcut to the mocked session on the downloader."""
    return downloader.session


# ============== can_handle ==============


class TestCanHandle:
    """Tests for URL pattern matching in can_handle."""

    def test_pdf_extension_lowercase(self, downloader):
        assert downloader.can_handle("https://example.com/paper.pdf")

    def test_pdf_extension_uppercase(self, downloader):
        assert downloader.can_handle("https://example.com/paper.PDF")

    def test_pdf_extension_mixed_case(self, downloader):
        assert downloader.can_handle("https://example.com/paper.Pdf")

    def test_pdf_with_path_segments(self, downloader):
        assert downloader.can_handle("https://example.com/files/2024/paper.pdf")

    def test_pdf_with_query_string(self, downloader):
        assert downloader.can_handle("https://example.com/paper.pdf?token=abc")

    def test_pdf_with_fragment(self, downloader):
        assert downloader.can_handle("https://example.com/paper.pdf#page=5")

    def test_pdf_in_path_segment(self, downloader):
        """URLs with /pdf/ in the path should be handled."""
        assert downloader.can_handle("https://arxiv.org/pdf/2301.12345")

    def test_type_pdf_query_param(self, downloader):
        assert downloader.can_handle("https://example.com/doc?type=pdf")

    def test_format_pdf_query_param(self, downloader):
        assert downloader.can_handle("https://example.com/doc?format=pdf")

    def test_rejects_html_url(self, downloader):
        assert not downloader.can_handle("https://example.com/page.html")

    def test_rejects_plain_url(self, downloader):
        assert not downloader.can_handle("https://example.com/article")

    def test_rejects_docx(self, downloader):
        assert not downloader.can_handle("https://example.com/file.docx")

    def test_rejects_empty_string(self, downloader):
        assert not downloader.can_handle("")

    def test_rejects_url_with_pdf_in_domain(self, downloader):
        """pdf in domain name (not path) should not match unless path also matches."""
        assert not downloader.can_handle("https://pdfreader.com/view")

    def test_handles_url_parsing_error(self, downloader):
        """Malformed URLs should return False, not raise."""
        # urlparse is very permissive; test that the except branch works
        with patch(
            "local_deep_research.research_library.downloaders.direct_pdf.urlparse",
            side_effect=ValueError("bad url"),
        ):
            assert not downloader.can_handle("https://example.com/paper.pdf")


# ============== download (ContentType.PDF, default) ==============


class TestDownloadPDF:
    """Tests for download() with default PDF content type."""

    def test_successful_download(
        self, downloader, mock_session, mock_pdf_content
    ):
        result = downloader.download("https://example.com/paper.pdf")
        assert result is not None
        assert result == mock_pdf_content

    def test_returns_none_on_network_error(self, downloader, mock_session):
        mock_session.get.side_effect = requests.exceptions.ConnectionError(
            "refused"
        )
        result = downloader.download("https://example.com/paper.pdf")
        assert result is None

    def test_returns_none_on_timeout(self, downloader, mock_session):
        mock_session.get.side_effect = requests.exceptions.Timeout("timed out")
        result = downloader.download("https://example.com/paper.pdf")
        assert result is None

    def test_returns_none_on_404(self, downloader, mock_session, mocker):
        mock_response = mocker.Mock()
        mock_response.status_code = 404
        mock_response.headers = {"content-type": "text/html"}
        mock_response.content = b"Not Found"
        mock_session.get.return_value = mock_response
        result = downloader.download("https://example.com/paper.pdf")
        assert result is None

    def test_returns_none_on_non_pdf_content(
        self, downloader, mock_session, mocker
    ):
        """200 response but content is HTML, not PDF."""
        mock_response = mocker.Mock()
        mock_response.status_code = 200
        mock_response.headers = {"content-type": "text/html"}
        mock_response.content = b"<html>Not a PDF</html>"
        mock_session.get.return_value = mock_response
        result = downloader.download("https://example.com/paper.pdf")
        assert result is None


# ============== download (ContentType.TEXT) ==============


class TestDownloadText:
    """Tests for download() with TEXT content type."""

    def test_text_extraction_success(
        self, downloader, mock_session, mock_pdf_content
    ):
        """Should download PDF, extract text, and return utf-8 bytes."""
        with patch.object(
            DirectPDFDownloader,
            "extract_text_from_pdf",
            return_value="Extracted text from paper",
        ):
            result = downloader.download(
                "https://example.com/paper.pdf", content_type=ContentType.TEXT
            )
        assert result is not None
        assert result == b"Extracted text from paper"

    def test_text_extraction_returns_none_when_no_text(
        self, downloader, mock_session, mock_pdf_content
    ):
        """Should return None when text extraction yields nothing."""
        with patch.object(
            DirectPDFDownloader, "extract_text_from_pdf", return_value=None
        ):
            result = downloader.download(
                "https://example.com/paper.pdf", content_type=ContentType.TEXT
            )
        assert result is None

    def test_text_extraction_returns_none_when_download_fails(
        self, downloader, mock_session
    ):
        """Should return None when the PDF download itself fails."""
        mock_session.get.side_effect = requests.exceptions.ConnectionError(
            "fail"
        )
        result = downloader.download(
            "https://example.com/paper.pdf", content_type=ContentType.TEXT
        )
        assert result is None


# ============== download_with_result (ContentType.PDF) ==============


class TestDownloadWithResultPDF:
    """Tests for download_with_result() with PDF content type."""

    def test_success(self, downloader, mock_session, mock_pdf_content):
        result = downloader.download_with_result(
            "https://example.com/paper.pdf"
        )
        assert isinstance(result, DownloadResult)
        assert result.is_success is True
        assert result.content == mock_pdf_content

    def test_failure_with_404_head(self, downloader, mock_session, mocker):
        """When download fails, HEAD request returns 404."""
        # Make GET return non-PDF so _download_pdf returns None
        fail_response = mocker.Mock()
        fail_response.status_code = 403
        fail_response.headers = {"content-type": "text/html"}
        fail_response.content = b"Forbidden"
        mock_session.get.return_value = fail_response

        head_response = mocker.Mock()
        head_response.status_code = 404
        mock_session.head.return_value = head_response

        result = downloader.download_with_result(
            "https://example.com/paper.pdf"
        )
        assert result.is_success is False
        assert "404" in result.skip_reason

    def test_failure_with_403_head(self, downloader, mock_session, mocker):
        """When download fails, HEAD request returns 403."""
        fail_response = mocker.Mock()
        fail_response.status_code = 500
        fail_response.headers = {"content-type": "text/html"}
        fail_response.content = b"Error"
        mock_session.get.return_value = fail_response

        head_response = mocker.Mock()
        head_response.status_code = 403
        mock_session.head.return_value = head_response

        result = downloader.download_with_result(
            "https://example.com/paper.pdf"
        )
        assert result.is_success is False
        assert "403" in result.skip_reason
        assert "authentication" in result.skip_reason.lower()

    def test_failure_with_500_head(self, downloader, mock_session, mocker):
        """When download fails, HEAD request returns 500."""
        fail_response = mocker.Mock()
        fail_response.status_code = 500
        fail_response.headers = {"content-type": "text/html"}
        fail_response.content = b"Error"
        mock_session.get.return_value = fail_response

        head_response = mocker.Mock()
        head_response.status_code = 500
        mock_session.head.return_value = head_response

        result = downloader.download_with_result(
            "https://example.com/paper.pdf"
        )
        assert result.is_success is False
        assert "500" in result.skip_reason
        assert "server error" in result.skip_reason.lower()

    def test_failure_with_other_status_head(
        self, downloader, mock_session, mocker
    ):
        """When download fails, HEAD request returns unexpected status like 301."""
        fail_response = mocker.Mock()
        fail_response.status_code = 500
        fail_response.headers = {"content-type": "text/html"}
        fail_response.content = b"Error"
        mock_session.get.return_value = fail_response

        head_response = mocker.Mock()
        head_response.status_code = 301
        mock_session.head.return_value = head_response

        result = downloader.download_with_result(
            "https://example.com/paper.pdf"
        )
        assert result.is_success is False
        assert "301" in result.skip_reason

    def test_failure_head_request_raises(
        self, downloader, mock_session, mocker
    ):
        """When download fails and HEAD also raises, return generic skip reason."""
        fail_response = mocker.Mock()
        fail_response.status_code = 500
        fail_response.headers = {"content-type": "text/html"}
        fail_response.content = b"Error"
        mock_session.get.return_value = fail_response
        mock_session.head.side_effect = requests.exceptions.ConnectionError(
            "refused"
        )

        result = downloader.download_with_result(
            "https://example.com/paper.pdf"
        )
        assert result.is_success is False
        assert "failed" in result.skip_reason.lower()


# ============== download_with_result (ContentType.TEXT) ==============


class TestDownloadWithResultText:
    """Tests for download_with_result() with TEXT content type."""

    def test_success(self, downloader, mock_session, mock_pdf_content):
        with patch.object(
            DirectPDFDownloader,
            "extract_text_from_pdf",
            return_value="Hello world",
        ):
            result = downloader.download_with_result(
                "https://example.com/paper.pdf", content_type=ContentType.TEXT
            )
        assert result.is_success is True
        assert result.content == b"Hello world"

    def test_text_extraction_fails(
        self, downloader, mock_session, mock_pdf_content
    ):
        """PDF downloads but text extraction returns None."""
        with patch.object(
            DirectPDFDownloader, "extract_text_from_pdf", return_value=None
        ):
            result = downloader.download_with_result(
                "https://example.com/paper.pdf", content_type=ContentType.TEXT
            )
        assert result.is_success is False
        assert "text extraction failed" in result.skip_reason.lower()

    def test_pdf_download_fails(self, downloader, mock_session):
        """PDF download itself fails."""
        mock_session.get.side_effect = requests.exceptions.ConnectionError(
            "fail"
        )
        result = downloader.download_with_result(
            "https://example.com/paper.pdf", content_type=ContentType.TEXT
        )
        assert result.is_success is False
        assert "could not download" in result.skip_reason.lower()


# ============== _download_pdf (internal, delegates to super) ==============


class TestInternalDownloadPdf:
    """Tests for the _download_pdf override that delegates to super()."""

    def test_delegates_to_base(
        self, downloader, mock_session, mock_pdf_content
    ):
        """_download_pdf should delegate to BaseDownloader._download_pdf."""
        result = downloader._download_pdf("https://example.com/paper.pdf")
        assert result == mock_pdf_content
        mock_session.get.assert_called()
