"""TDD tests for axm_bib.clients.semantic_scholar (httpx-based)."""

from __future__ import annotations

from typing import Any
from unittest.mock import MagicMock, patch

import pytest

from axm_bib.clients.semantic_scholar import SemanticScholarClient, SemanticScholarError
from axm_bib.models import Paper
from tests.conftest import SAMPLE_DOI


def _s2_response(papers: list[dict[str, Any]]) -> MagicMock:
    """Build a mock httpx.Response with given paper dicts."""
    resp = MagicMock()
    resp.status_code = 200
    resp.json.return_value = {"total": len(papers), "data": papers}
    return resp


SAMPLE_PAPER = {
    "paperId": "abc123",
    "title": "Deep Learning",
    "authors": [{"name": "John Smith"}],
    "year": 2024,
    "externalIds": {"DOI": SAMPLE_DOI, "ArXiv": "2401.12345"},
    "abstract": "We present a novel deep learning approach for formal verification.",
    "venue": "NeurIPS",
    "citationCount": 42,
    "isOpenAccess": True,
    "openAccessPdf": {"url": "https://arxiv.org/pdf/2401.12345.pdf"},
}


class TestSearchPaper:
    """SemanticScholarClient.search_paper() tests."""

    @patch("axm_bib.clients.semantic_scholar.httpx.get")
    def test_search_paper_returns_papers(self, mock_get: MagicMock) -> None:
        """Returns list[Paper] with normalized fields."""
        mock_get.return_value = _s2_response([SAMPLE_PAPER])

        client = SemanticScholarClient()
        papers = client.search_paper("Deep Learning", limit=5)
        assert len(papers) == 1
        assert isinstance(papers[0], Paper)
        assert papers[0].source == "semantic_scholar"

    @patch("axm_bib.clients.semantic_scholar.httpx.get")
    def test_search_paper_extracts_doi(self, mock_get: MagicMock) -> None:
        """DOI from externalIds is mapped to paper.doi."""
        mock_get.return_value = _s2_response([SAMPLE_PAPER])

        client = SemanticScholarClient()
        papers = client.search_paper("Deep Learning", limit=1)
        assert papers[0].doi == SAMPLE_DOI

    @patch("axm_bib.clients.semantic_scholar.httpx.get")
    def test_search_paper_extracts_abstract(self, mock_get: MagicMock) -> None:
        """Abstract is correctly extracted from API response."""
        mock_get.return_value = _s2_response([SAMPLE_PAPER])

        client = SemanticScholarClient()
        papers = client.search_paper("Deep Learning", limit=1)
        assert papers[0].abstract == SAMPLE_PAPER["abstract"]

    @patch("axm_bib.clients.semantic_scholar.httpx.get")
    def test_search_no_doi_available(self, mock_get: MagicMock) -> None:
        """Paper without DOI has paper.doi = None, no crash."""
        paper = {**SAMPLE_PAPER, "externalIds": {}}
        mock_get.return_value = _s2_response([paper])

        client = SemanticScholarClient()
        papers = client.search_paper("Workshop", limit=1)
        assert papers[0].doi is None

    @patch("axm_bib.clients.semantic_scholar.httpx.get")
    def test_search_empty_query(self, mock_get: MagicMock) -> None:
        """Empty query returns [] without API call."""
        client = SemanticScholarClient()
        papers = client.search_paper("", limit=5)
        assert papers == []
        mock_get.assert_not_called()

    @patch("axm_bib.clients.semantic_scholar.httpx.get")
    def test_open_access_pdf_extracted(self, mock_get: MagicMock) -> None:
        """open_access_pdf URL is populated when available."""
        mock_get.return_value = _s2_response([SAMPLE_PAPER])

        client = SemanticScholarClient()
        papers = client.search_paper("Deep Learning", limit=1)
        assert papers[0].open_access_pdf == "https://arxiv.org/pdf/2401.12345.pdf"

    @patch("axm_bib.clients.semantic_scholar.httpx.get")
    def test_api_error_raises(self, mock_get: MagicMock) -> None:
        """API failure raises SemanticScholarError."""
        mock_get.side_effect = Exception("429 Too Many Requests")

        client = SemanticScholarClient()
        with pytest.raises(SemanticScholarError):
            client.search_paper("Deep Learning", limit=5)

    @patch("axm_bib.clients.semantic_scholar.httpx.get")
    def test_limit_respected(self, mock_get: MagicMock) -> None:
        """limit parameter is forwarded to the API."""
        mock_get.return_value = _s2_response([])

        client = SemanticScholarClient()
        client.search_paper("Query", limit=3)
        _, kwargs = mock_get.call_args
        assert kwargs["params"]["limit"] == 3

    @patch("axm_bib.clients.semantic_scholar.httpx.get")
    def test_requests_abstract_field(self, mock_get: MagicMock) -> None:
        """API call includes abstract in requested fields."""
        mock_get.return_value = _s2_response([])

        client = SemanticScholarClient()
        client.search_paper("test", limit=1)
        _, kwargs = mock_get.call_args
        assert "abstract" in kwargs["params"]["fields"]
