"""Tests for the arXiv API client."""

import pytest

from arxivy.api.client import ArxivClient
from arxivy.api.errors import ArxivError, NetworkError, NotFoundError, RateLimitError
from arxivy.api.ids import normalize_arxiv_id
from arxivy.api.parser import parse_feed
from arxivy.api.types import SortBy, SortOrder
from arxivy.models import PaperListResult

from .conftest import (
    SAMPLE_EMPTY_FEED,
    SAMPLE_MULTI_FEED,
    SAMPLE_NOT_FOUND_FEED,
    SAMPLE_SEARCH_FEED,
)

# ── ID Normalization ─────────────────────────────────────────


class TestNormalizeArxivId:
    def test_plain_id(self):
        assert normalize_arxiv_id("1706.03762") == "1706.03762"

    def test_versioned_id(self):
        assert normalize_arxiv_id("1706.03762v7") == "1706.03762"

    def test_https_url(self):
        assert normalize_arxiv_id("https://arxiv.org/abs/1706.03762v7") == "1706.03762"

    def test_http_url(self):
        assert normalize_arxiv_id("http://arxiv.org/abs/1706.03762") == "1706.03762"

    def test_old_style_id(self):
        assert normalize_arxiv_id("hep-ph/0601001") == "hep-ph/0601001"

    def test_old_style_versioned(self):
        assert normalize_arxiv_id("hep-ph/0601001v2") == "hep-ph/0601001"

    def test_old_style_url(self):
        assert normalize_arxiv_id("https://arxiv.org/abs/hep-ph/0601001") == "hep-ph/0601001"

    def test_whitespace_stripped(self):
        assert normalize_arxiv_id("  1706.03762  ") == "1706.03762"


# ── XML Parsing ──────────────────────────────────────────────


class TestParseFeed:
    def test_search_feed(self):
        entries, meta = parse_feed(SAMPLE_SEARCH_FEED)
        assert len(entries) == 1
        assert meta.total_results == 1
        assert meta.start_index == 0
        assert meta.items_per_page == 1

    def test_search_feed_entry_fields(self):
        entries, _ = parse_feed(SAMPLE_SEARCH_FEED)
        paper = entries[0]

        assert paper.arxiv_id == "1706.03762"
        assert paper.title == "Attention Is All You Need"
        assert len(paper.authors) == 3
        assert paper.authors[0].name == "Ashish Vaswani"
        assert paper.authors[0].affiliation == "Google Brain"
        assert paper.published == "2017-06-12T17:57:34Z"
        assert paper.primary_category == "cs.CL"
        assert "cs.CL" in paper.categories
        assert "cs.LG" in paper.categories
        assert paper.comment == "15 pages, 5 figures"
        assert "NIPS 2017" in paper.journal_ref
        assert paper.doi == "10.48550/arXiv.1706.03762"
        assert paper.pdf_url == "http://arxiv.org/pdf/1706.03762v7"
        assert paper.abstract_url == "http://arxiv.org/abs/1706.03762v7"

    def test_empty_feed(self):
        entries, meta = parse_feed(SAMPLE_EMPTY_FEED)
        assert entries == []
        assert meta.total_results == 0

    def test_not_found_feed_returns_empty(self):
        """arXiv returns an entry with empty title for invalid IDs; we filter it out."""
        entries, meta = parse_feed(SAMPLE_NOT_FOUND_FEED)
        assert entries == []

    def test_multi_paper_feed(self):
        entries, meta = parse_feed(SAMPLE_MULTI_FEED)
        assert len(entries) == 2
        assert meta.total_results == 2
        assert entries[0].arxiv_id == "1706.03762"
        assert entries[1].arxiv_id == "2010.11929"

    def test_title_whitespace_normalized(self):
        entries, _ = parse_feed(SAMPLE_SEARCH_FEED)
        # Title should have no extra whitespace
        assert "\n" not in entries[0].title
        assert "  " not in entries[0].title

    def test_summary_whitespace_normalized(self):
        entries, _ = parse_feed(SAMPLE_SEARCH_FEED)
        assert "\n" not in entries[0].summary


# ── Error Classes ────────────────────────────────────────────


