"""Tests for middleware stack: CORS headers, rate limiting, dev auth flow.

Critical invariants:
- ALL responses (including 429) must carry CORS headers when origin is allowed
- Dev login flow must survive repeated logins without hitting rate limit
- OPTIONS preflight must never be rate-limited

Note: each test uses a unique X-Forwarded-For IP to isolate rate limit counters,
since the rate limiter is in-memory and shared across the app instance.
"""

from __future__ import annotations

import pytest
from httpx import ASGITransport, AsyncClient

import app.models  # noqa: F401 — registers all models with Base.metadata before create_all
from app.database import init_db
from app.main import app


@pytest.fixture(scope="module", autouse=True)
async def _create_tables():
    """Create DB tables once for this module.

    ASGITransport does not trigger the FastAPI lifespan, so init_db() would
    never run. Tests that hit endpoints backed by the DB need the tables to
    exist beforehand.
    """
    await init_db()

ORIGIN = "http://localhost:3002"
CHALLENGE_URL = "/api/v1/auth/challenge"
VERIFY_URL = "/api/v1/auth/verify"
ME_URL = "/api/v1/auth/me"

DEV_ADDRESS = "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY"


# ---------------------------------------------------------------------------
# INVARIANT 1: 429 responses must include CORS headers
# Without this, the browser sees "Failed to fetch" instead of "Too Many Requests"
# ---------------------------------------------------------------------------


@pytest.mark.anyio
async def test_rate_limited_response_has_cors_headers():
    """A 429 from the rate limiter must still carry Access-Control-Allow-Origin.

    Root cause of the "no response / Failed to fetch" bug: the rate limiter
    sits outside CORS in the middleware stack, so its 429 responses have no
    CORS headers and the browser discards them as network errors.
    """
    # dev auth limit = 60/min (RATE_LIMIT_DEFAULT), prod = 10/min (RATE_LIMIT_AUTH)
    # Send 65 requests to guarantee 429 regardless of environment.
    transport = ASGITransport(app=app)
    async with AsyncClient(transport=transport, base_url="http://test") as client:
        last_response = None
        for _ in range(65):
            resp = await client.post(
                CHALLENGE_URL,
                json={"address": DEV_ADDRESS},
                headers={"Origin": ORIGIN, "X-Forwarded-For": "10.0.1.1"},
            )
            last_response = resp
            if resp.status_code == 429:
                break

        assert last_response is not None
        assert last_response.status_code == 429, (
            "Expected 429 after exceeding auth rate limit"
        )
        assert "access-control-allow-origin" in last_response.headers, (
            "429 response must include CORS headers so the browser can read the error"
        )
        assert last_response.headers["access-control-allow-origin"] == ORIGIN


# ---------------------------------------------------------------------------
# INVARIANT 2: OPTIONS preflight must never be rate-limited
# ---------------------------------------------------------------------------


@pytest.mark.anyio
async def test_options_preflight_never_rate_limited():
    """OPTIONS requests must pass through regardless of request count.

    Browsers send a preflight before every cross-origin POST with custom headers.
    A 429 on OPTIONS prevents the real request from ever being sent.
    """
    transport = ASGITransport(app=app)
    async with AsyncClient(transport=transport, base_url="http://test") as client:
        for i in range(20):
            resp = await client.options(
                CHALLENGE_URL,
                headers={
                    "Origin": ORIGIN,
                    "Access-Control-Request-Method": "POST",
                    "Access-Control-Request-Headers": "content-type",
                    "X-Forwarded-For": "10.0.2.1",
                },
            )
            assert resp.status_code != 429, (
                f"OPTIONS request #{i + 1} was rate-limited (429) — preflights must never be blocked"
            )


# ---------------------------------------------------------------------------
# INVARIANT 3: Dev login flow must survive ≥ 10 consecutive logins
# (challenge + verify cycle, dev profile bypass)
# ---------------------------------------------------------------------------


@pytest.mark.anyio
async def test_dev_login_survives_repeated_cycles():
    """Complete login cycle (challenge → verify) must work ≥ 10 times in a row.

    In dev mode, the developer disconnects and reconnects frequently.
    With auth rate limit = 10/min, the 6th challenge request would be blocked.
    Dev mode must use a higher limit (≥ 60/min) to prevent login lockout.
    """
    transport = ASGITransport(app=app)
    async with AsyncClient(transport=transport, base_url="http://test") as client:
        tokens = []
        for i in range(10):
            # Step 1: get challenge
            ch_resp = await client.post(
                CHALLENGE_URL,
                json={"address": DEV_ADDRESS},
                headers={"Origin": ORIGIN, "X-Forwarded-For": "10.0.3.1"},
            )
            assert ch_resp.status_code == 200, (
                f"Login cycle #{i + 1}: challenge returned {ch_resp.status_code} — "
                f"rate limit likely hit. Dev mode requires RATE_LIMIT_AUTH ≥ 60/min."
            )
            challenge = ch_resp.json()["challenge"]

            # Step 2: verify (dev bypass — any signature accepted for dev addresses)
            v_resp = await client.post(
                VERIFY_URL,
                json={
                    "address": DEV_ADDRESS,
                    "challenge": challenge,
                    "signature": "0x" + "ab" * 64,
                },
                headers={"Origin": ORIGIN, "X-Forwarded-For": "10.0.3.1"},
            )
            assert v_resp.status_code == 200, (
                f"Login cycle #{i + 1}: verify returned {v_resp.status_code}"
            )
            tokens.append(v_resp.json()["token"])

        assert len(tokens) == 10, "Expected 10 successful login cycles"
        assert len(set(tokens)) == 10, "Each login cycle must produce a unique token"


# ---------------------------------------------------------------------------
# INVARIANT 4: /auth/me OPTIONS preflight must return CORS headers
# ---------------------------------------------------------------------------


@pytest.mark.anyio
async def test_auth_me_options_preflight_returns_cors():
    """OPTIONS preflight for /auth/me must return 200 with CORS headers.

    This was the root cause of the session-lost-on-reload bug:
    repeated /auth/me calls would exhaust the auth rate limit,
    the 429 OPTIONS response had no CORS headers,
    and the browser threw 'Failed to fetch'.
    """
    transport = ASGITransport(app=app)
    async with AsyncClient(transport=transport, base_url="http://test") as client:
        resp = await client.options(
            ME_URL,
            headers={
                "Origin": ORIGIN,
                "Access-Control-Request-Method": "GET",
                "Access-Control-Request-Headers": "authorization",
                "X-Forwarded-For": "10.0.4.1",
            },
        )
        assert resp.status_code in (200, 204), (
            f"OPTIONS /auth/me returned {resp.status_code}"
        )
        assert "access-control-allow-origin" in resp.headers
