"""OpenAlexClient graph API tests — cited_by, referenced_works."""

from __future__ import annotations

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

import httpx
import pytest

from axm_bib.clients.openalex import OpenAlexClient, OpenAlexError

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

SAMPLE_WORK: dict[str, Any] = {
    "id": "https://openalex.org/W2741809807",
    "title": "Attention Is All You Need",
    "publication_year": 2017,
    "ids": {
        "openalex": "https://openalex.org/W2741809807",
        "doi": "https://doi.org/10.48550/arXiv.1706.03762",
    },
    "authorships": [
        {"author": {"display_name": "Ashish Vaswani"}},
        {"author": {"display_name": "Noam Shazeer"}},
    ],
    "primary_location": {
        "source": {"display_name": "NeurIPS"},
    },
    "cited_by_count": 98234,
    "abstract_inverted_index": None,
}


def _mock_response(
    json_data: dict[str, Any] | None = None,
    status_code: int = 200,
) -> MagicMock:
    """Create a mock httpx.Response."""
    resp = MagicMock(spec=httpx.Response)
    resp.status_code = status_code
    resp.headers = {}
    resp.json.return_value = json_data or {}
    resp.raise_for_status = MagicMock()
    if status_code >= 400:
        resp.raise_for_status.side_effect = httpx.HTTPStatusError(
            message=f"HTTP {status_code}",
            request=MagicMock(spec=httpx.Request),
            response=resp,
        )
    return resp


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


class TestOpenAlexCitedBy:
    """OpenAlexClient.cited_by() tests."""

    @patch("axm_bib.http.httpx.request")
    def test_openalex_cited_by(self, mock_get: MagicMock) -> None:
        """Returns list[Paper] from citing works."""
        mock_get.return_value = _mock_response(
            {"results": [SAMPLE_WORK, SAMPLE_WORK], "meta": {"count": 2}},
        )

        client = OpenAlexClient()
        papers = client.cited_by("W2741809807", limit=10)

        assert len(papers) == 2
        assert all(p.source == "openalex" for p in papers)
        # Verify filter param uses cites: with full URL
        call_kwargs = mock_get.call_args[1]
        assert "cites:" in call_kwargs["params"]["filter"]

    @patch("axm_bib.http.httpx.request")
    def test_cited_by_empty(self, mock_get: MagicMock) -> None:
        """No citing papers returns empty list."""
        mock_get.return_value = _mock_response(
            {"results": [], "meta": {"count": 0}},
        )

        client = OpenAlexClient()
        papers = client.cited_by("W0000000000")

        assert papers == []

    @patch("axm_bib.http.httpx.request")
    def test_cited_by_api_error(self, mock_get: MagicMock) -> None:
        """HTTP error raises OpenAlexError."""
        mock_get.return_value = _mock_response(status_code=500)

        client = OpenAlexClient()
        with pytest.raises(OpenAlexError, match="cited_by failed"):
            client.cited_by("W1234")

    @patch("axm_bib.http.httpx.request")
    def test_cited_by_accepts_full_url(self, mock_get: MagicMock) -> None:
        """Full OpenAlex URL is passed directly."""
        mock_get.return_value = _mock_response(
            {"results": [], "meta": {"count": 0}},
        )

        client = OpenAlexClient()
        client.cited_by("https://openalex.org/W2741809807")

        call_kwargs = mock_get.call_args[1]
        assert "https://openalex.org/W2741809807" in call_kwargs["params"]["filter"]


class TestOpenAlexReferencedWorks:
    """OpenAlexClient.referenced_works() tests."""

    @patch("axm_bib.http.httpx.request")
    def test_openalex_referenced_works(self, mock_get: MagicMock) -> None:
        """Returns list[Paper] from referenced works."""
        # First call: get work with referenced_works
        work_with_refs = {
            **SAMPLE_WORK,
            "referenced_works": [
                "https://openalex.org/W1234",
                "https://openalex.org/W5678",
            ],
        }
        # Second call: batch-fetch referenced works
        mock_get.side_effect = [
            _mock_response(work_with_refs),
            _mock_response(
                {"results": [SAMPLE_WORK], "meta": {"count": 1}},
            ),
        ]

        client = OpenAlexClient()
        papers = client.referenced_works("W2741809807")

        assert len(papers) == 1
        assert papers[0].source == "openalex"
        assert mock_get.call_count == 2

    @patch("axm_bib.http.httpx.request")
    def test_referenced_works_empty(self, mock_get: MagicMock) -> None:
        """Work with no referenced_works returns empty list."""
        work_no_refs = {**SAMPLE_WORK, "referenced_works": []}
        mock_get.return_value = _mock_response(work_no_refs)

        client = OpenAlexClient()
        papers = client.referenced_works("W2741809807")

        assert papers == []
        # Only one call (no batch fetch needed)
        assert mock_get.call_count == 1

    @patch("axm_bib.http.httpx.request")
    def test_referenced_works_api_error(self, mock_get: MagicMock) -> None:
        """HTTP error raises OpenAlexError."""
        mock_get.return_value = _mock_response(status_code=404)

        client = OpenAlexClient()
        with pytest.raises(OpenAlexError, match="referenced_works failed"):
            client.referenced_works("W0000000000")