class TestErrors:
    def test_arxiv_error_to_dict(self):
        e = ArxivError("Something broke", code="TEST", suggestion="Try again")
        d = e.to_dict()
        assert d["error"]["code"] == "TEST"
        assert d["error"]["message"] == "Something broke"
        assert d["error"]["suggestion"] == "Try again"
        assert "documentation" in d["error"]

    def test_not_found_error(self):
        e = NotFoundError("9999.99999")
        assert e.arxiv_id == "9999.99999"
        assert e.code == "NOT_FOUND"
        assert "9999.99999" in e.message

    def test_network_error(self):
        e = NetworkError("Connection refused")
        assert e.code == "NETWORK_ERROR"
        assert "Connection refused" in e.message

    def test_rate_limit_error(self):
        e = RateLimitError(retry_after=12)
        assert e.code == "RATE_LIMITED"
        assert e.status_code == 429
        assert e.retry_after == 12


# ── HTTP Request Parameters ──────────────────────────────────


class TestSearchParams:
    def test_basic_search(self, httpx_mock):
        httpx_mock.add_response(text=SAMPLE_SEARCH_FEED)

        with ArxivClient(throttle_seconds=0) as api:
            result = api.search("attention is all you need")

        assert isinstance(result, PaperListResult)
        assert len(result.results) == 1
        assert result.meta.query == "attention is all you need"

        request = httpx_mock.get_request()
        assert "search_query=all%3Aattention" in str(request.url)

    def test_search_with_category(self, httpx_mock):
        httpx_mock.add_response(text=SAMPLE_SEARCH_FEED)

        with ArxivClient(throttle_seconds=0) as api:
            api.search("test", category="cs.AI")

        request = httpx_mock.get_request()
        url_str = str(request.url)
        assert "cat%3Acs.AI" in url_str
        assert "all%3Atest" in url_str

    def test_search_with_sort(self, httpx_mock):
        httpx_mock.add_response(text=SAMPLE_SEARCH_FEED)

        with ArxivClient(throttle_seconds=0) as api:
            api.search("test", sort_by=SortBy.SUBMITTED_DATE, sort_order=SortOrder.DESCENDING)

        request = httpx_mock.get_request()
        url_str = str(request.url)
        assert "sortBy=submittedDate" in url_str
        assert "sortOrder=descending" in url_str

    def test_search_with_limit_and_offset(self, httpx_mock):
        httpx_mock.add_response(text=SAMPLE_SEARCH_FEED)

        with ArxivClient(throttle_seconds=0) as api:
            api.search("test", limit=20, offset=5)

        request = httpx_mock.get_request()
        url_str = str(request.url)
        assert "max_results=20" in url_str
        assert "start=5" in url_str


class TestGetPaperParams:
    def test_single_paper(self, httpx_mock):
        httpx_mock.add_response(text=SAMPLE_SEARCH_FEED)

        with ArxivClient(throttle_seconds=0) as api:
            result = api.get_paper("1706.03762")

        assert result.arxiv_id == "1706.03762"

        request = httpx_mock.get_request()
        assert "id_list=1706.03762" in str(request.url)

    def test_paper_not_found(self, httpx_mock):
        httpx_mock.add_response(text=SAMPLE_NOT_FOUND_FEED)

        with ArxivClient(throttle_seconds=0) as api:
            with pytest.raises(NotFoundError) as exc:
                api.get_paper("9999.99999")

        assert exc.value.arxiv_id == "9999.99999"

    def test_normalizes_url_id(self, httpx_mock):
        httpx_mock.add_response(text=SAMPLE_SEARCH_FEED)

        with ArxivClient(throttle_seconds=0) as api:
            api.get_paper("https://arxiv.org/abs/1706.03762v7")

        request = httpx_mock.get_request()
        assert "id_list=1706.03762" in str(request.url)


class TestGetPapersParams:
    def test_batch_papers(self, httpx_mock):
        httpx_mock.add_response(text=SAMPLE_MULTI_FEED)

        with ArxivClient(throttle_seconds=0) as api:
            results = api.get_papers(["1706.03762", "2010.11929"])

        assert len(results) == 2
        assert results[0].arxiv_id == "1706.03762"
        assert results[1].arxiv_id == "2010.11929"

        request = httpx_mock.get_request()
        assert "id_list=1706.03762%2C2010.11929" in str(request.url)


