"""Comprehensive tests for BioRxivDownloader to increase coverage."""

from unittest.mock import Mock, patch

import pytest
import requests

from local_deep_research.research_library.downloaders.base import (
    ContentType,
)
from local_deep_research.research_library.downloaders.biorxiv import (
    BioRxivDownloader,
)


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


@pytest.fixture
def raw_downloader():
    """Create a BioRxivDownloader without mocked internals (for pure logic tests)."""
    d = BioRxivDownloader.__new__(BioRxivDownloader)
    d.timeout = 30
    d.session = Mock()
    d.session.headers = {"User-Agent": "Test"}
    d.rate_tracker = Mock()
    d.rate_tracker.apply_rate_limit.return_value = 0.0
    d.rate_tracker.record_outcome.return_value = None
    return d


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


class TestCanHandle:
    """Tests for BioRxivDownloader.can_handle."""

    def test_biorxiv_www(self, raw_downloader):
        assert (
            raw_downloader.can_handle(
                "https://www.biorxiv.org/content/10.1101/2024.01.01.123456v1"
            )
            is True
        )

    def test_biorxiv_bare(self, raw_downloader):
        assert (
            raw_downloader.can_handle(
                "https://biorxiv.org/content/10.1101/2024.01.01.123456v1"
            )
            is True
        )

    def test_medrxiv_www(self, raw_downloader):
        assert (
            raw_downloader.can_handle(
                "https://www.medrxiv.org/content/10.1101/2024.01.01.123456v1"
            )
            is True
        )

    def test_medrxiv_bare(self, raw_downloader):
        assert (
            raw_downloader.can_handle(
                "https://medrxiv.org/content/10.1101/2024.01.01.123456v1"
            )
            is True
        )

    def test_biorxiv_subdomain(self, raw_downloader):
        """Subdomains like submit.biorxiv.org should match."""
        assert (
            raw_downloader.can_handle("https://submit.biorxiv.org/something")
            is True
        )

    def test_medrxiv_subdomain(self, raw_downloader):
        assert (
            raw_downloader.can_handle("https://api.medrxiv.org/something")
            is True
        )

    def test_rejects_arxiv(self, raw_downloader):
        assert (
            raw_downloader.can_handle("https://arxiv.org/abs/2301.12345")
            is False
        )

    def test_rejects_pubmed(self, raw_downloader):
        assert (
            raw_downloader.can_handle("https://pubmed.ncbi.nlm.nih.gov/12345")
            is False
        )

    def test_rejects_example(self, raw_downloader):
        assert (
            raw_downloader.can_handle("https://example.com/paper.pdf") is False
        )

    def test_rejects_empty(self, raw_downloader):
        assert raw_downloader.can_handle("") is False

    def test_rejects_garbage(self, raw_downloader):
        assert raw_downloader.can_handle("not-a-url-at-all") is False

    def test_rejects_none(self, raw_downloader):
        """None should return False (not raise)."""
        # urlparse(None) raises in some Python versions; the try/except catches it
        try:
            result = raw_downloader.can_handle(None)
            assert result is False
        except (TypeError, AttributeError):
            pass  # acceptable

    def test_rejects_similar_domain(self, raw_downloader):
        """fakebiorxiv.org should not match."""
        assert (
            raw_downloader.can_handle("https://fakebiorxiv.org/content/foo")
            is False
        )

    def test_http_scheme(self, raw_downloader):
        assert (
            raw_downloader.can_handle(
                "http://www.biorxiv.org/content/10.1101/foo"
            )
            is True
        )


# =====================================================================
# _convert_to_pdf_url
# =====================================================================


