"""Python starter tests, mirroring the TypeScript A1 to A4. Offline: a fake facilitator and a mock registry."""
import base64
import json
import logging
from pathlib import Path

import httpx
import pytest
from eth_account import Account
from eth_account.messages import encode_typed_data
from fastapi.testclient import TestClient

from app import create_app
from evm_pay import exact_payment_header
from list_offer import ListOfferError, list_offer
from x402 import USDC, decode_header, encode_header, to_atomic_usdc

ROOT = Path(__file__).resolve().parents[1]
PAY_TO = "0x3c1A1E01e790Cd326495631B3b38Bd7068F08d6F"
PAYMENT = {"x402Version": 1, "scheme": "exact", "network": "base-sepolia",
           "payload": {"signature": "0xsig", "authorization": {"from": "0x" + "11" * 20}}}
FIVE_LAWS = ["Solidarity Oath", "Renewal Mandate", "Treasury Equilibrium", "Harm Shield", "Transparency Covenant"]


class FakeFacilitator:
    def __init__(self, valid=True, settles=True, verify_raises=False, settle_raises=False):
        self.calls, self.seen = [], []
        self.valid, self.settles, self.verify_raises, self.settle_raises = valid, settles, verify_raises, settle_raises

    async def verify(self, p, r):
        self.calls.append("verify"); self.seen.append((p, r))
        if self.verify_raises:
            raise RuntimeError("down")
        return {"isValid": True, "payer": "0x" + "11" * 20} if self.valid else {"isValid": False, "invalidReason": "invalid_signature"}

    async def settle(self, p, r):
        self.calls.append("settle")
        if self.settle_raises:
            raise RuntimeError("down")
        return {"success": True, "transaction": "0xabc123", "network": r["network"]} if self.settles \
            else {"success": False, "transaction": "", "network": r["network"], "errorReason": "insufficient_funds"}


def client(fac, network="base-sepolia", price="0.01", work=None):
    return TestClient(create_app(price=price, pay_to=PAY_TO, network=network, facilitator=fac, description="test call", work=work),
                      base_url="https://agent.example")


@pytest.mark.parametrize("network", ["base", "base-sepolia"])
def test_a1_unpaid_call_answers_402_with_requirements(network):
    r = client(FakeFacilitator(), network=network, price="0.25").get("/paid?x=1")
    assert r.status_code == 402
    b = r.json()
    assert b["x402Version"] == 1 and isinstance(b["error"], str) and len(b["accepts"]) == 1
    req = b["accepts"][0]
    assert req["scheme"] == "exact" and req["network"] == network
    assert req["maxAmountRequired"] == "250000"
    assert req["resource"] == "https://agent.example/paid"
    assert req["payTo"] == PAY_TO and req["mimeType"] == "application/json"
    assert req["asset"] == USDC[network]["address"]
    assert req["maxTimeoutSeconds"] > 0
    assert client(FakeFacilitator()).get("/health").json() == {"ok": True}


def test_a1_prices_and_bad_configuration():
    assert to_atomic_usdc("0.01") == "10000" and to_atomic_usdc("1") == "1000000" and to_atomic_usdc("0.000001") == "1"
    for bad in ["0", "-1", "0.0000001", "abc", "1e3"]:
        with pytest.raises(ValueError):
            to_atomic_usdc(bad)
    with pytest.raises(ValueError):
        create_app(price="0.01", pay_to="nope", network="base", facilitator=FakeFacilitator())


def test_a2_paid_call_verified_settled_then_200():
    fac = FakeFacilitator()

    async def work(_req):
        fac.calls.append("work")
        return {"ok": True, "answer": 42}

    r = client(fac, work=work).get("/paid", headers={"X-PAYMENT": encode_header(PAYMENT)})
    assert r.status_code == 200 and r.json() == {"ok": True, "answer": 42}
    assert fac.calls == ["verify", "settle", "work"]
    assert fac.seen[0][0] == PAYMENT and fac.seen[0][1]["maxAmountRequired"] == "10000"
    receipt = decode_header(r.headers["X-PAYMENT-RESPONSE"])
    assert receipt["success"] is True and receipt["transaction"] == "0xabc123" and receipt["network"] == "base-sepolia"


@pytest.mark.parametrize("kw,expected", [
    ({"valid": False}, ["verify"]), ({"verify_raises": True}, ["verify"]),
    ({"settles": False}, ["verify", "settle"]), ({"settle_raises": True}, ["verify", "settle"]),
])
def test_a2_failed_verify_or_settle_gets_402_and_no_work(kw, expected):
    fac = FakeFacilitator(**kw)
    worked = []

    async def work(_req):
        worked.append(1)
        return {}

    r = client(fac, work=work).get("/paid", headers={"X-PAYMENT": encode_header(PAYMENT)})
    assert r.status_code == 402 and "X-PAYMENT-RESPONSE" not in r.headers
    assert fac.calls == expected and worked == []


@pytest.mark.parametrize("header", ["%%%", encode_header({**PAYMENT, "network": "base"}), encode_header({**PAYMENT, "scheme": "upto"}), encode_header({"x402Version": 1})])
def test_a2_malformed_header_never_reaches_facilitator(header):
    fac = FakeFacilitator()
    assert client(fac).get("/paid", headers={"X-PAYMENT": header}).status_code == 402
    assert fac.calls == []


