"""Comprehensive coverage tests for SemanticScholarDownloader."""

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.semantic_scholar import (
    SemanticScholarDownloader,
)


VALID_HASH = "a" * 40  # 40-char hex hash used in Semantic Scholar URLs


@pytest.fixture
def raw_downloader():
    """Create a SemanticScholarDownloader without hitting real network."""
    d = SemanticScholarDownloader.__new__(SemanticScholarDownloader)
    d.timeout = 30
    d.api_key = None
    d.base_api_url = "https://api.semanticscholar.org/graph/v1"
    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


# =====================================================================
# download_with_result — TEXT content type returns skip reason
# =====================================================================


class TestDownloadWithResultText:
    """download_with_result should skip when content_type is TEXT."""

    def test_text_content_type_returns_skip_reason(self, raw_downloader):
        result = raw_downloader.download_with_result(
            "https://www.semanticscholar.org/paper/Title/" + VALID_HASH,
            ContentType.TEXT,
        )
        assert result.is_success is False
        assert result.skip_reason is not None
        assert "Text extraction not yet supported" in result.skip_reason


# =====================================================================
# _extract_paper_id — edge cases
# =====================================================================


class TestExtractPaperId:
    """Tests for _extract_paper_id covering URL variations."""

    def test_url_with_query_string(self, raw_downloader):
        """Query parameters should be ignored; hash still extracted."""
        url = (
            "https://www.semanticscholar.org/paper/Title/"
            + VALID_HASH
            + "?ref=search"
        )
        assert raw_downloader._extract_paper_id(url) == VALID_HASH

    def test_url_with_fragment(self, raw_downloader):
        """Fragment should be ignored; hash still extracted."""
        url = (
            "https://www.semanticscholar.org/paper/Title/"
            + VALID_HASH
            + "#abstract"
        )
        assert raw_downloader._extract_paper_id(url) == VALID_HASH

    def test_wrong_domain_returns_none(self, raw_downloader):
        """Non-semanticscholar domain should return None."""
        url = "https://arxiv.org/paper/" + VALID_HASH
        assert raw_downloader._extract_paper_id(url) is None

    def test_no_hex_hash_returns_none(self, raw_downloader):
        """Path without a 40-char hex hash should return None."""
        url = "https://www.semanticscholar.org/paper/SomeTitle/short123"
        assert raw_downloader._extract_paper_id(url) is None

    def test_direct_hash_url(self, raw_downloader):
        """URL with hash directly after /paper/ (no title slug)."""
        url = "https://www.semanticscholar.org/paper/" + VALID_HASH
        assert raw_downloader._extract_paper_id(url) == VALID_HASH


# =====================================================================
# _get_pdf_url — various API response scenarios
# =====================================================================