class TestConvertToPdfUrl:
    """Tests for BioRxivDownloader._convert_to_pdf_url."""

    def test_basic_content_url(self, raw_downloader):
        url = "https://www.biorxiv.org/content/10.1101/2024.01.15.575123v1"
        result = raw_downloader._convert_to_pdf_url(url)
        assert (
            result
            == "https://www.biorxiv.org/content/10.1101/2024.01.15.575123v1.full.pdf"
        )

    def test_medrxiv_url(self, raw_downloader):
        url = "https://www.medrxiv.org/content/10.1101/2024.02.20.12345678v2"
        result = raw_downloader._convert_to_pdf_url(url)
        assert (
            result
            == "https://www.medrxiv.org/content/10.1101/2024.02.20.12345678v2.full.pdf"
        )

    def test_already_full_pdf(self, raw_downloader):
        """URL ending with .full.pdf should not get double-suffixed."""
        url = "https://www.biorxiv.org/content/10.1101/2024.01.15.575123v1.full.pdf"
        result = raw_downloader._convert_to_pdf_url(url)
        assert result.endswith(".full.pdf")
        assert ".full.full.pdf" not in result

    def test_already_full_suffix(self, raw_downloader):
        """URL ending with .full should be cleaned then re-suffixed."""
        url = "https://www.biorxiv.org/content/10.1101/2024.01.15.575123v1.full"
        result = raw_downloader._convert_to_pdf_url(url)
        assert result.endswith(".full.pdf")
        assert ".full.full" not in result

    def test_url_with_trailing_slash(self, raw_downloader):
        url = "https://www.biorxiv.org/content/10.1101/2024.01.15.575123v1/"
        result = raw_downloader._convert_to_pdf_url(url)
        assert result.endswith(".full.pdf")
        assert "//" not in result.split("://", 1)[1]  # no double slash in path

    def test_early_url_converted(self, raw_downloader):
        """URLs with /content/early/ should have 'early/' removed."""
        url = (
            "https://www.biorxiv.org/content/early/2024/01/15/2024.01.15.575123"
        )
        result = raw_downloader._convert_to_pdf_url(url)
        assert "/content/early/" not in result
        assert "/content/" in result

    def test_standalone_pdf_extension(self, raw_downloader):
        """URL already ending in .pdf (but not .full.pdf) should be returned as-is."""
        url = "https://www.biorxiv.org/content/10.1101/2024.01.15.575123v1.pdf"
        result = raw_downloader._convert_to_pdf_url(url)
        assert result.endswith(".pdf")

    def test_no_version_suffix(self, raw_downloader):
        url = "https://www.biorxiv.org/content/10.1101/2024.01.15.575123"
        result = raw_downloader._convert_to_pdf_url(url)
        assert result.endswith(".full.pdf")


# =====================================================================
# _fetch_abstract_from_page
# =====================================================================