def test_a3_offer_passes_lax_rules_with_five_laws():
    o = json.loads((ROOT / "lax-offer.json").read_text())
    assert 3 <= len(o["name"]) <= 80 and 20 <= len(o["description"]) <= 600
    assert o["endpoint"].startswith("https://")
    assert o["agent_did"].startswith("did:")
    for law in FIVE_LAWS:
        assert law in o["constitutional_constraints"]
    cap = o["capabilities"][0]
    assert cap["pricing"] == {"model": "per_call", "amount": "0.01", "currency": "USDC", "rail": "x402"}
    assert set(o["payment_rails"]) <= {"x402", "usdc", "usdc-base"}
    assert "\u2014" not in json.dumps(o, ensure_ascii=False) and "\u2013" not in json.dumps(o, ensure_ascii=False)


def mock_registry(version):
    posts = []
    terms = {"x402Version": version, "error": "pay", "accepts": [{"scheme": "exact", "network": "eip155:8453" if version == 2 else "base",
             "amount": "10000", "maxAmountRequired": "10000", "payTo": "0xf62fb064f9d9d03b8013e5b6023e3501805ba973",
             "asset": USDC["base"]["address"], "maxTimeoutSeconds": 60, "extra": {"name": "USD Coin", "version": "2"}}]}

    def handler(request: httpx.Request):
        posts.append(request)
        if not (request.headers.get("PAYMENT-SIGNATURE") or request.headers.get("X-PAYMENT")):
            headers = {"PAYMENT-REQUIRED": encode_header(terms)} if version == 2 else {}
            return httpx.Response(402, json={"x402Version": 2, "error": "pay"} if version == 2 else terms, headers=headers)
        return httpx.Response(201, json={"ok": True, "id": "lax-ext-test123"})

    return httpx.Client(transport=httpx.MockTransport(handler)), posts, terms


@pytest.mark.parametrize("version,header", [(2, "PAYMENT-SIGNATURE"), (1, "X-PAYMENT")])
def test_a4_list_offer_pays_the_402_and_returns_the_id(version, header):
    c, posts, terms = mock_registry(version)
    offer = json.loads((ROOT / "lax-offer.json").read_text())
    seen = []
    out = list_offer(offer, lambda t: seen.append(t) or "cGF5bWVudA==", registry_url="https://reg.example/registry/", client=c)
    assert out == {"id": "lax-ext-test123"}
    assert len(posts) == 2 and json.loads(posts[1].content) == offer
    assert posts[1].headers[header] == "cGF5bWVudA=="
    assert seen[0]["accepts"] == terms["accepts"]


def test_a4_key_never_logged_and_payment_signature_is_valid(caplog, capsys):
    acct = Account.create()
    key_hex = acct.key.hex().removeprefix("0x")
    c, posts, _ = mock_registry(2)
    offer = json.loads((ROOT / "lax-offer.json").read_text())
    with caplog.at_level(logging.DEBUG):
        list_offer(offer, lambda t: exact_payment_header(t, acct, max_atomic=10000), client=c)
    out = capsys.readouterr()
    assert key_hex not in out.out + out.err + caplog.text
    hdr = posts[1].headers["PAYMENT-SIGNATURE"]
    assert key_hex not in hdr
    p = json.loads(base64.b64decode(hdr))
    a = p["payload"]["authorization"]
    typed = {"types": {"EIP712Domain": [{"name": "name", "type": "string"}, {"name": "version", "type": "string"}, {"name": "chainId", "type": "uint256"}, {"name": "verifyingContract", "type": "address"}],
                       "TransferWithAuthorization": [{"name": n, "type": t} for n, t in [("from", "address"), ("to", "address"), ("value", "uint256"), ("validAfter", "uint256"), ("validBefore", "uint256"), ("nonce", "bytes32")]]},
             "primaryType": "TransferWithAuthorization",
             "domain": {"name": "USD Coin", "version": "2", "chainId": 8453, "verifyingContract": USDC["base"]["address"]},
             "message": {"from": a["from"], "to": a["to"], "value": int(a["value"]), "validAfter": int(a["validAfter"]), "validBefore": int(a["validBefore"]), "nonce": bytes.fromhex(a["nonce"][2:])}}
    assert Account.recover_message(encode_typed_data(full_message=typed), signature=p["payload"]["signature"]) == acct.address
    c2, _, terms = mock_registry(2)
    with pytest.raises(ValueError, match="above the cap"):
        exact_payment_header(terms, acct, max_atomic=9999)


def test_a4_registry_error_is_raised():
    c = httpx.Client(transport=httpx.MockTransport(lambda r: httpx.Response(400, json={"error": "name must be at least 3 characters"})))
    with pytest.raises(ListOfferError, match="at least 3"):
        list_offer({"name": "x"}, lambda t: "x", client=c)


def test_a5_readme_holds_base_sepolia_proof():
    import re
    readme = (Path(__file__).resolve().parent.parent / "README.md").read_text()
    assert re.search(r"Base Sepolia proof[^\n]*\n[\s\S]*?https://sepolia\.basescan\.org/tx/0x[0-9a-fA-F]{64}", readme)
