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

from __future__ import annotations

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

import pytest

from axm_bib.clients.openreview import OpenReviewClient, OpenReviewError
from axm_bib.models import BibEntry, Paper

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


def _or_search_response(notes: list[dict[str, Any]]) -> MagicMock:
    """Build a mock httpx.Response with OpenReview notes JSON."""
    resp = MagicMock()
    resp.status_code = 200
    resp.json.return_value = {"notes": notes, "count": len(notes)}
    return resp


def _or_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 (API v2 format: values wrapped in {value: "..."}) ────────

SAMPLE_NOTE_V2: dict[str, Any] = {
    "id": "abc123forum",
    "forum": "abc123forum",
    "content": {
        "title": {"value": "Attention Is All You Need"},
        "authors": {
            "value": ["Ashish Vaswani", "Noam Shazeer"],
        },
        "venue": {"value": "NeurIPS 2017"},
        "abstract": {"value": "The dominant sequence transduction models..."},
        "_bibtex": {
            "value": (
                "@inproceedings{vaswani2017attention,\n"
                "  title={Attention Is All You Need},\n"
                "  author={Ashish Vaswani and Noam Shazeer},\n"
                "  year={2017},\n"
                "  booktitle={NeurIPS}\n"
                "}"
            ),
        },
    },
    "pdate": 1512345678000,
}

SAMPLE_NOTE_V2_NO_AUTHORS: dict[str, Any] = {
    "id": "noauth456",
    "forum": "noauth456",
    "content": {
        "title": {"value": "Paper Without Authors"},
        "venue": {"value": "Workshop 2023"},
    },
    "pdate": 1672531200000,
}

SAMPLE_NOTE_V2_WITH_PDF: dict[str, Any] = {
    "id": "pdfnote789",
    "forum": "pdfnote789",
    "content": {
        "title": {"value": "Paper With PDF"},
        "authors": {"value": ["Author One"]},
        "venue": {"value": "ICLR 2024"},
        "pdf": {"value": "/pdf/pdfnote789.pdf"},
    },
    "pdate": 1704067200000,
}

# API v1 format: values are plain strings (no {value: ...} wrapper)
SAMPLE_NOTE_V1: dict[str, Any] = {
    "id": "v1note999",
    "forum": "v1note999",
    "content": {
        "title": "Legacy API Format Paper",
        "authors": ["Legacy Author"],
        "venue": "NeurIPS 2020",
    },
    "pdate": 1609459200000,
}


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


class TestOpenReviewSearch:
    """OpenReviewClient.search() tests."""

    @patch("axm_bib.http.httpx.request")
    def test_search_returns_papers(self, mock_get: MagicMock) -> None:
        """Returns list[Paper] with normalized fields."""
        mock_get.return_value = _or_search_response([SAMPLE_NOTE_V2])

        client = OpenReviewClient()
        papers = client.search("Attention Is All You Need", limit=5)
        assert len(papers) == 1
        assert isinstance(papers[0], Paper)
        assert papers[0].source == "openreview"
        assert papers[0].title == "Attention Is All You Need"

    @patch("axm_bib.http.httpx.request")
    def test_search_extracts_forum_id(self, mock_get: MagicMock) -> None:
        """Forum ID is mapped to paper.openreview_id."""
        mock_get.return_value = _or_search_response([SAMPLE_NOTE_V2])

        client = OpenReviewClient()
        papers = client.search("Attention", limit=1)
        assert papers[0].openreview_id == "abc123forum"

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

        client = OpenReviewClient()
        with pytest.raises(OpenReviewError):
            client.search("query", limit=5)


# ── Venue Papers Tests ───────────────────────────────────────────────────


class TestOpenReviewVenuePapers:
    """OpenReviewClient.venue_papers() tests."""

    @patch("axm_bib.http.httpx.request")
    def test_venue_papers_filters(self, mock_get: MagicMock) -> None:
        """Venue ID filter is sent in params."""
        mock_get.return_value = _or_search_response([SAMPLE_NOTE_V2])

        client = OpenReviewClient()
        client.venue_papers("NeurIPS.cc/2024/Conference", limit=10)
        _, kwargs = mock_get.call_args
        assert kwargs["params"]["content.venueid"] == "NeurIPS.cc/2024/Conference"


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