class TestFetchAbstractFromPage:
    """Tests for BioRxivDownloader._fetch_abstract_from_page."""

    def test_extracts_all_metadata(
        self, raw_downloader, mock_biorxiv_page_html
    ):
        """Should extract title, authors, and abstract from DC meta tags."""
        mock_resp = Mock()
        mock_resp.status_code = 200
        mock_resp.text = mock_biorxiv_page_html
        raw_downloader.session.get.return_value = mock_resp

        result = raw_downloader._fetch_abstract_from_page(
            "https://www.biorxiv.org/content/10.1101/test"
        )
        assert result is not None
        assert "Novel CRISPR Gene Editing Technique" in result
        assert "Alice Johnson" in result
        assert "CRISPR-Cas9" in result

    def test_extracts_title_only(self, raw_downloader):
        """Should return title even if abstract is missing."""
        html = '<html><head><meta name="DC.Title" content="Some Title"></head></html>'
        mock_resp = Mock()
        mock_resp.status_code = 200
        mock_resp.text = html
        raw_downloader.session.get.return_value = mock_resp

        result = raw_downloader._fetch_abstract_from_page(
            "https://www.biorxiv.org/content/10.1101/test"
        )
        assert result is not None
        assert "Some Title" in result

    def test_returns_none_for_empty_html(self, raw_downloader):
        """HTML with no DC meta tags returns None."""
        mock_resp = Mock()
        mock_resp.status_code = 200
        mock_resp.text = "<html><head></head><body>nothing here</body></html>"
        raw_downloader.session.get.return_value = mock_resp

        result = raw_downloader._fetch_abstract_from_page(
            "https://www.biorxiv.org/content/10.1101/test"
        )
        assert result is None

    def test_returns_none_on_404(self, raw_downloader):
        mock_resp = Mock()
        mock_resp.status_code = 404
        mock_resp.text = "Not found"
        raw_downloader.session.get.return_value = mock_resp

        result = raw_downloader._fetch_abstract_from_page(
            "https://www.biorxiv.org/content/10.1101/test"
        )
        assert result is None

    def test_returns_none_on_network_error(self, raw_downloader):
        raw_downloader.session.get.side_effect = requests.ConnectionError(
            "fail"
        )

        result = raw_downloader._fetch_abstract_from_page(
            "https://www.biorxiv.org/content/10.1101/test"
        )
        assert result is None

    def test_returns_none_on_timeout(self, raw_downloader):
        raw_downloader.session.get.side_effect = requests.Timeout("timeout")

        result = raw_downloader._fetch_abstract_from_page(
            "https://www.biorxiv.org/content/10.1101/test"
        )
        assert result is None

    def test_html_entity_decoding(self, raw_downloader):
        """HTML entities in abstract should be decoded."""
        html = (
            "<html><head>"
            '<meta name="DC.Description" content="A &amp; B are &lt;great&gt; with &quot;quotes&quot; and &#39;apos&#39;">'
            "</head></html>"
        )
        mock_resp = Mock()
        mock_resp.status_code = 200
        mock_resp.text = html
        raw_downloader.session.get.return_value = mock_resp

        result = raw_downloader._fetch_abstract_from_page(
            "https://www.biorxiv.org/content/10.1101/test"
        )
        assert result is not None
        assert "A & B" in result
        assert "<great>" in result
        assert '"quotes"' in result
        assert "'apos'" in result


# =====================================================================
# download (PDF and TEXT modes)
# =====================================================================


class TestDownload:
    """Tests for BioRxivDownloader.download."""

    def test_download_pdf_success(self, raw_downloader, mock_pdf_content):
        """PDF download delegates to _download_pdf."""
        raw_downloader._download_pdf = Mock(return_value=mock_pdf_content)
        result = raw_downloader.download(
            "https://www.biorxiv.org/content/10.1101/test"
        )
        assert result == mock_pdf_content
        raw_downloader._download_pdf.assert_called_once()

    def test_download_text_success(self, raw_downloader):
        """TEXT download delegates to _download_text."""
        raw_downloader._download_text = Mock(return_value=b"some text")
        result = raw_downloader.download(
            "https://www.biorxiv.org/content/10.1101/test", ContentType.TEXT
        )
        assert result == b"some text"
        raw_downloader._download_text.assert_called_once()

    def test_download_pdf_failure(self, raw_downloader):
        raw_downloader._download_pdf = Mock(return_value=None)
        result = raw_downloader.download(
            "https://www.biorxiv.org/content/10.1101/test"
        )
        assert result is None

    def test_download_text_failure(self, raw_downloader):
        raw_downloader._download_text = Mock(return_value=None)
        result = raw_downloader.download(
            "https://www.biorxiv.org/content/10.1101/test", ContentType.TEXT
        )
        assert result is None


# =====================================================================
# _download_pdf (internal)
# =====================================================================


