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

from unittest.mock import Mock, patch, MagicMock

import pytest
import requests

from local_deep_research.research_library.downloaders.base import (
    ContentType,
    DownloadResult,
)
from local_deep_research.research_library.downloaders.openalex import (
    OpenAlexDownloader,
)


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


@pytest.fixture
def downloader():
    """Create an OpenAlexDownloader with mocked session/rate_tracker."""
    d = OpenAlexDownloader.__new__(OpenAlexDownloader)
    d.timeout = 30
    d.polite_pool_email = None
    d.base_api_url = "https://api.openalex.org"
    d.session = MagicMock()
    d.session.headers = {"User-Agent": "Test"}
    d.rate_tracker = MagicMock()
    d.rate_tracker.apply_rate_limit.return_value = 0.0
    return d


@pytest.fixture
def pdf_bytes():
    """Minimal PDF content."""
    return b"%PDF-1.4 minimal test content"


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


class TestCanHandle:
    def test_openalex_org(self, downloader):
        assert downloader.can_handle("https://openalex.org/W123456789") is True

    def test_subdomain(self, downloader):
        assert (
            downloader.can_handle("https://api.openalex.org/works/W123") is True
        )

    def test_http_scheme(self, downloader):
        assert downloader.can_handle("http://openalex.org/W999") is True

    def test_with_query_string(self, downloader):
        assert (
            downloader.can_handle("https://openalex.org/W1?select=id") is True
        )

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

    def test_rejects_example(self, downloader):
        assert downloader.can_handle("https://example.com/openalex") is False

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

    def test_rejects_none(self, downloader):
        assert downloader.can_handle(None) is False

    def test_rejects_not_a_url(self, downloader):
        assert downloader.can_handle("just some text") is False

    def test_rejects_similar_domain(self, downloader):
        assert downloader.can_handle("https://notopenalex.org/W123") is False

    def test_rejects_openalex_in_path(self, downloader):
        assert (
            downloader.can_handle("https://example.com/openalex.org/W1")
            is False
        )


# ============== _extract_work_id ==============


class TestExtractWorkId:
    def test_direct_work_url(self, downloader):
        assert (
            downloader._extract_work_id("https://openalex.org/W1234567890")
            == "W1234567890"
        )

    def test_works_path(self, downloader):
        assert (
            downloader._extract_work_id("https://openalex.org/works/W9999")
            == "W9999"
        )

    def test_api_subdomain(self, downloader):
        assert (
            downloader._extract_work_id("https://api.openalex.org/works/W42")
            == "W42"
        )

    def test_with_trailing_slash(self, downloader):
        assert (
            downloader._extract_work_id("https://openalex.org/W100/") == "W100"
        )

    def test_with_query_params(self, downloader):
        assert (
            downloader._extract_work_id("https://openalex.org/W55?select=id")
            == "W55"
        )

    def test_with_fragment(self, downloader):
        assert (
            downloader._extract_work_id("https://openalex.org/W77#section")
            == "W77"
        )

    def test_non_openalex_domain(self, downloader):
        assert downloader._extract_work_id("https://example.com/W123") is None

    def test_no_work_id_in_path(self, downloader):
        assert (
            downloader._extract_work_id("https://openalex.org/authors/A123")
            is None
        )

    def test_empty_string(self, downloader):
        assert downloader._extract_work_id("") is None

    def test_invalid_url(self, downloader):
        assert downloader._extract_work_id("not-a-url") is None


# ============== _get_pdf_url ==============