class TestOpenReviewBibtex:
    """OpenReviewClient.bibtex() tests."""

    @patch("axm_bib.http.httpx.request")
    def test_bibtex_generation(self, mock_get: MagicMock) -> None:
        """Valid @inproceedings BibTeX is generated from metadata."""
        mock_get.return_value = _or_search_response([SAMPLE_NOTE_V2])

        client = OpenReviewClient()
        entry = client.bibtex("abc123forum")
        assert isinstance(entry, BibEntry)
        assert entry.entry_type == "inproceedings"
        assert entry.title == "Attention Is All You Need"
        assert "Ashish Vaswani" in entry.raw
        assert entry.key == "vaswani2017attention"


# ── Content Format Tests ─────────────────────────────────────────────────


class TestOpenReviewContentFormat:
    """API v1 vs v2 content format handling."""

    @patch("axm_bib.http.httpx.request")
    def test_api_v2_content_unwrap(self, mock_get: MagicMock) -> None:
        """v2 {value: "..."} wrapper is unwrapped correctly."""
        mock_get.return_value = _or_search_response([SAMPLE_NOTE_V2])

        client = OpenReviewClient()
        papers = client.search("test", limit=1)
        assert papers[0].title == "Attention Is All You Need"
        assert papers[0].authors == ["Ashish Vaswani", "Noam Shazeer"]

    @patch("axm_bib.http.httpx.request")
    def test_api_v1_format(self, mock_get: MagicMock) -> None:
        """v1 plain value format is handled."""
        mock_get.return_value = _or_search_response([SAMPLE_NOTE_V1])

        client = OpenReviewClient()
        papers = client.search("test", limit=1)
        assert papers[0].title == "Legacy API Format Paper"
        assert papers[0].authors == ["Legacy Author"]


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


class TestOpenReviewEdgeCases:
    """Edge case tests for OpenReviewClient."""

    @patch("axm_bib.http.httpx.request")
    def test_missing_authors(self, mock_get: MagicMock) -> None:
        """Note without authors falls back to 'Unknown'."""
        mock_get.return_value = _or_search_response(
            [SAMPLE_NOTE_V2_NO_AUTHORS],
        )

        client = OpenReviewClient()
        papers = client.search("test", limit=1)
        assert papers[0].authors == ["Unknown"]

    @patch("axm_bib.http.httpx.request")
    def test_pdf_url_extraction(self, mock_get: MagicMock) -> None:
        """PDF URL is extracted from note content."""
        mock_get.return_value = _or_search_response(
            [SAMPLE_NOTE_V2_WITH_PDF],
        )

        client = OpenReviewClient()
        papers = client.search("test", limit=1)
        expected = "https://openreview.net/pdf/pdfnote789.pdf"
        assert papers[0].open_access_pdf == expected


# ── v1 API Fallback Tests ────────────────────────────────────────────────

# ICLR 2023 note (stored in v1 API)
SAMPLE_NOTE_V1_ICLR: dict[str, Any] = {
    "id": "_nGgzQjzaRy",
    "forum": "_nGgzQjzaRy",
    "content": {
        "title": "Decomposed Prompting",
        "authors": ["Tushar Khot"],
        "venue": "ICLR 2023",
    },
    "pdate": 1672531200000,
}


class TestFetchNoteV1Fallback:
    """_fetch_note() v1 API fallback tests."""

    @patch("axm_bib.http.httpx.request")
    def test_v1_fallback_when_v2_empty(self, mock_get: MagicMock) -> None:
        """AC4+AC5: v2 returns nothing → v1 returns the note."""
        v2_empty = _or_search_response([])  # empty notes for v2

        def _side_effect(*args: Any, **kwargs: Any) -> MagicMock:
            url = args[1] if len(args) > 1 else kwargs.get("url", "")
            if "api2.openreview.net" in url:
                return v2_empty
            # v1 endpoint returns the note
            return _or_search_response([SAMPLE_NOTE_V1_ICLR])

        mock_get.side_effect = _side_effect

        client = OpenReviewClient()
        note = client._fetch_note("_nGgzQjzaRy")
        assert note["id"] == "_nGgzQjzaRy"
        assert note["content"]["title"] == "Decomposed Prompting"

    @patch("axm_bib.http.httpx.request")
    def test_v1_also_empty_raises(self, mock_get: MagicMock) -> None:
        """Edge: both v2 and v1 return nothing → OpenReviewError."""
        empty = _or_search_response([])
        mock_get.return_value = empty

        client = OpenReviewClient()
        with pytest.raises(OpenReviewError, match="No note found"):
            client._fetch_note("nonexistent")
