"""Tests for REST API error handling: HTTP error codes, missing/malformed input,
and rate-limit behavior across all REST endpoint groups.

Covers:
  - 422 Unprocessable Entity: missing required query parameters (FastAPI validation)
  - 400 Bad Request: malformed refs, invalid enum values, wrong ref type
  - 404 Not Found: nonexistent module names, unresolved book names, missing headwords
  - 429 Too Many Requests: rate-limit enforcement (< 100 ms inter-request gap)
"""

from __future__ import annotations


import pytest

try:
    from accordance_api import app, _last_request_time, _modules
    from fastapi.testclient import TestClient

    _has_api = True
except Exception:
    _has_api = False


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


def _find_any_lexicon() -> str:
    """Return a real lexicon abbreviation from the registry, or a fallback."""
    for abbr, info in sorted(_modules.items()):
        if info["type"] in (2, 3, 4) and info.get("path"):
            return abbr
    return "BDAG"


def _find_any_commentary() -> str:
    """Return a real commentary abbreviation from the registry, or a fallback."""
    for abbr, info in sorted(_modules.items()):
        if info["type"] in (5, 6) and info.get("path"):
            return abbr
    return "JFB"


def _find_any_acc2() -> str | None:
    """Return a real .acc2 abbreviation, or None if none available."""
    for abbr, info in sorted(_modules.items()):
        if info["type"] == 15 and info.get("path"):
            return abbr
    return None


# =============================================================================
# Group A: 422 — Missing Required Query Parameters (FastAPI auto-validation)
# =============================================================================


@pytest.mark.skipif(not _has_api, reason="accordance_api not available")
class TestMissingParams422:
    """Every endpoint with required query params returns 422 when they're absent."""

    def setup_method(self):
        self.client = TestClient(app)

    # ── Bible endpoints ─────────────────────────────────────────────────────

    def test_bible_verse_missing_ref_422(self):
        resp = self.client.get("/bibles/KJVS/verses")
        assert resp.status_code == 422, (
            f"Expected 422, got {resp.status_code}: {resp.text[:200]}"
        )

    def test_bible_interlinear_missing_ref_422(self):
        resp = self.client.get("/bibles/KJVS/interlinear")
        assert resp.status_code == 422, f"Got {resp.status_code}: {resp.text[:200]}"

    def test_bible_range_missing_params_422(self):
        resp = self.client.get("/bibles/KJVS/verses/range")
        assert resp.status_code == 422, f"Got {resp.status_code}: {resp.text[:200]}"

    # ── Lexicon endpoints ───────────────────────────────────────────────────

    def test_lexicon_lookup_missing_headword_422(self):
        lex = _find_any_lexicon()
        resp = self.client.get(f"/lexicons/{lex}/lookup")
        assert resp.status_code == 422, f"Got {resp.status_code}: {resp.text[:200]}"

    def test_lexicon_search_missing_q_422(self):
        lex = _find_any_lexicon()
        resp = self.client.get(f"/lexicons/{lex}/search")
        assert resp.status_code == 422, f"Got {resp.status_code}: {resp.text[:200]}"

    def test_lexicon_metadata_missing_headword_422(self):
        lex = _find_any_lexicon()
        resp = self.client.get(f"/lexicons/{lex}/metadata")
        assert resp.status_code == 422, f"Got {resp.status_code}: {resp.text[:200]}"

    # ── Commentary endpoints ────────────────────────────────────────────────

    def test_commentary_section_missing_ref_422(self):
        cm = _find_any_commentary()
        resp = self.client.get(f"/commentaries/{cm}/section")
        assert resp.status_code == 422, f"Got {resp.status_code}: {resp.text[:200]}"

    def test_commentary_sections_missing_params_422(self):
        cm = _find_any_commentary()
        resp = self.client.get(f"/commentaries/{cm}/sections")
        assert resp.status_code == 422, f"Got {resp.status_code}: {resp.text[:200]}"

    # ── Search endpoints ────────────────────────────────────────────────────

    def test_search_missing_q_422(self):
        resp = self.client.get("/search")
        assert resp.status_code == 422, f"Got {resp.status_code}: {resp.text[:200]}"

    def test_search_transliterated_missing_q_422(self):
        resp = self.client.get("/search/transliterated")
        assert resp.status_code == 422, f"Got {resp.status_code}: {resp.text[:200]}"

    # ── Morphology endpoint ─────────────────────────────────────────────────

    def test_morphology_missing_ref_422(self):
        resp = self.client.get("/morphology")
        assert resp.status_code == 422, f"Got {resp.status_code}: {resp.text[:200]}"

    # ── Canon endpoints ─────────────────────────────────────────────────────

    def test_canon_resolve_missing_name_422(self):
        resp = self.client.get("/canon/resolve")
        assert resp.status_code == 422, f"Got {resp.status_code}: {resp.text[:200]}"

    def test_canon_parse_missing_ref_422(self):
        resp = self.client.get("/canon/parse")
        assert resp.status_code == 422, f"Got {resp.status_code}: {resp.text[:200]}"