class TestGetPdfUrl:
    def test_returns_pdf_url_when_oa_with_pdf(self, downloader):
        """Open access work with pdf_url in best_oa_location."""
        resp = Mock()
        resp.status_code = 200
        resp.json.return_value = {
            "open_access": {"is_oa": True},
            "best_oa_location": {
                "pdf_url": "https://example.com/paper.pdf",
                "landing_page_url": "https://example.com/paper",
            },
        }
        downloader.session.get.return_value = resp

        result = downloader._get_pdf_url("W123")
        assert result == "https://example.com/paper.pdf"

    def test_returns_none_when_not_oa(self, downloader):
        """Work is not open access."""
        resp = Mock()
        resp.status_code = 200
        resp.json.return_value = {
            "open_access": {"is_oa": False},
            "best_oa_location": None,
        }
        downloader.session.get.return_value = resp

        assert downloader._get_pdf_url("W123") is None

    def test_returns_landing_url_when_pdf_content_type(self, downloader):
        """No pdf_url but landing_page_url serves a PDF."""
        api_resp = Mock()
        api_resp.status_code = 200
        api_resp.json.return_value = {
            "open_access": {"is_oa": True},
            "best_oa_location": {
                "pdf_url": None,
                "landing_page_url": "https://example.com/landing",
            },
        }

        head_resp = Mock()
        head_resp.headers = {"Content-Type": "application/pdf"}

        downloader.session.get.return_value = api_resp
        downloader.session.head.return_value = head_resp

        result = downloader._get_pdf_url("W123")
        assert result == "https://example.com/landing"

    def test_skips_landing_url_when_not_pdf(self, downloader):
        """Landing page is HTML, not PDF."""
        api_resp = Mock()
        api_resp.status_code = 200
        api_resp.json.return_value = {
            "open_access": {"is_oa": True},
            "best_oa_location": {
                "pdf_url": None,
                "landing_page_url": "https://example.com/landing",
            },
        }

        head_resp = Mock()
        head_resp.headers = {"Content-Type": "text/html; charset=utf-8"}

        downloader.session.get.return_value = api_resp
        downloader.session.head.return_value = head_resp

        assert downloader._get_pdf_url("W123") is None

    def test_landing_url_head_request_fails(self, downloader):
        """HEAD request for landing page raises an exception."""
        api_resp = Mock()
        api_resp.status_code = 200
        api_resp.json.return_value = {
            "open_access": {"is_oa": True},
            "best_oa_location": {
                "pdf_url": None,
                "landing_page_url": "https://example.com/landing",
            },
        }

        downloader.session.get.return_value = api_resp
        downloader.session.head.side_effect = (
            requests.exceptions.ConnectionError("fail")
        )

        assert downloader._get_pdf_url("W123") is None

    def test_no_best_oa_location(self, downloader):
        """Open access but best_oa_location is empty/None."""
        resp = Mock()
        resp.status_code = 200
        resp.json.return_value = {
            "open_access": {"is_oa": True},
            "best_oa_location": None,
        }
        downloader.session.get.return_value = resp

        assert downloader._get_pdf_url("W123") is None

    def test_oa_location_empty_dict(self, downloader):
        """best_oa_location is an empty dict (no pdf_url, no landing_page_url)."""
        resp = Mock()
        resp.status_code = 200
        resp.json.return_value = {
            "open_access": {"is_oa": True},
            "best_oa_location": {},
        }
        downloader.session.get.return_value = resp

        assert downloader._get_pdf_url("W123") is None

    def test_404_response(self, downloader):
        """API returns 404."""
        resp = Mock()
        resp.status_code = 404
        downloader.session.get.return_value = resp

        assert downloader._get_pdf_url("W999") is None

    def test_500_response(self, downloader):
        """API returns 500 server error."""
        resp = Mock()
        resp.status_code = 500
        downloader.session.get.return_value = resp

        assert downloader._get_pdf_url("W999") is None

    def test_request_exception(self, downloader):
        """Network error during API call."""
        downloader.session.get.side_effect = (
            requests.exceptions.ConnectionError("timeout")
        )

        assert downloader._get_pdf_url("W123") is None

    def test_json_decode_error(self, downloader):
        """API returns invalid JSON."""
        resp = Mock()
        resp.status_code = 200
        resp.json.side_effect = ValueError("No JSON object")
        downloader.session.get.return_value = resp

        assert downloader._get_pdf_url("W123") is None

    def test_polite_pool_email_in_header(self, downloader):
        """When polite_pool_email is set, User-Agent header is sent."""
        downloader.polite_pool_email = "test@example.com"

        resp = Mock()
        resp.status_code = 404
        downloader.session.get.return_value = resp

        downloader._get_pdf_url("W123")

        call_kwargs = downloader.session.get.call_args
        headers = (
            call_kwargs[1]["headers"]
            if "headers" in call_kwargs[1]
            else call_kwargs.kwargs["headers"]
        )
        assert "mailto:test@example.com" in headers.get("User-Agent", "")

    def test_no_polite_pool_email_empty_headers(self, downloader):
        """When polite_pool_email is None, no User-Agent override."""
        resp = Mock()
        resp.status_code = 404
        downloader.session.get.return_value = resp

        downloader._get_pdf_url("W123")

        call_kwargs = downloader.session.get.call_args
        headers = call_kwargs[1].get(
            "headers", call_kwargs.kwargs.get("headers", {})
        )
        assert headers == {}

    def test_missing_open_access_key(self, downloader):
        """Response JSON has no open_access key at all."""
        resp = Mock()
        resp.status_code = 200
        resp.json.return_value = {
            "best_oa_location": {"pdf_url": "https://x.com/a.pdf"},
        }
        downloader.session.get.return_value = resp

        # is_oa defaults to False because open_access is empty dict
        assert downloader._get_pdf_url("W123") is None


