"""HALClient unit tests — mocked httpx, following test_semantic_scholar pattern."""

from __future__ import annotations

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

import httpx
import pytest

from axm_bib.clients.hal import HALClient, HALError

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

SAMPLE_SEARCH_RESPONSE: dict[str, Any] = {
    "response": {
        "numFound": 1,
        "start": 0,
        "docs": [
            {
                "halId_s": "hal-01234567",
                "title_s": [
                    "Optimisation multi-objectif par algorithmes génétiques",
                ],
                "authFullName_s": [
                    "Alice Martin",
                    "Bob Leroy",
                ],
                "producedDateY_i": 2024,
                "doiId_s": "10.1234/example.2024",
                "abstract_s": [
                    "Cet article présente une méthode d'optimisation...",
                ],
                "journalTitle_s": "Revue d'Intelligence Artificielle",
            },
        ],
    },
}

SAMPLE_BIBTEX_RESPONSE = """@article{martin:hal-01234567,
  TITLE = {{Optimisation multi-objectif par algorithmes g\'en\'etiques}},
  AUTHOR = {Martin, Alice and Leroy, Bob},
  JOURNAL = {{Revue d'Intelligence Artificielle}},
  YEAR = {2024},
  DOI = {10.1234/example.2024},
  HAL_ID = {hal-01234567},
}
"""


def _mock_response(
    json_data: dict[str, Any] | None = None,
    text_data: str = "",
    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.text = text_data
    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


# ── Test Classes ─────────────────────────────────────────────────────────


class TestHALSearch:
    """Search endpoint tests."""

    @patch("axm_bib.http.httpx.request")
    def test_search_returns_papers(self, mock_get: MagicMock) -> None:
        """Search returns list of Paper objects."""
        mock_get.return_value = _mock_response(json_data=SAMPLE_SEARCH_RESPONSE)

        client = HALClient()
        papers = client.search("formal verification", limit=5)

        assert len(papers) == 1
        p = papers[0]
        assert "Optimisation" in p.title
        assert len(p.authors) == 2
        assert p.year == 2024
        assert p.source == "hal"
        assert "api.archives-ouvertes.fr" in mock_get.call_args[0][1]

    @patch("axm_bib.http.httpx.request")
    def test_hal_id_extracted(self, mock_get: MagicMock) -> None:
        """HAL ID extracted from response."""
        mock_get.return_value = _mock_response(json_data=SAMPLE_SEARCH_RESPONSE)

        client = HALClient()
        papers = client.search("formal")

        assert papers[0].hal_id == "hal-01234567"

    @patch("axm_bib.http.httpx.request")
    def test_doi_extracted(self, mock_get: MagicMock) -> None:
        """DOI extracted from response."""
        mock_get.return_value = _mock_response(json_data=SAMPLE_SEARCH_RESPONSE)

        client = HALClient()
        papers = client.search("formal")

        assert papers[0].doi == "10.1234/example.2024"

    @patch("axm_bib.http.httpx.request")
    def test_empty_results(self, mock_get: MagicMock) -> None:
        """Empty results return empty list."""
        mock_get.return_value = _mock_response(
            json_data={"response": {"numFound": 0, "start": 0, "docs": []}}
        )

        client = HALClient()
        papers = client.search("nonexistent xyz")

        assert papers == []

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

        client = HALClient()
        with pytest.raises(HALError, match="failed"):
            client.search("test")

    @patch("axm_bib.http.httpx.request")
    def test_empty_query_returns_empty(self, mock_get: MagicMock) -> None:
        """Empty query returns empty list without API call."""
        client = HALClient()
        papers = client.search("  ")

        assert papers == []
        mock_get.assert_not_called()


class TestHALBibTeX:
    """BibTeX endpoint tests."""

    @patch("axm_bib.http.httpx.request")
    def test_bibtex_native(self, mock_get: MagicMock) -> None:
        """Native BibTeX returned as BibEntry."""
        mock_get.return_value = _mock_response(text_data=SAMPLE_BIBTEX_RESPONSE)

        client = HALClient()
        entry = client.bibtex("hal-01234567")

        assert entry.key == "martin:hal-01234567"
        assert "Optimisation" in entry.raw
        assert entry.doi == "10.1234/example.2024"

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

        client = HALClient()
        with pytest.raises(HALError, match="failed"):
            client.bibtex("hal-nonexistent")

    @patch("axm_bib.http.httpx.request")
    def test_bibtex_empty_response(self, mock_get: MagicMock) -> None:
        """Empty BibTeX response raises HALError."""
        mock_get.return_value = _mock_response(text_data="")

        client = HALClient()
        with pytest.raises(HALError, match="empty"):
            client.bibtex("hal-01234567")


class TestHALEdgeCases:
    """Edge cases."""

    @patch("axm_bib.http.httpx.request")
    def test_missing_optional_fields(self, mock_get: MagicMock) -> None:
        """Doc with missing optional fields doesn't crash."""
        response: dict[str, Any] = {
            "response": {
                "numFound": 1,
                "start": 0,
                "docs": [
                    {
                        "halId_s": "hal-99999999",
                        "title_s": ["Minimal Paper"],
                        "authFullName_s": ["Author One"],
                    },
                ],
            },
        }
        mock_get.return_value = _mock_response(json_data=response)

        client = HALClient()
        papers = client.search("test")

        p = papers[0]
        assert p.title == "Minimal Paper"
        assert p.doi is None
        assert p.venue is None
        assert p.abstract is None
        assert p.year is None

    @patch("axm_bib.http.httpx.request")
    def test_title_as_string(self, mock_get: MagicMock) -> None:
        """title_s can be a plain string instead of list."""
        response: dict[str, Any] = {
            "response": {
                "numFound": 1,
                "start": 0,
                "docs": [
                    {
                        "halId_s": "hal-11111111",
                        "title_s": "String Title",
                        "authFullName_s": ["Author"],
                    },
                ],
            },
        }
        mock_get.return_value = _mock_response(json_data=response)

        client = HALClient()
        papers = client.search("test")

        assert papers[0].title == "String Title"