class TestGetPdfUrl:
    """Tests for _get_pdf_url covering API key, errors, and edge cases."""

    def test_api_key_header_sent(self, raw_downloader):
        """When api_key is set, x-api-key header should be included."""
        raw_downloader.api_key = "my-secret-key"

        mock_resp = Mock()
        mock_resp.status_code = 200
        mock_resp.json.return_value = {
            "openAccessPdf": {"url": "https://example.com/paper.pdf"}
        }
        raw_downloader.session.get.return_value = mock_resp

        result = raw_downloader._get_pdf_url("someid")

        call_kwargs = raw_downloader.session.get.call_args
        assert call_kwargs.kwargs["headers"]["x-api-key"] == "my-secret-key"
        assert result == "https://example.com/paper.pdf"

    def test_no_api_key_sends_empty_headers(self, raw_downloader):
        """When api_key is None, no x-api-key header should be sent."""
        raw_downloader.api_key = None

        mock_resp = Mock()
        mock_resp.status_code = 200
        mock_resp.json.return_value = {"openAccessPdf": None}
        raw_downloader.session.get.return_value = mock_resp

        raw_downloader._get_pdf_url("someid")

        call_kwargs = raw_downloader.session.get.call_args
        assert "x-api-key" not in call_kwargs.kwargs["headers"]

    def test_json_decode_error_returns_none(self, raw_downloader):
        """ValueError from json() should be caught and return None."""
        mock_resp = Mock()
        mock_resp.status_code = 200
        mock_resp.json.side_effect = ValueError("No JSON")
        raw_downloader.session.get.return_value = mock_resp

        result = raw_downloader._get_pdf_url("someid")
        assert result is None

    def test_request_exception_returns_none(self, raw_downloader):
        """requests.RequestException should be caught and return None."""
        raw_downloader.session.get.side_effect = (
            requests.exceptions.ConnectionError("network down")
        )

        result = raw_downloader._get_pdf_url("someid")
        assert result is None

    def test_open_access_pdf_none_returns_none(self, raw_downloader):
        """openAccessPdf=None in API response should return None."""
        mock_resp = Mock()
        mock_resp.status_code = 200
        mock_resp.json.return_value = {"openAccessPdf": None}
        raw_downloader.session.get.return_value = mock_resp

        result = raw_downloader._get_pdf_url("someid")
        assert result is None

    def test_open_access_pdf_not_dict_returns_none(self, raw_downloader):
        """openAccessPdf that is not a dict should return None."""
        mock_resp = Mock()
        mock_resp.status_code = 200
        mock_resp.json.return_value = {"openAccessPdf": "just-a-string"}
        raw_downloader.session.get.return_value = mock_resp

        result = raw_downloader._get_pdf_url("someid")
        assert result is None


# =====================================================================
# download_with_result — PDF download failure path
# =====================================================================


class TestDownloadWithResultPdfFailure:
    """download_with_result returns skip reason when PDF download fails."""

    def test_pdf_download_failure_returns_skip_reason(
        self, raw_downloader, mock_pdf_content
    ):
        """When _get_pdf_url succeeds but actual PDF download fails."""
        with (
            patch.object(
                raw_downloader,
                "_extract_paper_id",
                return_value=VALID_HASH,
            ),
            patch.object(
                raw_downloader,
                "_get_pdf_url",
                return_value="https://example.com/paper.pdf",
            ),
            patch.object(
                SemanticScholarDownloader.__bases__[0],
                "_download_pdf",
                return_value=None,
            ),
        ):
            result = raw_downloader.download_with_result(
                "https://www.semanticscholar.org/paper/Title/" + VALID_HASH
            )

        assert result.is_success is False
        assert "download failed" in result.skip_reason.lower()

    def test_invalid_paper_id_returns_skip_reason(self, raw_downloader):
        """When _extract_paper_id returns None, skip reason about invalid URL."""
        result = raw_downloader.download_with_result(
            "https://www.semanticscholar.org/paper/no-hash-here"
        )
        assert result.is_success is False
        assert "Invalid" in result.skip_reason


# =====================================================================
# can_handle — exception branches
# =====================================================================


class TestCanHandleExceptions:
    """can_handle should return False for inputs that cause exceptions."""

    def test_value_error_returns_false(self, raw_downloader):
        """ValueError from urlparse should be caught."""
        with patch(
            "local_deep_research.research_library.downloaders"
            ".semantic_scholar.urlparse",
            side_effect=ValueError("bad url"),
        ):
            assert raw_downloader.can_handle("anything") is False

    def test_attribute_error_returns_false(self, raw_downloader):
        """AttributeError (e.g. None input) should be caught."""
        with patch(
            "local_deep_research.research_library.downloaders"
            ".semantic_scholar.urlparse",
            side_effect=AttributeError("no attribute"),
        ):
            assert raw_downloader.can_handle("anything") is False

    def test_type_error_returns_false(self, raw_downloader):
        """TypeError (e.g. non-string input) should be caught."""
        with patch(
            "local_deep_research.research_library.downloaders"
            ".semantic_scholar.urlparse",
            side_effect=TypeError("wrong type"),
        ):
            assert raw_downloader.can_handle(12345) is False
