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

from __future__ import annotations

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

import pytest

from axm_bib.clients.dblp import DBLPClient, DBLPError
from axm_bib.models import BibEntry, Paper
from tests.conftest import SAMPLE_DOI

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


def _dblp_search_response(hits: list[dict[str, Any]]) -> MagicMock:
    """Build a mock httpx.Response with DBLP search JSON."""
    resp = MagicMock()
    resp.status_code = 200
    resp.json.return_value = {
        "result": {
            "hits": {
                "@total": str(len(hits)),
                "hit": [{"info": h} for h in hits],
            },
        },
    }
    return resp


def _dblp_bibtex_response(bibtex: str) -> MagicMock:
    """Build a mock httpx.Response for BibTeX endpoint."""
    resp = MagicMock()
    resp.status_code = 200
    resp.text = bibtex
    return resp


def _dblp_error_response(status: int) -> MagicMock:
    """Build a mock httpx.Response that raises on raise_for_status."""
    resp = MagicMock()
    resp.status_code = status
    resp.raise_for_status.side_effect = Exception(f"{status} Error")
    return resp


# ── Sample Data ──────────────────────────────────────────────────────────

SAMPLE_DBLP_HIT: dict[str, Any] = {
    "key": "conf/nips/VaswaniSPUJGKP17",
    "title": "Attention Is All You Need.",
    "authors": {"author": [{"text": "Ashish Vaswani"}, {"text": "Noam Shazeer"}]},
    "year": "2017",
    "venue": "NeurIPS",
    "doi": SAMPLE_DOI,
    "url": "https://dblp.org/rec/conf/nips/VaswaniSPUJGKP17",
}

SAMPLE_DBLP_HIT_NO_DOI: dict[str, Any] = {
    "key": "conf/acl/SomeWorkshop23",
    "title": "Workshop Paper Without DOI.",
    "authors": {"author": [{"text": "Alice Bob"}]},
    "year": "2023",
    "venue": "ACL Workshop",
    "url": "https://dblp.org/rec/conf/acl/SomeWorkshop23",
}

SAMPLE_DBLP_BIBTEX = (
    "@inproceedings{DBLP:conf/nips/VaswaniSPUJGKP17,\n"
    "  author = {Ashish Vaswani and Noam Shazeer},\n"
    "  title = {Attention Is All You Need},\n"
    "  year = {2017},\n"
    "  booktitle = {NeurIPS}\n"
    "}"
)


# ── Search Tests ─────────────────────────────────────────────────────────


class TestDBLPSearch:
    """DBLPClient.search() tests."""

    @patch("axm_bib.http.httpx.request")
    def test_search_returns_papers(self, mock_get: MagicMock) -> None:
        """Returns list[Paper] with title, authors, venue."""
        mock_get.return_value = _dblp_search_response([SAMPLE_DBLP_HIT])

        client = DBLPClient()
        papers = client.search("Attention Is All You Need", limit=5)
        assert len(papers) == 1
        assert isinstance(papers[0], Paper)
        assert papers[0].source == "dblp"
        assert papers[0].title == "Attention Is All You Need"
        assert papers[0].venue == "NeurIPS"
        assert papers[0].authors == ["Ashish Vaswani", "Noam Shazeer"]

    @patch("axm_bib.http.httpx.request")
    def test_search_extracts_doi(self, mock_get: MagicMock) -> None:
        """DOI from info is mapped to paper.doi."""
        mock_get.return_value = _dblp_search_response([SAMPLE_DBLP_HIT])

        client = DBLPClient()
        papers = client.search("Attention", limit=1)
        assert papers[0].doi == SAMPLE_DOI

    @patch("axm_bib.http.httpx.request")
    def test_search_no_doi(self, mock_get: MagicMock) -> None:
        """Paper without DOI has paper.doi is None, no crash."""
        mock_get.return_value = _dblp_search_response([SAMPLE_DBLP_HIT_NO_DOI])

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

    @patch("axm_bib.http.httpx.request")
    def test_search_populates_dblp_key(self, mock_get: MagicMock) -> None:
        """dblp_key field is populated from the hit key."""
        mock_get.return_value = _dblp_search_response([SAMPLE_DBLP_HIT])

        client = DBLPClient()
        papers = client.search("Attention", limit=1)
        assert papers[0].dblp_key == "conf/nips/VaswaniSPUJGKP17"

    @patch("axm_bib.http.httpx.request")
    def test_search_venue_filter(self, mock_get: MagicMock) -> None:
        """Venue filter is included in query params."""
        mock_get.return_value = _dblp_search_response([])

        client = DBLPClient()
        client.search("diffusion venue:NeurIPS:", limit=10)
        _, kwargs = mock_get.call_args
        assert "venue:NeurIPS:" in kwargs["params"]["q"]

    @patch("axm_bib.http.httpx.request")
    def test_empty_results(self, mock_get: MagicMock) -> None:
        """Empty hits returns []."""
        resp = MagicMock()
        resp.status_code = 200
        resp.json.return_value = {
            "result": {"hits": {"@total": "0"}},
        }
        mock_get.return_value = resp

        client = DBLPClient()
        papers = client.search("nonexistent paper xyz", limit=5)
        assert papers == []

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

        client = DBLPClient()
        with pytest.raises(DBLPError):
            client.search("query", limit=5)