class TestDownloadPdfInternal:
    """Tests for BioRxivDownloader._download_pdf."""

    def test_converts_url_and_delegates(self, raw_downloader, mock_pdf_content):
        """Should convert URL then call super()._download_pdf."""
        with patch.object(
            BioRxivDownloader.__bases__[0],
            "_download_pdf",
            return_value=mock_pdf_content,
        ) as mock_super:
            result = raw_downloader._download_pdf(
                "https://www.biorxiv.org/content/10.1101/2024.01.15.575123v1"
            )
        assert result == mock_pdf_content
        # super()._download_pdf should have been called with .full.pdf URL
        called_url = mock_super.call_args[0][0]
        assert called_url.endswith(".full.pdf")

    def test_returns_none_when_convert_fails(self, raw_downloader):
        """If _convert_to_pdf_url returned None, should return None."""
        raw_downloader._convert_to_pdf_url = Mock(return_value=None)
        result = raw_downloader._download_pdf("https://example.com/bad")
        assert result is None


# =====================================================================
# _download_text (internal)
# =====================================================================


class TestDownloadTextInternal:
    """Tests for BioRxivDownloader._download_text."""

    def test_returns_abstract_when_available(self, raw_downloader):
        raw_downloader._fetch_abstract_from_page = Mock(
            return_value="Title: Foo\nAbstract: Bar"
        )
        result = raw_downloader._download_text(
            "https://www.biorxiv.org/content/10.1101/test"
        )
        assert result == b"Title: Foo\nAbstract: Bar"

    def test_falls_back_to_pdf_extraction(
        self, raw_downloader, mock_pdf_content
    ):
        """When abstract fetch fails, should try PDF text extraction."""
        raw_downloader._fetch_abstract_from_page = Mock(return_value=None)
        raw_downloader._download_pdf = Mock(return_value=mock_pdf_content)
        raw_downloader.extract_text_from_pdf = Mock(
            return_value="extracted text"
        )

        result = raw_downloader._download_text(
            "https://www.biorxiv.org/content/10.1101/test"
        )
        assert result == b"extracted text"

    def test_returns_none_when_all_fail(self, raw_downloader):
        raw_downloader._fetch_abstract_from_page = Mock(return_value=None)
        raw_downloader._download_pdf = Mock(return_value=None)

        result = raw_downloader._download_text(
            "https://www.biorxiv.org/content/10.1101/test"
        )
        assert result is None

    def test_returns_none_when_pdf_downloaded_but_extraction_fails(
        self, raw_downloader, mock_pdf_content
    ):
        raw_downloader._fetch_abstract_from_page = Mock(return_value=None)
        raw_downloader._download_pdf = Mock(return_value=mock_pdf_content)
        raw_downloader.extract_text_from_pdf = Mock(return_value=None)

        result = raw_downloader._download_text(
            "https://www.biorxiv.org/content/10.1101/test"
        )
        assert result is None


# =====================================================================
# download_with_result (TEXT mode)
# =====================================================================


class TestDownloadWithResultText:
    """Tests for download_with_result with ContentType.TEXT."""

    def test_success_from_abstract(self, raw_downloader):
        raw_downloader._fetch_abstract_from_page = Mock(
            return_value="Title: X\nAbstract: Y"
        )

        result = raw_downloader.download_with_result(
            "https://www.biorxiv.org/content/10.1101/test", ContentType.TEXT
        )
        assert result.is_success is True
        assert result.content == b"Title: X\nAbstract: Y"

    def test_fallback_to_pdf_extraction(self, raw_downloader, mock_pdf_content):
        raw_downloader._fetch_abstract_from_page = Mock(return_value=None)
        raw_downloader._download_pdf = Mock(return_value=mock_pdf_content)
        raw_downloader.extract_text_from_pdf = Mock(
            return_value="pdf text here"
        )

        result = raw_downloader.download_with_result(
            "https://www.biorxiv.org/content/10.1101/test", ContentType.TEXT
        )
        assert result.is_success is True
        assert result.content == b"pdf text here"

    def test_failure_returns_skip_reason(self, raw_downloader):
        raw_downloader._fetch_abstract_from_page = Mock(return_value=None)
        raw_downloader._download_pdf = Mock(return_value=None)

        result = raw_downloader.download_with_result(
            "https://www.biorxiv.org/content/10.1101/test", ContentType.TEXT
        )
        assert result.is_success is False
        assert result.skip_reason is not None
        assert (
            "text" in result.skip_reason.lower()
            or "bioRxiv" in result.skip_reason
        )

    def test_failure_when_pdf_downloaded_but_no_text(
        self, raw_downloader, mock_pdf_content
    ):
        raw_downloader._fetch_abstract_from_page = Mock(return_value=None)
        raw_downloader._download_pdf = Mock(return_value=mock_pdf_content)
        raw_downloader.extract_text_from_pdf = Mock(return_value=None)

        result = raw_downloader.download_with_result(
            "https://www.biorxiv.org/content/10.1101/test", ContentType.TEXT
        )
        assert result.is_success is False
        assert result.skip_reason is not None


