"""Server-side Turnstile verification (tasks 2.10, 8.5).

The open registration endpoint is the only unauthenticated *create* path in the system.
Before this module existed the request carried a `turnstile_token` that nothing ever
checked — a bot defence that rendered in the browser and did nothing on the server.

These tests pin the two properties that matter: an unverified token never gets through, and
a Cloudflare outage fails closed rather than open.
"""

import httpx
import pytest

from app.config import get_settings
from app.services import turnstile


@pytest.fixture
def _live_secret(monkeypatch: pytest.MonkeyPatch):
    """Configure a non-test secret so verification actually calls out."""
    monkeypatch.setenv("TURNSTILE_SECRET_KEY", "0x4AAAAAAArealsecret")
    get_settings.cache_clear()
    yield
    get_settings.cache_clear()


def _stub(monkeypatch: pytest.MonkeyPatch, handler) -> None:
    """Replace the HTTP client with one that answers from `handler`, so the test exercises
    the real request-building and response-parsing code rather than a mock of it."""
    transport = httpx.MockTransport(handler)
    original = httpx.AsyncClient

    def factory(*args, **kwargs):
        kwargs["transport"] = transport
        return original(*args, **kwargs)

    monkeypatch.setattr(httpx, "AsyncClient", factory)


async def test_the_dev_test_key_short_circuits(monkeypatch: pytest.MonkeyPatch) -> None:
    """Cloudflare's 1x0000… secret passes for any input. Skipping the network call keeps
    local development and CI offline, and the prod guard refuses to boot on this key."""
    monkeypatch.setenv("TURNSTILE_SECRET_KEY", "1x0000000000000000000000000000000AA")
    get_settings.cache_clear()
    try:
        assert await turnstile.verify(None) is True
    finally:
        get_settings.cache_clear()


@pytest.mark.usefixtures("_live_secret")
async def test_a_missing_token_is_rejected_without_calling_out() -> None:
    """No token means the widget was never solved. There is nothing to verify."""
    assert await turnstile.verify(None) is False
    assert await turnstile.verify("") is False


@pytest.mark.usefixtures("_live_secret")
async def test_a_token_cloudflare_accepts_passes(monkeypatch: pytest.MonkeyPatch) -> None:
    _stub(monkeypatch, lambda request: httpx.Response(200, json={"success": True}))
    assert await turnstile.verify("a-real-widget-token") is True


@pytest.mark.usefixtures("_live_secret")
async def test_a_token_cloudflare_rejects_fails(monkeypatch: pytest.MonkeyPatch) -> None:
    _stub(
        monkeypatch,
        lambda request: httpx.Response(
            200, json={"success": False, "error-codes": ["invalid-input-response"]}
        ),
    )
    assert await turnstile.verify("a-forged-token") is False


@pytest.mark.usefixtures("_live_secret")
async def test_a_reused_token_fails(monkeypatch: pytest.MonkeyPatch) -> None:
    """Turnstile tokens are single-use; Cloudflare returns this code on the second
    exchange, which is what stops one solved challenge fronting a thousand submissions."""
    _stub(
        monkeypatch,
        lambda request: httpx.Response(
            200, json={"success": False, "error-codes": ["timeout-or-duplicate"]}
        ),
    )
    assert await turnstile.verify("a-replayed-token") is False


@pytest.mark.usefixtures("_live_secret")
async def test_the_secret_is_sent_and_the_token_is_never_logged(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """The secret must reach Cloudflare in the body, not the query string — a URL ends up
    in proxy logs."""
    seen: dict[str, str] = {}

    def handler(request: httpx.Request) -> httpx.Response:
        seen["url"] = str(request.url)
        seen["body"] = request.content.decode()
        return httpx.Response(200, json={"success": True})

    _stub(monkeypatch, handler)
    await turnstile.verify("widget-token", remote_ip="203.0.113.9")

    assert "secret" in seen["body"] and "widget-token" in seen["body"]
    assert "remoteip=203.0.113.9" in seen["body"]
    assert "?" not in seen["url"], "credentials must not travel in the query string"


@pytest.mark.usefixtures("_live_secret")
async def test_an_unreachable_cloudflare_fails_closed(monkeypatch: pytest.MonkeyPatch) -> None:
    """The important one. If a network blip made this return True, every bot would sail
    through for the duration of the outage — and nothing would look wrong at the time."""

    def handler(request: httpx.Request) -> httpx.Response:
        raise httpx.ConnectError("cloudflare unreachable")

    _stub(monkeypatch, handler)
    assert await turnstile.verify("a-real-token") is False


@pytest.mark.usefixtures("_live_secret")
async def test_a_server_error_from_cloudflare_fails_closed(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    _stub(monkeypatch, lambda request: httpx.Response(502, text="bad gateway"))
    assert await turnstile.verify("a-real-token") is False


@pytest.mark.usefixtures("_live_secret")
async def test_a_non_json_response_fails_closed(monkeypatch: pytest.MonkeyPatch) -> None:
    _stub(monkeypatch, lambda request: httpx.Response(200, text="<html>captive portal</html>"))
    assert await turnstile.verify("a-real-token") is False


def test_the_open_registration_route_verifies_before_writing() -> None:
    """A guard against the regression this module was written to fix: the endpoint
    accepting a token it never checks."""
    import inspect

    from app.routers import rsvp as rsvp_router

    source = inspect.getsource(rsvp_router.open_link_rsvp)
    assert "turnstile.verify" in source, "open registration must verify the Turnstile token"
    # Ordering matters: verifying after the guest row is written defeats the point.
    assert source.index("turnstile.verify") < source.index("resolve_events")
