"""
Comprehensive coverage tests for research_library/downloaders/arxiv.py

Focuses on:
- download_with_result() TEXT path: with metadata, without metadata, no PDF, no extracted text
- _download_text(): with/without metadata, invalid ID, no PDF, no extracted text
- _fetch_from_arxiv_api(): categories parsing, exception handling, empty entry
- PDF URL construction correctness
"""

from unittest.mock import MagicMock

import pytest

from local_deep_research.research_library.downloaders.arxiv import (
    ArxivDownloader,
)
from local_deep_research.research_library.downloaders.base import (
    ContentType,
)


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------

ARXIV_API_FULL_XML = """<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom"
      xmlns:arxiv="http://arxiv.org/schemas/atom">
    <entry>
        <title>  Deep Learning Survey  </title>
        <summary>  We survey deep learning methods.  </summary>
        <author><name>Alice</name></author>
        <author><name>Bob</name></author>
        <category term="cs.LG"/>
        <category term="stat.ML"/>
    </entry>
</feed>"""

ARXIV_API_NO_ENTRY_XML = """<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
</feed>"""

ARXIV_API_EMPTY_ENTRY_XML = """<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom"
      xmlns:arxiv="http://arxiv.org/schemas/atom">
    <entry>
    </entry>
</feed>"""

ARXIV_API_CATEGORIES_ONLY_XML = """<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom"
      xmlns:arxiv="http://arxiv.org/schemas/atom">
    <entry>
        <title>Cats</title>
        <category term="cs.CV"/>
        <category term="cs.AI"/>
        <category term=""/>
    </entry>
</feed>"""


@pytest.fixture
def downloader():
    """Create an ArxivDownloader instance."""
    return ArxivDownloader(timeout=30)


@pytest.fixture
def mock_pdf_bytes():
    """Minimal PDF bytes that start with the PDF magic header."""
    return b"%PDF-1.4 fake pdf content for testing"


# ---------------------------------------------------------------------------
# download_with_result  --  TEXT content type
# ---------------------------------------------------------------------------


class TestDownloadWithResultText:
    """Tests for download_with_result when content_type is TEXT."""

    def test_text_with_metadata(self, downloader, mocker, mock_pdf_bytes):
        """When PDF is downloaded, text extracted, and metadata fetched,
        the result should contain metadata + separator + extracted text."""
        mocker.patch.object(
            downloader, "_download_pdf", return_value=mock_pdf_bytes
        )
        mocker.patch.object(
            downloader, "extract_text_from_pdf", return_value="Extracted body."
        )
        mocker.patch.object(
            downloader,
            "_fetch_from_arxiv_api",
            return_value="Title: Some Title\nAuthors: A, B",
        )

        result = downloader.download_with_result(
            "https://arxiv.org/abs/2301.12345", ContentType.TEXT
        )

        assert result.is_success is True
        text = result.content.decode("utf-8")
        assert "Title: Some Title" in text
        assert "FULL PAPER TEXT" in text
        assert "Extracted body." in text

    def test_text_without_metadata(self, downloader, mocker, mock_pdf_bytes):
        """When metadata fetch returns None, the result should contain only
        the extracted text (no separator header)."""
        mocker.patch.object(
            downloader, "_download_pdf", return_value=mock_pdf_bytes
        )
        mocker.patch.object(
            downloader, "extract_text_from_pdf", return_value="Just the text."
        )
        mocker.patch.object(
            downloader, "_fetch_from_arxiv_api", return_value=None
        )

        result = downloader.download_with_result(
            "https://arxiv.org/abs/2301.12345", ContentType.TEXT
        )

        assert result.is_success is True
        text = result.content.decode("utf-8")
        assert text == "Just the text."
        assert "FULL PAPER TEXT" not in text

    def test_text_no_pdf(self, downloader, mocker):
        """When PDF download fails, the result should be a skip with reason."""
        mocker.patch.object(downloader, "_download_pdf", return_value=None)

        result = downloader.download_with_result(
            "https://arxiv.org/abs/2301.12345", ContentType.TEXT
        )

        assert result.is_success is False
        assert "Could not retrieve full text" in result.skip_reason

    def test_text_no_extracted_text(self, downloader, mocker, mock_pdf_bytes):
        """When PDF is downloaded but text extraction returns None,
        the result should be a skip."""
        mocker.patch.object(
            downloader, "_download_pdf", return_value=mock_pdf_bytes
        )
        mocker.patch.object(
            downloader, "extract_text_from_pdf", return_value=None
        )

        result = downloader.download_with_result(
            "https://arxiv.org/abs/2301.12345", ContentType.TEXT
        )

        assert result.is_success is False
        assert result.skip_reason is not None
        assert "2301.12345" in result.skip_reason

    def test_text_empty_extracted_text(
        self, downloader, mocker, mock_pdf_bytes
    ):
        """When extract_text_from_pdf returns an empty string (falsy),
        the result should still be a skip."""
        mocker.patch.object(
            downloader, "_download_pdf", return_value=mock_pdf_bytes
        )
        mocker.patch.object(
            downloader, "extract_text_from_pdf", return_value=""
        )

        result = downloader.download_with_result(
            "https://arxiv.org/abs/2301.12345", ContentType.TEXT
        )

        assert result.is_success is False

    def test_text_invalid_url(self, downloader):
        """An invalid URL should yield a skip about not extracting article ID."""
        result = downloader.download_with_result(
            "https://example.com/random", ContentType.TEXT
        )

        assert result.is_success is False
        assert "could not extract" in result.skip_reason.lower()