# =============================================================================
# Group B: 400 — Malformed / Invalid Input
# =============================================================================


@pytest.mark.skipif(not _has_api, reason="accordance_api not available")
class TestMalformedInput400:
    """Invalid enum values, unparseable refs, wrong endpoint ref type → 400."""

    def setup_method(self):
        self.client = TestClient(app)

    # ── Bible endpoints: bad references ─────────────────────────────────────

    def test_bible_verse_invalid_ref_400(self):
        resp = self.client.get("/bibles/KJVS/verses",
                               params={"ref": "not_a_reference"})
        assert resp.status_code == 400, (
            f"Expected 400, got {resp.status_code}: {resp.text[:200]}"
        )
        assert "Invalid reference" in resp.text, (
            f"Expected 'Invalid reference' in body: {resp.text[:200]}"
        )

    def test_bible_range_no_verse_number_400(self):
        """Start ref is chapter-only ('Genesis 1'), which has no verse number."""
        resp = self.client.get("/bibles/KJVS/verses/range",
                               params={"start": "Genesis 1", "end": "Genesis 1:5"})
        assert resp.status_code == 400, f"Got {resp.status_code}: {resp.text[:200]}"

    def test_bible_chapter_with_verse_ref_400(self):
        """Chapter endpoint rejects a verse reference."""
        resp = self.client.get("/bibles/KJVS/verses/chapter",
                               params={"ref": "Genesis 1:1"})
        assert resp.status_code == 400, f"Got {resp.status_code}: {resp.text[:200]}"

    # ── Lexicon: invalid enum ───────────────────────────────────────────────

    def test_lexicon_lookup_invalid_summary_level_400(self):
        lex = _find_any_lexicon()
        resp = self.client.get(f"/lexicons/{lex}/lookup",
                               params={"headword": "test", "summary_level": "invalid"})
        assert resp.status_code == 400, f"Got {resp.status_code}: {resp.text[:200]}"

    # ── Search: invalid type filter ─────────────────────────────────────────

    def test_search_invalid_type_400(self):
        resp = self.client.get("/search", params={"q": "test", "type": "invalid"})
        assert resp.status_code == 400, f"Got {resp.status_code}: {resp.text[:200]}"

    # ── Lectionary: bad date string ─────────────────────────────────────────

    def test_lectionary_invalid_date_400(self):
        resp = self.client.get("/lectionary", params={"date": "not-a-date"})
        assert resp.status_code == 400, f"Got {resp.status_code}: {resp.text[:200]}"

    # ── Canon: invalid enum values ──────────────────────────────────────────

    def test_canon_parse_invalid_canon_400(self):
        resp = self.client.get("/canon/parse",
                               params={"ref": "John 3:16", "canon": "invalid"})
        assert resp.status_code == 400, f"Got {resp.status_code}: {resp.text[:200]}"

    def test_canon_books_invalid_canon_400(self):
        resp = self.client.get("/canon/books", params={"canon": "invalid"})
        assert resp.status_code == 400, f"Got {resp.status_code}: {resp.text[:200]}"

    # ── Acc2: paragraph number out of range ─────────────────────────────────

    def test_acc2_paragraph_below_one_400(self):
        acc2_mod = _find_any_acc2()
        if acc2_mod is None:
            # Even with a nonexistent module, the n < 1 check in the handler
            # runs before module lookup — 400 is guaranteed.
            resp = self.client.get("/acc2/FAKE/paragraph/0")
        else:
            resp = self.client.get(f"/acc2/{acc2_mod}/paragraph/0")
        assert resp.status_code == 400, f"Got {resp.status_code}: {resp.text[:200]}"