# ── BibTeX Tests ─────────────────────────────────────────────────────────


class TestDBLPBibtex:
    """DBLPClient.bibtex() tests."""

    @patch("axm_bib.http.httpx.request")
    def test_bibtex_fetch(self, mock_get: MagicMock) -> None:
        """Returns BibEntry with parsed key."""
        mock_get.return_value = _dblp_bibtex_response(SAMPLE_DBLP_BIBTEX)

        client = DBLPClient()
        entry = client.bibtex("conf/nips/VaswaniSPUJGKP17")
        assert isinstance(entry, BibEntry)
        assert entry.key == "vaswani2017attention"
        assert entry.entry_type == "inproceedings"
        assert "Attention Is All You Need" in entry.raw

    @patch("axm_bib.http.httpx.request")
    def test_bibtex_key_not_found(self, mock_get: MagicMock) -> None:
        """404 raises DBLPError."""
        mock_get.return_value = _dblp_error_response(404)

        client = DBLPClient()
        with pytest.raises(DBLPError):
            client.bibtex("nonexistent/key")


# ── Edge Cases ───────────────────────────────────────────────────────────


class TestDBLPEdgeCases:
    """Edge case tests for DBLPClient."""

    @patch("axm_bib.http.httpx.request")
    def test_title_with_special_chars(self, mock_get: MagicMock) -> None:
        """Special characters in title are URL-encoded correctly."""
        mock_get.return_value = _dblp_search_response([])

        client = DBLPClient()
        client.search("C++ templates", limit=5)
        _, kwargs = mock_get.call_args
        assert "C++ templates" in kwargs["params"]["q"]

    @patch("axm_bib.http.httpx.request")
    def test_dblp_key_with_slashes(self, mock_get: MagicMock) -> None:
        """DBLP key with slashes is handled in URL construction."""
        mock_get.return_value = _dblp_bibtex_response(SAMPLE_DBLP_BIBTEX)

        client = DBLPClient()
        client.bibtex("conf/nips/VaswaniSPUJGKP17")
        # URL must contain the full key path
        assert "conf/nips/VaswaniSPUJGKP17" in mock_get.call_args[0][1]

    @patch("axm_bib.http.httpx.request")
    def test_multiline_authors_split(self, mock_get: MagicMock) -> None:
        """Multiline 'and\\n' separators are split correctly (AXM-245)."""
        multiline_bibtex = (
            "@inproceedings{DBLP:conf/nips/VaswaniSPUJGKP17,\n"
            "  author       = {Ashish Vaswani and\n"
            "                  Noam Shazeer and\n"
            "                  Niki Parmar},\n"
            "  title        = {Attention Is All You Need},\n"
            "  year         = {2017},\n"
            "  booktitle    = {NeurIPS}\n"
            "}"
        )
        mock_get.return_value = _dblp_bibtex_response(multiline_bibtex)

        client = DBLPClient()
        entry = client.bibtex("conf/nips/VaswaniSPUJGKP17")
        assert len(entry.authors) == 3
        assert entry.authors[0] == "Ashish Vaswani"
        assert entry.authors[1] == "Noam Shazeer"
        assert entry.authors[2] == "Niki Parmar"

    @patch("axm_bib.http.httpx.request")
    def test_single_author_bibtex(self, mock_get: MagicMock) -> None:
        """Single author in BibTeX produces one-element list."""
        single_bibtex = (
            "@article{DBLP:journals/corr/abs-2301-12345,\n"
            "  author = {Solo Author},\n"
            "  title  = {A Paper},\n"
            "  year   = {2023}\n"
            "}"
        )
        mock_get.return_value = _dblp_bibtex_response(single_bibtex)

        client = DBLPClient()
        entry = client.bibtex("journals/corr/abs-2301-12345")
        assert entry.authors == ["Solo Author"]

    @patch("axm_bib.http.httpx.request")
    def test_single_author_format(self, mock_get: MagicMock) -> None:
        """Single author (not in list) is handled."""
        hit = {
            **SAMPLE_DBLP_HIT,
            "authors": {"author": {"text": "Solo Author"}},
        }
        mock_get.return_value = _dblp_search_response([hit])

        client = DBLPClient()
        papers = client.search("test", limit=1)
        assert papers[0].authors == ["Solo Author"]