# ============== download_with_result ==============


class TestDownloadWithResult:
    def test_non_pdf_content_type_returns_skip(self, downloader):
        """Requesting TEXT content type returns skip reason."""
        result = downloader.download_with_result(
            "https://openalex.org/W123", content_type=ContentType.TEXT
        )
        assert result.is_success is False
        assert (
            "not yet supported" in result.skip_reason.lower()
            or "Text extraction" in result.skip_reason
        )

    def test_invalid_url_returns_skip(self, downloader):
        """URL that doesn't contain a work ID returns skip."""
        result = downloader.download_with_result(
            "https://openalex.org/authors/A1"
        )
        assert result.is_success is False
        assert "work ID" in result.skip_reason

    def test_no_pdf_url_returns_skip(self, downloader):
        """When _get_pdf_url returns None, skip reason mentions not open access."""
        with patch.object(downloader, "_get_pdf_url", return_value=None):
            result = downloader.download_with_result(
                "https://openalex.org/W123"
            )
        assert result.is_success is False
        assert (
            "not open access" in result.skip_reason.lower()
            or "no free PDF" in result.skip_reason
        )

    def test_successful_download(self, downloader, pdf_bytes):
        """Full successful flow: extract ID -> get PDF URL -> download PDF."""
        with patch.object(
            downloader,
            "_get_pdf_url",
            return_value="https://example.com/paper.pdf",
        ):
            with patch(
                "local_deep_research.research_library.downloaders.base.BaseDownloader._download_pdf",
                return_value=pdf_bytes,
            ) as mock_dl:
                result = downloader.download_with_result(
                    "https://openalex.org/W123"
                )

        assert result.is_success is True
        assert result.content == pdf_bytes
        mock_dl.assert_called_once_with("https://example.com/paper.pdf")

    def test_pdf_download_fails(self, downloader):
        """PDF URL found but actual download returns None."""
        with patch.object(
            downloader,
            "_get_pdf_url",
            return_value="https://example.com/paper.pdf",
        ):
            with patch(
                "local_deep_research.research_library.downloaders.base.BaseDownloader._download_pdf",
                return_value=None,
            ):
                result = downloader.download_with_result(
                    "https://openalex.org/W123"
                )

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


# ============== download ==============


class TestDownload:
    def test_returns_bytes_on_success(self, downloader, pdf_bytes):
        """download() returns content bytes when successful."""
        with patch.object(
            downloader,
            "download_with_result",
            return_value=DownloadResult(content=pdf_bytes, is_success=True),
        ):
            result = downloader.download("https://openalex.org/W123")
        assert result == pdf_bytes

    def test_returns_none_on_failure(self, downloader):
        """download() returns None when download_with_result fails."""
        with patch.object(
            downloader,
            "download_with_result",
            return_value=DownloadResult(skip_reason="No PDF"),
        ):
            result = downloader.download("https://openalex.org/W123")
        assert result is None

    def test_passes_content_type(self, downloader):
        """download() passes content_type through to download_with_result."""
        with patch.object(
            downloader,
            "download_with_result",
            return_value=DownloadResult(skip_reason="unsupported"),
        ) as mock_dwr:
            downloader.download(
                "https://openalex.org/W123", content_type=ContentType.TEXT
            )

        mock_dwr.assert_called_once_with(
            "https://openalex.org/W123", ContentType.TEXT
        )


# ============== __init__ ==============


class TestInit:
    def test_default_timeout(self):
        """Default timeout is 30."""
        d = OpenAlexDownloader()
        assert d.timeout == 30

    def test_custom_timeout(self):
        """Custom timeout is respected."""
        d = OpenAlexDownloader(timeout=60)
        assert d.timeout == 60

    def test_polite_pool_email(self):
        """Polite pool email is stored."""
        d = OpenAlexDownloader(polite_pool_email="me@example.com")
        assert d.polite_pool_email == "me@example.com"

    def test_polite_pool_default_none(self):
        """Polite pool email defaults to None."""
        d = OpenAlexDownloader()
        assert d.polite_pool_email is None

    def test_base_api_url(self):
        """Base API URL is set correctly."""
        d = OpenAlexDownloader()
        assert d.base_api_url == "https://api.openalex.org"

    def test_session_created(self):
        """Session is created."""
        d = OpenAlexDownloader()
        assert d.session is not None