class TestGetNewParams:
    def test_new_in_category(self, httpx_mock):
        httpx_mock.add_response(text=SAMPLE_SEARCH_FEED)

        with ArxivClient(throttle_seconds=0) as api:
            result = api.get_new("cs.AI", limit=5)

        assert result.meta.category == "cs.AI"

        request = httpx_mock.get_request()
        url_str = str(request.url)
        assert "cat%3Acs.AI" in url_str
        assert "sortBy=submittedDate" in url_str
        assert "sortOrder=descending" in url_str
        assert "max_results=5" in url_str


# ── Retry & Error Handling ───────────────────────────────────


class TestRetryBehavior:
    def test_retries_on_5xx(self, httpx_mock):
        httpx_mock.add_response(status_code=500)
        httpx_mock.add_response(text=SAMPLE_SEARCH_FEED)

        with ArxivClient(throttle_seconds=0, max_retries=1, status_callback=lambda x: None) as api:
            result = api.search("test")

        assert len(result.results) == 1
        assert len(httpx_mock.get_requests()) == 2

    def test_gives_up_after_max_retries(self, httpx_mock):
        httpx_mock.add_response(status_code=500)
        httpx_mock.add_response(status_code=500)
        httpx_mock.add_response(status_code=500)

        with ArxivClient(throttle_seconds=0, max_retries=2, status_callback=lambda x: None) as api:
            with pytest.raises(ArxivError) as exc:
                api.search("test")

        assert exc.value.code == "SERVER_ERROR"
        assert len(httpx_mock.get_requests()) == 3

    def test_non_retryable_error(self, httpx_mock):
        httpx_mock.add_response(status_code=400)

        with ArxivClient(throttle_seconds=0) as api:
            with pytest.raises(ArxivError) as exc:
                api.search("test")

        assert exc.value.code == "API_ERROR"
        assert len(httpx_mock.get_requests()) == 1

    def test_retries_on_429(self, httpx_mock, monkeypatch):
        monkeypatch.setattr("arxivy.api.client.random.random", lambda: 0.5)
        sleep_calls: list[float] = []
        monkeypatch.setattr("arxivy.api.client.time.sleep", lambda s: sleep_calls.append(float(s)))

        httpx_mock.add_response(status_code=429, headers={"Retry-After": "1"})
        httpx_mock.add_response(text=SAMPLE_SEARCH_FEED)

        with ArxivClient(
            throttle_seconds=0,
            max_retries=1,
            status_callback=lambda x: None,
            retry_on_rate_limit=True,
        ) as api:
            result = api.search("test")

        assert len(result.results) == 1
        assert len(httpx_mock.get_requests()) == 2
        assert sleep_calls == [1.0]

    def test_rate_limit_no_retry_when_disabled(self, httpx_mock):
        httpx_mock.add_response(status_code=429)

        with ArxivClient(
            throttle_seconds=0,
            max_retries=3,
            status_callback=lambda x: None,
            retry_on_rate_limit=False,
        ) as api:
            with pytest.raises(RateLimitError):
                api.search("test")

        assert len(httpx_mock.get_requests()) == 1


class TestThrottle:
    def test_throttle_enforced(self, httpx_mock):
        """Verify that throttle creates a measurable delay between requests."""
        import time

        httpx_mock.add_response(text=SAMPLE_SEARCH_FEED)
        httpx_mock.add_response(text=SAMPLE_SEARCH_FEED)

        with ArxivClient(throttle_seconds=0.1) as api:
            api.search("test1")
            start = time.monotonic()
            api.search("test2")
            elapsed = time.monotonic() - start

        assert elapsed >= 0.08  # Allow small timing tolerance


class TestContextManager:
    def test_context_manager(self, httpx_mock):
        httpx_mock.add_response(text=SAMPLE_SEARCH_FEED)

        with ArxivClient(throttle_seconds=0) as api:
            result = api.search("test")
            assert len(result.results) == 1

        # Client should be closed
        assert api._client is None