# =============================================================================
# Group C: 404 — Nonexistent Resources
# =============================================================================


@pytest.mark.skipif(not _has_api, reason="accordance_api not available")
class TestNotFound404:
    """Nonexistent module names, unresolved books → 404."""

    def setup_method(self):
        self.client = TestClient(app)

    def test_lexicon_nonexistent_module_404(self):
        resp = self.client.get("/lexicons/NONEXISTENT/lookup",
                               params={"headword": "test"})
        assert resp.status_code == 404, f"Got {resp.status_code}: {resp.text[:200]}"

    def test_commentary_nonexistent_module_404(self):
        resp = self.client.get("/commentaries/NONEXISTENT/section",
                               params={"ref": "John 3:16"})
        assert resp.status_code == 404, f"Got {resp.status_code}: {resp.text[:200]}"

    def test_acc2_nonexistent_module_404(self):
        resp = self.client.get("/acc2/NONEXISTENT/paragraph/1")
        assert resp.status_code == 404, f"Got {resp.status_code}: {resp.text[:200]}"

    def test_canon_resolve_book_not_found_404(self):
        resp = self.client.get("/canon/resolve", params={"name": "Zorgon"})
        assert resp.status_code == 404, f"Got {resp.status_code}: {resp.text[:200]}"

    def test_lexicon_headword_not_found_404(self):
        """Real module, fake headword → 404."""
        lex = _find_any_lexicon()
        resp = self.client.get(f"/lexicons/{lex}/lookup",
                               params={"headword": "ZZZZNOTAREALHEADWORD98765"})
        assert resp.status_code == 404, f"Got {resp.status_code}: {resp.text[:200]}"


# =============================================================================
# Group D: 429 — Rate Limiting
# =============================================================================


@pytest.mark.skipif(not _has_api, reason="accordance_api not available")
class TestRateLimit429:
    """Rate-limit middleware enforces >= 100 ms inter-request gap."""

    def setup_method(self):
        self.client = TestClient(app)
        _last_request_time.clear()

    def teardown_method(self):
        _last_request_time.clear()

    def test_two_rapid_requests_429(self):
        """First request succeeds (200); second from same non-whitelisted IP
        within 100 ms returns 429 with 'Too many requests' detail."""
        # Create a TestClient with a non-whitelisted IP so the rate-limit
        # middleware is exercised (default "testclient" is excluded).
        fake_ip = ("10.99.88.77", 50000)
        rate_client = TestClient(app, client=fake_ip)

        # Request 1 — should pass
        resp1 = rate_client.get("/health")
        assert resp1.status_code == 200, (
            f"First request should succeed, got {resp1.status_code}: {resp1.text[:200]}"
        )

        # Request 2 — immediate, should be rate-limited
        resp2 = rate_client.get("/health")
        assert resp2.status_code == 429, (
            f"Expected 429, got {resp2.status_code}: {resp2.text[:200]}"
        )
        body = resp2.json()
        assert "detail" in body, f"Expected 'detail' key in 429 body: {body}"
        assert "Too many requests" in body["detail"], (
            f"Expected 'Too many requests' message, got: {body['detail']}"
        )

    def test_whitelisted_ip_no_rate_limit(self):
        """TestClient default IP ('testclient') and localhost (127.0.0.1) are
        exempt from rate limiting."""
        # Default TestClient IP is "testclient"
        resp1 = self.client.get("/health")
        assert resp1.status_code == 200
        resp2 = self.client.get("/health")
        assert resp2.status_code == 200, (
            f"Whitelisted IP should not be rate-limited, got {resp2.status_code}"
        )