# ---------------------------------------------------------------------------
# download_with_result  --  PDF content type
# ---------------------------------------------------------------------------


class TestDownloadWithResultPDF:
    """Tests for download_with_result when content_type is PDF."""

    def test_pdf_constructs_correct_url(
        self, downloader, mocker, mock_pdf_bytes
    ):
        """The method should call super()._download_pdf with the correct
        PDF URL."""
        mock_base_download = mocker.patch(
            "local_deep_research.research_library.downloaders"
            ".base.BaseDownloader._download_pdf",
            return_value=mock_pdf_bytes,
        )

        result = downloader.download_with_result(
            "https://arxiv.org/abs/2301.12345", ContentType.PDF
        )

        assert result.is_success is True
        assert result.content == mock_pdf_bytes
        mock_base_download.assert_called_once_with(
            "https://arxiv.org/pdf/2301.12345.pdf"
        )

    def test_pdf_old_format_url_construction(
        self, downloader, mocker, mock_pdf_bytes
    ):
        """Old-format arXiv IDs should produce correct PDF URLs."""
        mock_base_download = mocker.patch(
            "local_deep_research.research_library.downloaders"
            ".base.BaseDownloader._download_pdf",
            return_value=mock_pdf_bytes,
        )

        result = downloader.download_with_result(
            "https://arxiv.org/abs/cond-mat/0501234", ContentType.PDF
        )

        assert result.is_success is True
        mock_base_download.assert_called_once_with(
            "https://arxiv.org/pdf/cond-mat/0501234.pdf"
        )

    def test_pdf_failure_returns_skip(self, downloader, mocker):
        """When base _download_pdf returns None, should return skip reason."""
        mocker.patch(
            "local_deep_research.research_library.downloaders"
            ".base.BaseDownloader._download_pdf",
            return_value=None,
        )

        result = downloader.download_with_result(
            "https://arxiv.org/abs/2301.12345", ContentType.PDF
        )

        assert result.is_success is False
        assert "Failed to download PDF" in result.skip_reason


# ---------------------------------------------------------------------------
# _download_text
# ---------------------------------------------------------------------------


