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_RESPONSE = {
    "x402Version": 1,
    "accepts": [
        {
            "scheme": "exact",
            "resource": "GET /v1/financials",
            "description": "Test resource",
            "network": "Base Sepolia",
            "asset": "0xUSDC",
            "payTo": "0xMERCHANT",
            "priceUsd": 0.05,
            "value": "50000",
            "nonce": "0xdeadbeef",
            "validBefore": "1750000000",
            "apiKeyId": "ak_test",
        }
    ],
}

SETTLE_SUCCESS = {
    "payment": {
        "status": "settled",
        "txHash": "0xabc",
        "mintTxHash": "0xabc",
        "settlementMs": 1200,
    }
}

VALID_PAYMENT = base64.b64encode(
    json.dumps(
        {
            "apiKeyId": "ak_test",
            "network": "Base Sepolia",
            "authorization": {
                "from": "0xPAYER",
                "to": "0xMERCHANT",
                "value": "50000",
                "validAfter": "0",
                "validBefore": "1750000000",
                "nonce": "0xdeadbeef",
            },
            "signature": "0xSIG",
        }
    ).encode()
).decode()


def _make_app() -> tuple[Starlette, TestClient]:
    async def gated(request: Request) -> JSONResponse:
        return JSONResponse({"data": "secret"})

    async def public(request: Request) -> JSONResponse:
        return JSONResponse({"ok": True})

    cfg = A402Config(
        facilitator_url="https://facilitator.test",
        api_key="a402_test_key",
    )
    app = Starlette(
        routes=[
            Route("/v1/financials", gated, methods=["GET"]),
            Route("/public", public, methods=["GET"]),
        ]
    )
    app.add_middleware(
        A402Middleware,
        config=cfg,
        routes=[{"method": "GET", "path": "/v1/financials", "price": "$0.05", "description": "Test resource"}],
    )
    return app, TestClient(app, raise_server_exceptions=False)


def test_no_payment_returns_402_with_accepts():
    _, client = _make_app()

    with patch("a402.middleware.issue_challenge", return_value=CHALLENGE_RESPONSE):
        resp = client.get("/v1/financials")

    assert resp.status_code == 402
    body = resp.json()
    assert body["x402Version"] == 1
    assert isinstance(body["accepts"], list)
    assert len(body["accepts"]) == 1


def test_valid_payment_calls_settle_and_sets_header():
    _, client = _make_app()

    with patch("a402.middleware.settle", return_value=(200, SETTLE_SUCCESS)):
        resp = client.get("/v1/financials", headers={"X-PAYMENT": VALID_PAYMENT})

    assert resp.status_code == 200
    assert resp.json() == {"data": "secret"}
    assert "X-PAYMENT-RESPONSE" in resp.headers
    decoded = json.loads(base64.b64decode(resp.headers["X-PAYMENT-RESPONSE"]))
    assert decoded == SETTLE_SUCCESS


def test_403_from_settle_is_forwarded():
    _, client = _make_app()
    reject_body = {"error": "payment rejected at gate", "x402Version": 1}

    with patch("a402.middleware.settle", return_value=(403, reject_body)):
        resp = client.get("/v1/financials", headers={"X-PAYMENT": VALID_PAYMENT})

    assert resp.status_code == 403
    assert resp.json()["error"] == "payment rejected at gate"


def test_non_gated_route_passes_through():
    _, client = _make_app()
    resp = client.get("/public")
    assert resp.status_code == 200
    assert resp.json() == {"ok": True}


def test_price_parsing_dollar_string():
    from a402 import parse_price

    assert parse_price("$0.05") == pytest.approx(0.05)
    assert parse_price("$1.00") == pytest.approx(1.0)
    assert parse_price(0.10) == pytest.approx(0.10)
    assert parse_price(1) == pytest.approx(1.0)


def test_malformed_x_payment_returns_400():
    _, client = _make_app()

    with patch("a402.middleware.settle", return_value=(200, SETTLE_SUCCESS)):
        resp = client.get("/v1/financials", headers={"X-PAYMENT": "!!!not-base64-or-json!!!"})

    assert resp.status_code == 400
    assert "malformed" in resp.json()["error"]


def test_facilitator_network_error_returns_502():
    _, client = _make_app()

    with patch("a402.middleware.issue_challenge", side_effect=Exception("connection refused")):
        resp = client.get("/v1/financials")

    assert resp.status_code == 502


def test_settle_injects_api_key_token():
    """settle() must add apiKeyToken when the payload lacks it."""
    from unittest.mock import MagicMock

    import httpx

    from a402.client import settle
    from a402.config import A402Config

    cfg = A402Config(facilitator_url="https://facilitator.test", api_key="my_key")
    payload = {"apiKeyId": "ak_test", "network": "Base Sepolia", "signature": "0xSIG"}

    captured = {}

    def fake_post(url, **kwargs):
        captured["json"] = kwargs.get("json", {})
        mock_resp = MagicMock(spec=httpx.Response)
        mock_resp.status_code = 200
        mock_resp.json.return_value = SETTLE_SUCCESS
        return mock_resp

    with patch("a402.client.httpx.Client") as MockClient:
        instance = MockClient.return_value.__enter__.return_value
        instance.post.side_effect = fake_post
        settle(cfg, payload)

    assert captured["json"]["apiKeyToken"] == "my_key"


def test_settle_forces_seller_api_key_token():
    """A client-supplied apiKeyToken must be overwritten with the seller's key."""
    from unittest.mock import MagicMock

    import httpx

    from a402.client import settle
    from a402.config import A402Config

    cfg = A402Config(facilitator_url="https://facilitator.test", api_key="my_key")
    payload = {"apiKeyId": "ak_test", "apiKeyToken": "caller_key", "signature": "0xSIG"}

    captured = {}

    def fake_post(url, **kwargs):
        captured["json"] = kwargs.get("json", {})
        mock_resp = MagicMock(spec=httpx.Response)
        mock_resp.status_code = 200
        mock_resp.json.return_value = SETTLE_SUCCESS
        return mock_resp

    with patch("a402.client.httpx.Client") as MockClient:
        instance = MockClient.return_value.__enter__.return_value
        instance.post.side_effect = fake_post
        settle(cfg, payload)

    assert captured["json"]["apiKeyToken"] == "my_key"
