"""SemanticScholarClient graph API tests — citations, references, recommendations."""

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

# ── Helpers ──────────────────────────────────────────────────────────────

SAMPLE_PAPER_DATA: dict[str, Any] = {
    "paperId": "abc123",
    "title": "Deep Learning",
    "authors": [{"name": "John Smith"}],
    "year": 2024,
    "externalIds": {"DOI": SAMPLE_DOI, "ArXiv": "2401.12345"},
    "abstract": "A novel approach.",
    "venue": "NeurIPS",
    "citationCount": 42,
    "isOpenAccess": True,
    "openAccessPdf": {"url": "https://arxiv.org/pdf/2401.12345.pdf"},
}


def _graph_response(papers: list[dict[str, Any]], wrapper_key: str) -> MagicMock:
    """Build a mock httpx.Response for graph endpoints."""
    resp = MagicMock()
    resp.status_code = 200
    data = [{wrapper_key: p} for p in papers]
    resp.json.return_value = {"data": data}
    return resp


def _recs_response(papers: list[dict[str, Any]]) -> MagicMock:
    """Build a mock httpx.Response for recommendations endpoint."""
    resp = MagicMock()
    resp.status_code = 200
    resp.json.return_value = {"recommendedPapers": papers}
    return resp


# ── Tests ────────────────────────────────────────────────────────────────


class TestS2Citations:
    """SemanticScholarClient.citations() tests."""

    @patch("axm_bib.http.httpx.request")
    def test_s2_citations_returns_papers(self, mock_get: MagicMock) -> None:
        """Returns list[Paper] with normalized fields from citing papers."""
        mock_get.return_value = _graph_response(
            [SAMPLE_PAPER_DATA, SAMPLE_PAPER_DATA, SAMPLE_PAPER_DATA],
            "citingPaper",
        )

        client = SemanticScholarClient()
        papers = client.citations("abc123", limit=10)

        assert len(papers) == 3
        assert all(isinstance(p, Paper) for p in papers)
        assert all(p.source == "semantic_scholar" for p in papers)

    @patch("axm_bib.http.httpx.request")
    def test_s2_citations_empty(self, mock_get: MagicMock) -> None:
        """Empty citations list returns empty list."""
        resp = MagicMock()
        resp.status_code = 200
        resp.json.return_value = {"data": []}
        mock_get.return_value = resp

        client = SemanticScholarClient()
        papers = client.citations("abc123")

        assert papers == []

    @patch("axm_bib.http.httpx.request")
    def test_s2_citations_429_raises(self, mock_get: MagicMock) -> None:
        """429 rate limit raises SemanticScholarError."""
        mock_get.side_effect = Exception("429 Too Many Requests")

        client = SemanticScholarClient()
        with pytest.raises(SemanticScholarError):
            client.citations("abc123")


class TestS2References:
    """SemanticScholarClient.references() tests."""

    @patch("axm_bib.http.httpx.request")
    def test_s2_references_returns_papers(self, mock_get: MagicMock) -> None:
        """Returns list[Paper] from referenced papers."""
        mock_get.return_value = _graph_response(
            [SAMPLE_PAPER_DATA],
            "citedPaper",
        )

        client = SemanticScholarClient()
        papers = client.references("abc123", limit=5)

        assert len(papers) == 1
        assert isinstance(papers[0], Paper)
        assert papers[0].doi == SAMPLE_DOI


class TestS2Recommendations:
    """SemanticScholarClient.recommendations() tests."""

    @patch("axm_bib.http.httpx.request")
    def test_s2_recommendations(self, mock_get: MagicMock) -> None:
        """Returns list[Paper] from recommendations endpoint."""
        mock_get.return_value = _recs_response([SAMPLE_PAPER_DATA, SAMPLE_PAPER_DATA])

        client = SemanticScholarClient()
        papers = client.recommendations("abc123", limit=5)

        assert len(papers) == 2
        assert all(isinstance(p, Paper) for p in papers)

    @patch("axm_bib.http.httpx.request")
    def test_s2_recommendations_api_error(self, mock_get: MagicMock) -> None:
        """API failure raises SemanticScholarError."""
        mock_get.side_effect = Exception("500 Internal Server Error")

        client = SemanticScholarClient()
        with pytest.raises(SemanticScholarError, match="recommendations failed"):
            client.recommendations("abc123")