class TestDownloadText:
    """Tests for the _download_text private method."""

    def test_with_metadata(self, downloader, mocker, mock_pdf_bytes):
        """Returns bytes containing metadata + separator + extracted text."""
        mocker.patch.object(
            downloader, "_download_pdf", return_value=mock_pdf_bytes
        )
        mocker.patch.object(
            downloader, "extract_text_from_pdf", return_value="Paper text."
        )
        mocker.patch.object(
            downloader, "_fetch_from_arxiv_api", return_value="Title: Paper"
        )

        result = downloader._download_text("https://arxiv.org/abs/2301.12345")

        assert result is not None
        decoded = result.decode("utf-8")
        assert "Title: Paper" in decoded
        assert "FULL PAPER TEXT" in decoded
        assert "Paper text." in decoded

    def test_without_metadata(self, downloader, mocker, mock_pdf_bytes):
        """Returns only extracted text bytes when metadata is None."""
        mocker.patch.object(
            downloader, "_download_pdf", return_value=mock_pdf_bytes
        )
        mocker.patch.object(
            downloader, "extract_text_from_pdf", return_value="Only text."
        )
        mocker.patch.object(
            downloader, "_fetch_from_arxiv_api", return_value=None
        )

        result = downloader._download_text("https://arxiv.org/abs/2301.12345")

        assert result is not None
        assert result.decode("utf-8") == "Only text."

    def test_invalid_id_returns_none(self, downloader):
        """Invalid URL that yields no arXiv ID should return None."""
        result = downloader._download_text("https://example.com/nothing")
        assert result is None

    def test_no_pdf_returns_none(self, downloader, mocker):
        """When PDF download fails, returns None."""
        mocker.patch.object(downloader, "_download_pdf", return_value=None)

        result = downloader._download_text("https://arxiv.org/abs/2301.12345")
        assert result is None

    def test_no_extracted_text_returns_none(
        self, downloader, mocker, mock_pdf_bytes
    ):
        """When text extraction returns None, returns None."""
        mocker.patch.object(
            downloader, "_download_pdf", return_value=mock_pdf_bytes
        )
        mocker.patch.object(
            downloader, "extract_text_from_pdf", return_value=None
        )

        result = downloader._download_text("https://arxiv.org/abs/2301.12345")
        assert result is None


# ---------------------------------------------------------------------------
# _fetch_from_arxiv_api
# ---------------------------------------------------------------------------


class TestFetchFromArxivApi:
    """Tests for the _fetch_from_arxiv_api method."""

    def test_full_metadata_parsed(self, downloader, mocker):
        """Title, authors, abstract, and categories should all be parsed."""
        mock_resp = MagicMock()
        mock_resp.status_code = 200
        mock_resp.text = ARXIV_API_FULL_XML
        mocker.patch.object(downloader.session, "get", return_value=mock_resp)

        metadata = downloader._fetch_from_arxiv_api("2301.12345")

        assert metadata is not None
        assert "Title: Deep Learning Survey" in metadata
        assert "Alice" in metadata
        assert "Bob" in metadata
        assert "We survey deep learning methods." in metadata
        assert "cs.LG" in metadata
        assert "stat.ML" in metadata

    def test_categories_with_empty_term_skipped(self, downloader, mocker):
        """Categories with empty term attribute should be filtered out."""
        mock_resp = MagicMock()
        mock_resp.status_code = 200
        mock_resp.text = ARXIV_API_CATEGORIES_ONLY_XML
        mocker.patch.object(downloader.session, "get", return_value=mock_resp)

        metadata = downloader._fetch_from_arxiv_api("2301.12345")

        assert metadata is not None
        assert "cs.CV" in metadata
        assert "cs.AI" in metadata
        # The empty-string category should not appear as a dangling comma
        parts = metadata.split("Categories: ")[1]
        assert parts.strip() == "cs.CV, cs.AI"

    def test_exception_returns_none(self, downloader, mocker):
        """Any exception during API fetch should return None."""
        mocker.patch.object(
            downloader.session,
            "get",
            side_effect=Exception("connection refused"),
        )

        metadata = downloader._fetch_from_arxiv_api("2301.12345")
        assert metadata is None

    def test_non_200_returns_none(self, downloader, mocker):
        """Non-200 status code returns None."""
        mock_resp = MagicMock()
        mock_resp.status_code = 503
        mocker.patch.object(downloader.session, "get", return_value=mock_resp)

        metadata = downloader._fetch_from_arxiv_api("2301.12345")
        assert metadata is None

    def test_empty_entry_returns_none(self, downloader, mocker):
        """An entry with no title/authors/summary/categories returns None."""
        mock_resp = MagicMock()
        mock_resp.status_code = 200
        mock_resp.text = ARXIV_API_EMPTY_ENTRY_XML
        mocker.patch.object(downloader.session, "get", return_value=mock_resp)

        metadata = downloader._fetch_from_arxiv_api("2301.12345")
        assert metadata is None

    def test_no_entry_in_feed_returns_none(self, downloader, mocker):
        """Feed with no <entry> element returns None."""
        mock_resp = MagicMock()
        mock_resp.status_code = 200
        mock_resp.text = ARXIV_API_NO_ENTRY_XML
        mocker.patch.object(downloader.session, "get", return_value=mock_resp)

        metadata = downloader._fetch_from_arxiv_api("2301.12345")
        assert metadata is None

    def test_old_format_id_cleans_slash(self, downloader, mocker):
        """Old-format ID with slash should have slash removed for API query."""
        mock_resp = MagicMock()
        mock_resp.status_code = 200
        mock_resp.text = ARXIV_API_NO_ENTRY_XML
        mock_get = mocker.patch.object(
            downloader.session, "get", return_value=mock_resp
        )

        downloader._fetch_from_arxiv_api("cond-mat/0501234")

        called_url = mock_get.call_args[0][0]
        assert "cond-mat0501234" in called_url
        assert "/" not in called_url.split("id_list=")[1]