# =====================================================================
# download_with_result (PDF mode)
# =====================================================================


class TestDownloadWithResultPdf:
    """Tests for download_with_result with ContentType.PDF (default)."""

    def test_success(self, raw_downloader, mock_pdf_content):
        with patch.object(
            BioRxivDownloader.__bases__[0],
            "_download_pdf",
            return_value=mock_pdf_content,
        ):
            result = raw_downloader.download_with_result(
                "https://www.biorxiv.org/content/10.1101/2024.01.15.575123v1"
            )
        assert result.is_success is True
        assert result.content == mock_pdf_content

    def test_invalid_url_returns_skip(self, raw_downloader):
        raw_downloader._convert_to_pdf_url = Mock(return_value=None)

        result = raw_downloader.download_with_result("https://example.com/bad")
        assert result.is_success is False
        assert "Invalid" in result.skip_reason

    def test_pdf_download_failure_checks_head_404(self, raw_downloader):
        """When PDF download fails, should HEAD the URL and report 404."""
        with patch.object(
            BioRxivDownloader.__bases__[0], "_download_pdf", return_value=None
        ):
            head_resp = Mock()
            head_resp.status_code = 404
            raw_downloader.session.head.return_value = head_resp

            result = raw_downloader.download_with_result(
                "https://www.biorxiv.org/content/10.1101/2024.01.15.575123v1"
            )
        assert result.is_success is False
        assert "not found" in result.skip_reason.lower()

    def test_pdf_download_failure_checks_head_500(self, raw_downloader):
        """When PDF fails and HEAD returns 5xx, should report server issue."""
        with patch.object(
            BioRxivDownloader.__bases__[0], "_download_pdf", return_value=None
        ):
            head_resp = Mock()
            head_resp.status_code = 503
            raw_downloader.session.head.return_value = head_resp

            result = raw_downloader.download_with_result(
                "https://www.biorxiv.org/content/10.1101/2024.01.15.575123v1"
            )
        assert result.is_success is False
        assert "unavailable" in result.skip_reason.lower()

    def test_pdf_download_failure_head_also_fails(self, raw_downloader):
        """When both PDF download and HEAD request fail."""
        with patch.object(
            BioRxivDownloader.__bases__[0], "_download_pdf", return_value=None
        ):
            raw_downloader.session.head.side_effect = requests.ConnectionError(
                "no conn"
            )

            result = raw_downloader.download_with_result(
                "https://www.biorxiv.org/content/10.1101/2024.01.15.575123v1"
            )
        assert result.is_success is False
        assert "Failed to download" in result.skip_reason

    def test_pdf_download_failure_head_returns_other_code(self, raw_downloader):
        """HEAD returns non-404/non-5xx status (e.g. 403) -- generic failure."""
        with patch.object(
            BioRxivDownloader.__bases__[0], "_download_pdf", return_value=None
        ):
            head_resp = Mock()
            head_resp.status_code = 403
            raw_downloader.session.head.return_value = head_resp

            result = raw_downloader.download_with_result(
                "https://www.biorxiv.org/content/10.1101/2024.01.15.575123v1"
            )
        assert result.is_success is False
        assert "Failed to download" in result.skip_reason
