"""TDD tests for axm_bib.clients.crossref — CrossRef wrapper."""

from __future__ import annotations

from unittest.mock import MagicMock, patch

import pytest

from axm_bib.clients.crossref import CrossRefClient, CrossRefError
from axm_bib.models import BibEntry, Paper
from tests.conftest import SAMPLE_CROSSREF_BIBTEX, SAMPLE_DOI

# The target to mock: standalone function imported into crossref module
_CN_PATCH = "axm_bib.clients.crossref.content_negotiation"


class TestDoiToBibtex:
    """CrossRefClient.doi_to_bibtex() tests."""

    @patch(_CN_PATCH)
    def test_doi_to_bibtex_valid(self, mock_cn: MagicMock) -> None:
        """Valid DOI returns a BibEntry with parsed fields."""
        mock_cn.return_value = SAMPLE_CROSSREF_BIBTEX

        client = CrossRefClient(mailto="test@example.com")
        entry = client.doi_to_bibtex(SAMPLE_DOI)

        assert isinstance(entry, BibEntry)
        assert entry.doi == SAMPLE_DOI
        assert entry.entry_type in ("article", "inproceedings")
        assert len(entry.raw) > 0

    @patch(_CN_PATCH)
    def test_doi_to_bibtex_formats_key(self, mock_cn: MagicMock) -> None:
        """Key uses generate_key() format (author+year+word)."""
        mock_cn.return_value = SAMPLE_CROSSREF_BIBTEX

        client = CrossRefClient()
        entry = client.doi_to_bibtex(SAMPLE_DOI)

        # Key should be lowercase alphanumeric
        assert entry.key.isalnum()
        assert entry.key == entry.key.lower()

    @patch(_CN_PATCH)
    def test_doi_not_found_raises(self, mock_cn: MagicMock) -> None:
        """HTTP 404 raises CrossRefError."""
        mock_cn.side_effect = Exception("404 Not Found")

        client = CrossRefClient()
        with pytest.raises(CrossRefError):
            client.doi_to_bibtex("10.9999/nonexistent")

    def test_malformed_doi_raises(self) -> None:
        """Rejects obviously invalid DOI strings."""
        client = CrossRefClient()
        with pytest.raises(CrossRefError, match=r"[Ii]nvalid DOI"):
            client.doi_to_bibtex("not-a-doi")

    @patch(_CN_PATCH)
    def test_doi_with_special_chars(self, mock_cn: MagicMock) -> None:
        """DOIs with parentheses/special chars are handled."""
        mock_cn.return_value = SAMPLE_CROSSREF_BIBTEX

        client = CrossRefClient()
        entry = client.doi_to_bibtex("10.1000/foo(bar)")
        assert isinstance(entry, BibEntry)

    @patch(_CN_PATCH)
    def test_timeout_raises(self, mock_cn: MagicMock) -> None:
        """Timeout produces CrossRefError."""
        mock_cn.side_effect = TimeoutError("timeout")

        client = CrossRefClient()
        with pytest.raises(CrossRefError):
            client.doi_to_bibtex(SAMPLE_DOI)

    @patch(_CN_PATCH)
    def test_bibtex_with_missing_authors(self, mock_cn: MagicMock) -> None:
        """Graceful handling of BibTeX with no author field."""
        bibtex_no_author = "@article{_2024,\n  title = {Title},\n  year = {2024}\n}"
        mock_cn.return_value = bibtex_no_author

        client = CrossRefClient()
        entry = client.doi_to_bibtex(SAMPLE_DOI)
        assert isinstance(entry, BibEntry)
        # Should still produce a key (fallback behavior)
        assert len(entry.key) > 0


class TestSearchByTitle:
    """CrossRefClient.search_by_title() tests."""

    @patch("axm_bib.clients.crossref.Crossref")
    def test_search_by_title_returns_papers(self, mock_cr_class: MagicMock) -> None:
        """Returns list[Paper] with DOIs."""
        mock_cr = mock_cr_class.return_value
        mock_cr.works.return_value = {
            "message": {
                "items": [
                    {
                        "DOI": SAMPLE_DOI,
                        "title": ["Deep Learning for Citation Analysis"],
                        "author": [
                            {"given": "John", "family": "Smith"},
                        ],
                        "published-print": {"date-parts": [[2024]]},
                    }
                ]
            }
        }

        client = CrossRefClient()
        papers = client.search_by_title("Deep Learning", limit=5)
        assert len(papers) >= 1
        assert isinstance(papers[0], Paper)
        assert papers[0].doi == SAMPLE_DOI

    @patch("axm_bib.clients.crossref.Crossref")
    def test_search_no_results(self, mock_cr_class: MagicMock) -> None:
        """Empty result set returns [] not error."""
        mock_cr = mock_cr_class.return_value
        mock_cr.works.return_value = {"message": {"items": []}}

        client = CrossRefClient()
        papers = client.search_by_title("xyznonexistent", limit=5)
        assert papers == []

    @patch("axm_bib.clients.crossref.Crossref")
    def test_polite_pool_mailto(self, mock_cr_class: MagicMock) -> None:
        """mailto is passed to habanero Crossref constructor."""
        CrossRefClient(mailto="test@example.com")
        mock_cr_class.assert_called_once_with(mailto="test@example.com")