# ---------------------------------------------------------------------------
# PDF URL construction in _download_pdf
# ---------------------------------------------------------------------------


class TestPdfUrlConstruction:
    """Tests that _download_pdf constructs the right PDF URL."""

    def test_new_format_pdf_url(self, downloader, mocker, mock_pdf_bytes):
        """New-format ID produces https://arxiv.org/pdf/<id>.pdf."""
        mock_base = mocker.patch(
            "local_deep_research.research_library.downloaders"
            ".base.BaseDownloader._download_pdf",
            return_value=mock_pdf_bytes,
        )

        downloader._download_pdf("https://arxiv.org/abs/2301.12345")

        called_url = mock_base.call_args[0][0]
        assert called_url == "https://arxiv.org/pdf/2301.12345.pdf"

    def test_old_format_pdf_url(self, downloader, mocker, mock_pdf_bytes):
        """Old-format ID produces correct PDF URL with slash."""
        mock_base = mocker.patch(
            "local_deep_research.research_library.downloaders"
            ".base.BaseDownloader._download_pdf",
            return_value=mock_pdf_bytes,
        )

        downloader._download_pdf("https://arxiv.org/abs/cond-mat/0501234")

        called_url = mock_base.call_args[0][0]
        assert called_url == "https://arxiv.org/pdf/cond-mat/0501234.pdf"

    def test_invalid_url_returns_none(self, downloader):
        """Invalid URL yields None without calling base download."""
        result = downloader._download_pdf("https://example.com/nope")
        assert result is None

    def test_enhanced_headers_passed(self, downloader, mocker, mock_pdf_bytes):
        """The enhanced headers dict should be forwarded to
        base._download_pdf."""
        mock_base = mocker.patch(
            "local_deep_research.research_library.downloaders"
            ".base.BaseDownloader._download_pdf",
            return_value=mock_pdf_bytes,
        )

        downloader._download_pdf("https://arxiv.org/abs/2301.12345")

        call_kwargs = mock_base.call_args
        headers = call_kwargs[1].get("headers") if call_kwargs[1] else None
        assert headers is not None
        assert "User-Agent" in headers
        assert "application/pdf" in headers["Accept"]
        assert headers["Connection"] == "keep-alive"
