from __future__ import annotations

import base64
import json
from unittest.mock import patch

import pytest
from starlette.applications import Starlette
from starlette.requests import Request
from starlette.responses import JSONResponse
from starlette.routing import Route
from starlette.testclient import TestClient

from a402 import A402Config, A402Middleware

CHALLENGE = {"x402Version": 1, "accepts": [{"scheme": "exact", "network": "Base Sepolia", "nonce": "0xabc"}]}
SETTLED = {"payment": {"status": "settled", "txHash": "0xtx"}}
XP = {"apiKeyId": "ak_test", "network": "Base Sepolia",
      "authorization": {"from": "0xf", "to": "0xm", "value": "50000", "validAfter": "0", "validBefore": "9999999999", "nonce": "0xabc"},
      "signature": "0xsig"}


def _app(gate_routes=None, route_methods=("GET",)):
    async def gated(_: Request):
        return JSONResponse({"data": "secret"})

    async def public(_: Request):
        return JSONResponse({"ok": True})

    app = Starlette(routes=[
        Route("/r", gated, methods=list(route_methods)),
        Route("/public", public, methods=["GET"]),
    ])
    app.add_middleware(
        A402Middleware,
        config=A402Config(facilitator_url="https://f.test", api_key="sk_seller"),
        routes=gate_routes if gate_routes is not None else [{"method": "GET", "path": "/r", "price": "$0.05"}],
    )
    return TestClient(app, raise_server_exceptions=False)


def _xp_header(obj=XP) -> str:
    return base64.b64encode(json.dumps(obj).encode()).decode()


# ── routing ──────────────────────────────────────────────────────────────────
def test_non_gated_passthrough():
    assert _app().get("/public").json() == {"ok": True}


def test_method_mismatch_not_gated():
    # POST to a path gated only for GET → must NOT be gated (no 402).
    c = _app(route_methods=("GET", "POST"))
    with patch("a402.middleware.issue_challenge", return_value=CHALLENGE):
        r = c.post("/r")
    assert r.status_code != 402
    assert r.json() == {"data": "secret"}


def test_method_case_insensitive():
    c = _app(gate_routes=[{"method": "get", "path": "/r", "price": "$0.05"}])
    with patch("a402.middleware.issue_challenge", return_value=CHALLENGE):
        r = c.get("/r")
    assert r.status_code == 402


def test_multiple_routes_gated():
    c = _app(gate_routes=[{"path": "/r", "price": "$0.01"}, {"method": "GET", "path": "/public", "price": 1}])
    with patch("a402.middleware.issue_challenge", return_value=CHALLENGE):
        assert c.get("/r").status_code == 402
        assert c.get("/public").status_code == 402


# ── challenge ─────────────────────────────────────────────────────────────────
def test_no_payment_402():
    with patch("a402.middleware.issue_challenge", return_value=CHALLENGE):
        assert _app().get("/r").status_code == 402


def test_challenge_error_502():
    with patch("a402.middleware.issue_challenge", side_effect=Exception("down")):
        assert _app().get("/r").status_code == 502


# ── X-PAYMENT decoding ─────────────────────────────────────────────────────────
def test_base64_payment_reaches_settle():
    with patch("a402.middleware.settle", return_value=(200, SETTLED)) as s:
        _app().get("/r", headers={"X-PAYMENT": _xp_header()})
        assert s.called


def test_raw_json_payment_reaches_settle():
    with patch("a402.middleware.settle", return_value=(200, SETTLED)) as s:
        r = _app().get("/r", headers={"X-PAYMENT": json.dumps(XP)})
        assert s.called, "raw-JSON X-PAYMENT must reach settle"
        assert r.status_code != 400


def test_malformed_payment_400():
    with patch("a402.middleware.settle", return_value=(200, SETTLED)):
        r = _app().get("/r", headers={"X-PAYMENT": "!!!@@@not-anything!!!"})
    assert r.status_code == 400
    assert "malformed" in r.json()["error"]


# ── settle ──────────────────────────────────────────────────────────────────
def test_settle_200_passthrough_and_header():
    with patch("a402.middleware.settle", return_value=(200, SETTLED)):
        r = _app().get("/r", headers={"X-PAYMENT": _xp_header()})
    assert r.status_code == 200
    assert r.json() == {"data": "secret"}
    decoded = json.loads(base64.b64decode(r.headers["X-PAYMENT-RESPONSE"]))
    assert decoded == SETTLED


def test_settle_201_is_success():
    with patch("a402.middleware.settle", return_value=(201, SETTLED)):
        r = _app().get("/r", headers={"X-PAYMENT": _xp_header()})
    assert r.status_code == 200
    assert r.json() == {"data": "secret"}


@pytest.mark.parametrize("status", [402, 403, 500])
def test_settle_non_success_forwarded(status):
    body = {"error": f"status {status}"}
    with patch("a402.middleware.settle", return_value=(status, body)):
        r = _app().get("/r", headers={"X-PAYMENT": _xp_header()})
    assert r.status_code == status
    assert r.json()["error"] == f"status {status}"


def test_settle_network_error_502():
    with patch("a402.middleware.settle", side_effect=Exception("connection reset")):
        r = _app().get("/r", headers={"X-PAYMENT": _xp_header()})
    assert r.status_code == 502


def test_settle_200_without_payment_wrapper():
    body = {"status": "settled", "note": "bare"}
    with patch("a402.middleware.settle", return_value=(200, body)):
        r = _app().get("/r", headers={"X-PAYMENT": _xp_header()})
    assert r.status_code == 200
    decoded = json.loads(base64.b64decode(r.headers["X-PAYMENT-RESPONSE"]))
    assert decoded == body


# ── failed-settlement + timeout hardening (found via real staging settle) ─────
def test_settle_200_with_failed_payment_does_not_unlock():
    body = {"payment": {"status": "failed", "rejectionReason": "insufficient facilitator gas"}}
    with patch("a402.middleware.settle", return_value=(200, body)):
        r = _app().get("/r", headers={"X-PAYMENT": _xp_header()})
    assert r.status_code == 402, "a failed payment must not serve the gated resource"
    assert r.json()["payment"]["status"] == "failed"


def test_default_timeout_is_generous():
    from a402 import A402Config
    # 10s tripped real spurious 502s on the multi-chain challenge; default must be ≥30s.
    assert A402Config(api_key="k").timeout >= 30.0


def test_settle_200_bare_failed_body_does_not_unlock():
    # Review nit: guard must also catch a 200 whose body is {"status": "failed"}
    # WITHOUT the payment wrapper (theoretical today — facilitator always wraps).
    with patch("a402.middleware.settle", return_value=(200, {"status": "failed", "reason": "reverted"})):
        r = _app().get("/r", headers={"X-PAYMENT": _xp_header()})
    assert r.status_code == 402
