import {
  Hex,
  PublicKey,
  Rlp,
  Secp256k1,
  Signature,
  Value,
  WebAuthnP256,
} from 'ox'
import { describe, expect, test } from 'vp/test'
import * as KeyAuthorization from './KeyAuthorization.js'
import * as Period from './Period.js'
import * as SignatureEnvelope from './SignatureEnvelope.js'

const address = '0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c'
const expiry = 1234567890
const token = '0x20c0000000000000000000000000000000000001'

const privateKey_secp256k1 =
  '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80'
const signature_secp256k1 = Secp256k1.sign({
  payload: '0xdeadbeef',
  privateKey: privateKey_secp256k1,
})

const publicKey_p256 = PublicKey.from({
  prefix: 4,
  x: '0xad8ac16e167d6992c3e120d7f17d2376bc1cbcf30c46ba6dd00ce07303e742f5',
  y: '0x11edf6ce1c32de66846f56afa7be1cbd729bc35750b6d0cdcf3ec9d75461aba0',
})

const signature_p256_raw = Signature.from({
  r: '0xccbb3485d4726235f13cb15ef394fb7158179fb7b1925eccec0147671090c52e',
  s: '0x77c3c53373cc1e3b05e7c23f609deb17cea8fe097300c45411237e9fe4166b35',
  yParity: 0,
})

const signature_p256 = SignatureEnvelope.from({
  signature: signature_p256_raw,
  publicKey: publicKey_p256,
  prehash: true,
})

const signature_webauthn = SignatureEnvelope.from({
  signature: signature_p256_raw,
  publicKey: publicKey_p256,
  metadata: {
    authenticatorData: WebAuthnP256.getAuthenticatorData({ rpId: 'localhost' }),
    clientDataJSON: WebAuthnP256.getClientDataJSON({
      challenge: '0xdeadbeef',
      origin: 'http://localhost',
    }),
  },
})

const signature_multisig = SignatureEnvelope.from({
  account: address,
  signatures: [SignatureEnvelope.from(signature_secp256k1)],
  type: 'multisig',
})

describe('from', () => {
  test('default', () => {
    const authorization = KeyAuthorization.from({
      address,
      chainId: 1n,
      expiry,
      type: 'secp256k1',
      limits: [
        {
          token,
          limit: Value.from('10', 6),
        },
      ],
    })

    expect(authorization).toMatchInlineSnapshot(`
      {
        "address": "0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c",
        "chainId": 1n,
        "expiry": 1234567890,
        "limits": [
          {
            "limit": 10000000n,
            "token": "0x20c0000000000000000000000000000000000001",
          },
        ],
        "type": "secp256k1",
      }
    `)
  })

  test('with signature (secp256k1)', () => {
    const authorization = KeyAuthorization.from(
      {
        address,
        chainId: 1n,
        expiry,
        type: 'secp256k1',
        limits: [
          {
            token,
            limit: Value.from('10', 6),
          },
        ],
      },
      {
        signature: signature_secp256k1,
      },
    )

    expect(authorization).toMatchInlineSnapshot(`
      {
        "address": "0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c",
        "chainId": 1n,
        "expiry": 1234567890,
        "limits": [
          {
            "limit": 10000000n,
            "token": "0x20c0000000000000000000000000000000000001",
          },
        ],
        "signature": {
          "signature": {
            "r": "0xfa78c5905fb0b9d6066ef531f962a62bc6ef0d5eb59ecb134056d206f75aaed7",
            "s": "0x780926ff2601a935c2c79707d9e1799948c9f19dcdde1e090e903b19a07923d0",
            "yParity": 1,
          },
          "type": "secp256k1",
        },
        "type": "secp256k1",
      }
    `)
  })

  test('with signature (secp256k1)', () => {
    const authorization = KeyAuthorization.from(
      {
        address,
        chainId: 1n,
        expiry,
        type: 'secp256k1',
        limits: [
          {
            token,
            limit: Value.from('10', 6),
          },
        ],
      },
      {
        signature: SignatureEnvelope.from(signature_secp256k1),
      },
    )

    expect(authorization).toMatchInlineSnapshot(`
      {
        "address": "0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c",
        "chainId": 1n,
        "expiry": 1234567890,
        "limits": [
          {
            "limit": 10000000n,
            "token": "0x20c0000000000000000000000000000000000001",
          },
        ],
        "signature": {
          "signature": {
            "r": "0xfa78c5905fb0b9d6066ef531f962a62bc6ef0d5eb59ecb134056d206f75aaed7",
            "s": "0x780926ff2601a935c2c79707d9e1799948c9f19dcdde1e090e903b19a07923d0",
            "yParity": 1,
          },
          "type": "secp256k1",
        },
        "type": "secp256k1",
      }
    `)
  })

  test('with signature (p256)', () => {
    const authorization = KeyAuthorization.from(
      {
        address,
        chainId: 1n,
        expiry,
        type: 'p256',
        limits: [
          {
            token,
            limit: Value.from('10', 6),
          },
        ],
      },
      {
        signature: signature_p256,
      },
    )

    expect(authorization).toMatchInlineSnapshot(`
      {
        "address": "0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c",
        "chainId": 1n,
        "expiry": 1234567890,
        "limits": [
          {
            "limit": 10000000n,
            "token": "0x20c0000000000000000000000000000000000001",
          },
        ],
        "signature": {
          "prehash": true,
          "publicKey": {
            "prefix": 4,
            "x": "0xad8ac16e167d6992c3e120d7f17d2376bc1cbcf30c46ba6dd00ce07303e742f5",
            "y": "0x11edf6ce1c32de66846f56afa7be1cbd729bc35750b6d0cdcf3ec9d75461aba0",
          },
          "signature": {
            "r": "0xccbb3485d4726235f13cb15ef394fb7158179fb7b1925eccec0147671090c52e",
            "s": "0x77c3c53373cc1e3b05e7c23f609deb17cea8fe097300c45411237e9fe4166b35",
            "yParity": 0,
          },
          "type": "p256",
        },
        "type": "p256",
      }
    `)
  })

  test('with signature (webAuthn)', () => {
    const authorization = KeyAuthorization.from(
      {
        address,
        chainId: 1n,
        expiry,
        type: 'webAuthn',
        limits: [
          {
            token,
            limit: Value.from('10', 6),
          },
        ],
      },
      {
        signature: signature_webauthn,
      },
    )

    expect(authorization).toMatchInlineSnapshot(`
      {
        "address": "0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c",
        "chainId": 1n,
        "expiry": 1234567890,
        "limits": [
          {
            "limit": 10000000n,
            "token": "0x20c0000000000000000000000000000000000001",
          },
        ],
        "signature": {
          "metadata": {
            "authenticatorData": "0x49960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97630500000000",
            "clientDataJSON": "{"type":"webauthn.get","challenge":"3q2-7w","origin":"http://localhost","crossOrigin":false}",
          },
          "publicKey": {
            "prefix": 4,
            "x": "0xad8ac16e167d6992c3e120d7f17d2376bc1cbcf30c46ba6dd00ce07303e742f5",
            "y": "0x11edf6ce1c32de66846f56afa7be1cbd729bc35750b6d0cdcf3ec9d75461aba0",
          },
          "signature": {
            "r": "0xccbb3485d4726235f13cb15ef394fb7158179fb7b1925eccec0147671090c52e",
            "s": "0x77c3c53373cc1e3b05e7c23f609deb17cea8fe097300c45411237e9fe4166b35",
            "yParity": 0,
          },
          "type": "webAuthn",
        },
        "type": "webAuthn",
      }
    `)
  })

  test('with inline signature (secp256k1)', () => {
    const authorization = KeyAuthorization.from({
      address,
      chainId: 1n,
      expiry,
      type: 'secp256k1',
      limits: [
        {
          token,
          limit: Value.from('10', 6),
        },
      ],
      signature: SignatureEnvelope.from(signature_secp256k1),
    })

    expect(authorization).toMatchInlineSnapshot(`
      {
        "address": "0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c",
        "chainId": 1n,
        "expiry": 1234567890,
        "limits": [
          {
            "limit": 10000000n,
            "token": "0x20c0000000000000000000000000000000000001",
          },
        ],
        "signature": {
          "signature": {
            "r": "0xfa78c5905fb0b9d6066ef531f962a62bc6ef0d5eb59ecb134056d206f75aaed7",
            "s": "0x780926ff2601a935c2c79707d9e1799948c9f19dcdde1e090e903b19a07923d0",
            "yParity": 1,
          },
          "type": "secp256k1",
        },
        "type": "secp256k1",
      }
    `)
  })

  test('from rpc', () => {
    const authorization = KeyAuthorization.from({
      expiry: Hex.fromNumber(expiry),
      keyId: address,
      chainId: '0x1',
      keyType: 'secp256k1',
      limits: [{ token, limit: '0x989680' }],
      signature: {
        type: 'secp256k1',
        r: '0x635dc2033e60185bb36709c29c75d64ea51dfbd91c32ef4be198e4ceb169fb4d',
        s: '0x50c2667ac4c771072746acfdcf1f1483336dcca8bd2df47cd83175dbe60f0540',
        yParity: '0x0',
      },
    })

    expect(authorization).toMatchInlineSnapshot(`
      {
        "address": "0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c",
        "chainId": 1n,
        "expiry": 1234567890,
        "limits": [
          {
            "limit": 10000000n,
            "token": "0x20c0000000000000000000000000000000000001",
          },
        ],
        "signature": {
          "signature": {
            "r": "0x635dc2033e60185bb36709c29c75d64ea51dfbd91c32ef4be198e4ceb169fb4d",
            "s": "0x50c2667ac4c771072746acfdcf1f1483336dcca8bd2df47cd83175dbe60f0540",
            "yParity": 0,
          },
          "type": "secp256k1",
        },
        "type": "secp256k1",
      }
    `)
  })

  test('multiple limits', () => {
    const authorization = KeyAuthorization.from({
      address,
      chainId: 1n,
      expiry,
      type: 'secp256k1',
      limits: [
        {
          token: '0x20c0000000000000000000000000000000000001',
          limit: Value.from('10', 6),
        },
        {
          token: '0x20c0000000000000000000000000000000000002',
          limit: Value.from('20', 6),
        },
      ],
    })

    expect(authorization).toMatchInlineSnapshot(`
      {
        "address": "0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c",
        "chainId": 1n,
        "expiry": 1234567890,
        "limits": [
          {
            "limit": 10000000n,
            "token": "0x20c0000000000000000000000000000000000001",
          },
          {
            "limit": 20000000n,
            "token": "0x20c0000000000000000000000000000000000002",
          },
        ],
        "type": "secp256k1",
      }
    `)
  })

  test('zero expiry (never expires)', () => {
    const authorization = KeyAuthorization.from({
      address,
      chainId: 1n,
      expiry: 0,
      type: 'secp256k1',
      limits: [
        {
          token,
          limit: Value.from('10', 6),
        },
      ],
    })

    expect(authorization).toMatchInlineSnapshot(`
      {
        "address": "0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c",
        "chainId": 1n,
        "expiry": 0,
        "limits": [
          {
            "limit": 10000000n,
            "token": "0x20c0000000000000000000000000000000000001",
          },
        ],
        "type": "secp256k1",
      }
    `)
  })

  test('rejects a multisig signature', () => {
    const authorization = KeyAuthorization.from({
      address,
      chainId: 1n,
      type: 'secp256k1',
    })

    expect(() =>
      KeyAuthorization.from(authorization, {
        signature: signature_multisig as never,
      }),
    ).toThrowErrorMatchingInlineSnapshot(
      `[KeyAuthorization.InvalidSignatureTypeError: Signature type \`multisig\` is invalid for key authorizations; expected \`secp256k1\`, \`p256\`, or \`webAuthn\`.]`,
    )

    expect(() =>
      KeyAuthorization.from(authorization, {
        signature: SignatureEnvelope.serialize(signature_multisig),
      }),
    ).toThrowErrorMatchingInlineSnapshot(
      `[KeyAuthorization.InvalidSignatureTypeError: Signature type \`multisig\` is invalid for key authorizations; expected \`secp256k1\`, \`p256\`, or \`webAuthn\`.]`,
    )

    expect(() =>
      KeyAuthorization.from({
        ...authorization,
        signature: signature_multisig,
      } as never),
    ).toThrowErrorMatchingInlineSnapshot(
      `[KeyAuthorization.InvalidSignatureTypeError: Signature type \`multisig\` is invalid for key authorizations; expected \`secp256k1\`, \`p256\`, or \`webAuthn\`.]`,
    )
  })
})

describe('fromRpc', () => {
  test('rejects a multisig signature', () => {
    expect(() =>
      KeyAuthorization.fromRpc({
        chainId: '0x1',
        expiry: null,
        keyId: address,
        keyType: 'secp256k1',
        signature: SignatureEnvelope.toRpc(signature_multisig),
      } as never),
    ).toThrowErrorMatchingInlineSnapshot(
      `[KeyAuthorization.InvalidSignatureTypeError: Signature type \`multisig\` is invalid for key authorizations; expected \`secp256k1\`, \`p256\`, or \`webAuthn\`.]`,
    )
  })

  test('secp256k1', () => {
    const authorization = KeyAuthorization.fromRpc({
      chainId: '0x1',
      expiry: Hex.fromNumber(expiry),
      keyId: address,
      keyType: 'secp256k1',
      limits: [{ token, limit: '0x989680' }],
      signature: {
        type: 'secp256k1',
        r: '0x635dc2033e60185bb36709c29c75d64ea51dfbd91c32ef4be198e4ceb169fb4d',
        s: '0x50c2667ac4c771072746acfdcf1f1483336dcca8bd2df47cd83175dbe60f0540',
        yParity: '0x0',
      },
    })

    expect(authorization).toMatchInlineSnapshot(`
      {
        "address": "0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c",
        "chainId": 1n,
        "expiry": 1234567890,
        "limits": [
          {
            "limit": 10000000n,
            "token": "0x20c0000000000000000000000000000000000001",
          },
        ],
        "signature": {
          "signature": {
            "r": "0x635dc2033e60185bb36709c29c75d64ea51dfbd91c32ef4be198e4ceb169fb4d",
            "s": "0x50c2667ac4c771072746acfdcf1f1483336dcca8bd2df47cd83175dbe60f0540",
            "yParity": 0,
          },
          "type": "secp256k1",
        },
        "type": "secp256k1",
      }
    `)
  })

  test('p256', () => {
    const authorization = KeyAuthorization.fromRpc({
      chainId: '0x1',
      expiry: Hex.fromNumber(expiry),
      keyId: address,
      keyType: 'p256',
      limits: [{ token, limit: '0x989680' }],
      signature: {
        type: 'p256',
        preHash: true,
        pubKeyX: publicKey_p256.x,
        pubKeyY: publicKey_p256.y,
        r: signature_p256_raw.r,
        s: signature_p256_raw.s,
      },
    })

    expect(authorization).toMatchInlineSnapshot(`
      {
        "address": "0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c",
        "chainId": 1n,
        "expiry": 1234567890,
        "limits": [
          {
            "limit": 10000000n,
            "token": "0x20c0000000000000000000000000000000000001",
          },
        ],
        "signature": {
          "prehash": true,
          "publicKey": {
            "prefix": 4,
            "x": "0xad8ac16e167d6992c3e120d7f17d2376bc1cbcf30c46ba6dd00ce07303e742f5",
            "y": "0x11edf6ce1c32de66846f56afa7be1cbd729bc35750b6d0cdcf3ec9d75461aba0",
          },
          "signature": {
            "r": "0xccbb3485d4726235f13cb15ef394fb7158179fb7b1925eccec0147671090c52e",
            "s": "0x77c3c53373cc1e3b05e7c23f609deb17cea8fe097300c45411237e9fe4166b35",
          },
          "type": "p256",
        },
        "type": "p256",
      }
    `)
  })

  test('webAuthn', () => {
    const authorization = KeyAuthorization.fromRpc({
      chainId: '0x1',
      expiry: Hex.fromNumber(expiry),
      keyId: address,
      keyType: 'webAuthn',
      limits: [{ token, limit: '0x989680' }],
      signature: SignatureEnvelope.toRpc(signature_webauthn),
    })

    expect(authorization).toMatchInlineSnapshot(`
      {
        "address": "0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c",
        "chainId": 1n,
        "expiry": 1234567890,
        "limits": [
          {
            "limit": 10000000n,
            "token": "0x20c0000000000000000000000000000000000001",
          },
        ],
        "signature": {
          "metadata": {
            "authenticatorData": "0x49960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97630500000000",
            "clientDataJSON": "{"type":"webauthn.get","challenge":"3q2-7w","origin":"http://localhost","crossOrigin":false}",
          },
          "publicKey": {
            "prefix": 4,
            "x": "0xad8ac16e167d6992c3e120d7f17d2376bc1cbcf30c46ba6dd00ce07303e742f5",
            "y": "0x11edf6ce1c32de66846f56afa7be1cbd729bc35750b6d0cdcf3ec9d75461aba0",
          },
          "signature": {
            "r": "0xccbb3485d4726235f13cb15ef394fb7158179fb7b1925eccec0147671090c52e",
            "s": "0x77c3c53373cc1e3b05e7c23f609deb17cea8fe097300c45411237e9fe4166b35",
          },
          "type": "webAuthn",
        },
        "type": "webAuthn",
      }
    `)
  })

  test('multiple limits', () => {
    const authorization = KeyAuthorization.fromRpc({
      chainId: '0x1',
      expiry: Hex.fromNumber(expiry),
      keyId: address,
      keyType: 'secp256k1',
      limits: [
        {
          token: '0x20c0000000000000000000000000000000000001',
          limit: '0x989680',
        },
        {
          token: '0x20c0000000000000000000000000000000000002',
          limit: '0x1312d00',
        },
      ],
      signature: {
        type: 'secp256k1',
        r: '0x635dc2033e60185bb36709c29c75d64ea51dfbd91c32ef4be198e4ceb169fb4d',
        s: '0x50c2667ac4c771072746acfdcf1f1483336dcca8bd2df47cd83175dbe60f0540',
        yParity: '0x0',
      },
    })

    expect(authorization).toMatchInlineSnapshot(`
      {
        "address": "0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c",
        "chainId": 1n,
        "expiry": 1234567890,
        "limits": [
          {
            "limit": 10000000n,
            "token": "0x20c0000000000000000000000000000000000001",
          },
          {
            "limit": 20000000n,
            "token": "0x20c0000000000000000000000000000000000002",
          },
        ],
        "signature": {
          "signature": {
            "r": "0x635dc2033e60185bb36709c29c75d64ea51dfbd91c32ef4be198e4ceb169fb4d",
            "s": "0x50c2667ac4c771072746acfdcf1f1483336dcca8bd2df47cd83175dbe60f0540",
            "yParity": 0,
          },
          "type": "secp256k1",
        },
        "type": "secp256k1",
      }
    `)
  })

  test('handles chainId "0x" as 0n', () => {
    const authorization = KeyAuthorization.fromRpc({
      chainId: '0x',
      expiry: Hex.fromNumber(expiry),
      keyId: address,
      keyType: 'secp256k1',
      limits: [{ token, limit: '0x989680' }],
      signature: {
        type: 'secp256k1',
        r: '0x635dc2033e60185bb36709c29c75d64ea51dfbd91c32ef4be198e4ceb169fb4d',
        s: '0x50c2667ac4c771072746acfdcf1f1483336dcca8bd2df47cd83175dbe60f0540',
        yParity: '0x0',
      },
    })

    expect(authorization.chainId).toBe(0n)
  })

  test('omits expiry when null (never expires)', () => {
    const authorization = KeyAuthorization.fromRpc({
      chainId: '0x1',
      expiry: null,
      keyId: address,
      keyType: 'secp256k1',
      limits: [{ token, limit: '0x989680' }],
      signature: {
        type: 'secp256k1',
        r: '0x635dc2033e60185bb36709c29c75d64ea51dfbd91c32ef4be198e4ceb169fb4d',
        s: '0x50c2667ac4c771072746acfdcf1f1483336dcca8bd2df47cd83175dbe60f0540',
        yParity: '0x0',
      },
    })

    expect(authorization.expiry).toBeUndefined()
  })
})

describe('fromTuple', () => {
  test('default', () => {
    const authorization = KeyAuthorization.fromTuple([
      [
        '0x00', // chainId
        '0x00', // keyType (secp256k1)
        address,
        Hex.fromNumber(expiry),
        [[token, '0x989680']],
      ],
    ])

    expect(authorization).toMatchInlineSnapshot(`
      {
        "address": "0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c",
        "chainId": 0n,
        "expiry": 1234567890,
        "limits": [
          {
            "limit": 10000000n,
            "token": "0x20c0000000000000000000000000000000000001",
          },
        ],
        "type": "secp256k1",
      }
    `)
  })

  test('with signature (secp256k1)', () => {
    const signature = SignatureEnvelope.serialize(
      SignatureEnvelope.from(signature_secp256k1),
    )

    const authorization = KeyAuthorization.fromTuple([
      [
        '0x00', // chainId
        '0x00', // keyType (secp256k1)
        address,
        Hex.fromNumber(expiry),
        [[token, '0x989680']],
      ],
      signature,
    ])

    expect(authorization).toMatchInlineSnapshot(`
      {
        "address": "0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c",
        "chainId": 0n,
        "expiry": 1234567890,
        "limits": [
          {
            "limit": 10000000n,
            "token": "0x20c0000000000000000000000000000000000001",
          },
        ],
        "signature": {
          "signature": {
            "r": "0xfa78c5905fb0b9d6066ef531f962a62bc6ef0d5eb59ecb134056d206f75aaed7",
            "s": "0x780926ff2601a935c2c79707d9e1799948c9f19dcdde1e090e903b19a07923d0",
            "yParity": 1,
          },
          "type": "secp256k1",
        },
        "type": "secp256k1",
      }
    `)
  })

  test('with signature (p256)', () => {
    const signature = SignatureEnvelope.serialize(signature_p256)

    const authorization = KeyAuthorization.fromTuple([
      [
        '0x00', // chainId
        '0x01', // keyType (p256)
        address,
        Hex.fromNumber(expiry),
        [[token, '0x989680']],
      ],
      signature,
    ])

    expect(authorization).toMatchInlineSnapshot(`
      {
        "address": "0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c",
        "chainId": 0n,
        "expiry": 1234567890,
        "limits": [
          {
            "limit": 10000000n,
            "token": "0x20c0000000000000000000000000000000000001",
          },
        ],
        "signature": {
          "prehash": true,
          "publicKey": {
            "prefix": 4,
            "x": "0xad8ac16e167d6992c3e120d7f17d2376bc1cbcf30c46ba6dd00ce07303e742f5",
            "y": "0x11edf6ce1c32de66846f56afa7be1cbd729bc35750b6d0cdcf3ec9d75461aba0",
          },
          "signature": {
            "r": "0xccbb3485d4726235f13cb15ef394fb7158179fb7b1925eccec0147671090c52e",
            "s": "0x77c3c53373cc1e3b05e7c23f609deb17cea8fe097300c45411237e9fe4166b35",
          },
          "type": "p256",
        },
        "type": "p256",
      }
    `)
  })

  test('with signature (webAuthn)', () => {
    const signature = SignatureEnvelope.serialize(signature_webauthn)

    const authorization = KeyAuthorization.fromTuple([
      [
        '0x00', // chainId
        '0x02', // keyType (webAuthn)
        address,
        Hex.fromNumber(expiry),
        [[token, '0x989680']],
      ],
      signature,
    ])

    expect(authorization).toMatchInlineSnapshot(`
      {
        "address": "0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c",
        "chainId": 0n,
        "expiry": 1234567890,
        "limits": [
          {
            "limit": 10000000n,
            "token": "0x20c0000000000000000000000000000000000001",
          },
        ],
        "signature": {
          "metadata": {
            "authenticatorData": "0x49960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97630500000000",
            "clientDataJSON": "{"type":"webauthn.get","challenge":"3q2-7w","origin":"http://localhost","crossOrigin":false}",
          },
          "publicKey": {
            "prefix": 4,
            "x": "0xad8ac16e167d6992c3e120d7f17d2376bc1cbcf30c46ba6dd00ce07303e742f5",
            "y": "0x11edf6ce1c32de66846f56afa7be1cbd729bc35750b6d0cdcf3ec9d75461aba0",
          },
          "signature": {
            "r": "0xccbb3485d4726235f13cb15ef394fb7158179fb7b1925eccec0147671090c52e",
            "s": "0x77c3c53373cc1e3b05e7c23f609deb17cea8fe097300c45411237e9fe4166b35",
          },
          "type": "webAuthn",
        },
        "type": "webAuthn",
      }
    `)
  })

  test('multiple limits', () => {
    const authorization = KeyAuthorization.fromTuple([
      [
        '0x00', // chainId
        '0x00', // keyType (secp256k1)
        address,
        Hex.fromNumber(expiry),
        [
          ['0x20c0000000000000000000000000000000000001', '0x989680'],
          ['0x20c0000000000000000000000000000000000002', '0x1312d00'],
        ],
      ],
    ])

    expect(authorization).toMatchInlineSnapshot(`
      {
        "address": "0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c",
        "chainId": 0n,
        "expiry": 1234567890,
        "limits": [
          {
            "limit": 10000000n,
            "token": "0x20c0000000000000000000000000000000000001",
          },
          {
            "limit": 20000000n,
            "token": "0x20c0000000000000000000000000000000000002",
          },
        ],
        "type": "secp256k1",
      }
    `)
  })

  test('with non-zero chainId', () => {
    const authorization = KeyAuthorization.fromTuple([
      [
        Hex.fromNumber(123), // chainId
        '0x00', // keyType (secp256k1)
        address,
        Hex.fromNumber(expiry),
        [[token, '0x989680']],
      ],
    ])

    expect(authorization).toMatchInlineSnapshot(`
      {
        "address": "0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c",
        "chainId": 123n,
        "expiry": 1234567890,
        "limits": [
          {
            "limit": 10000000n,
            "token": "0x20c0000000000000000000000000000000000001",
          },
        ],
        "type": "secp256k1",
      }
    `)
  })

  test('empty keyType treated as secp256k1', () => {
    const authorization = KeyAuthorization.fromTuple([
      [
        '0x00', // chainId
        '0x', // keyType (empty = secp256k1)
        address,
        Hex.fromNumber(expiry),
        [[token, '0x989680']],
      ],
    ])

    expect(authorization).toMatchInlineSnapshot(`
      {
        "address": "0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c",
        "chainId": 0n,
        "expiry": 1234567890,
        "limits": [
          {
            "limit": 10000000n,
            "token": "0x20c0000000000000000000000000000000000001",
          },
        ],
        "type": "secp256k1",
      }
    `)
  })
})

describe('getSignPayload', () => {
  test('default', () => {
    const authorization = KeyAuthorization.from({
      address,
      chainId: 1n,
      expiry,
      type: 'secp256k1',
      limits: [
        {
          token,
          limit: Value.from('10', 6),
        },
      ],
    })

    const payload = KeyAuthorization.getSignPayload(authorization)

    expect(payload).toMatchInlineSnapshot(
      `"0x5a3a9a67cc6b68eafd00fe3ffb1ea8755ef29cfa1d1c2f655efa61966ef248f7"`,
    )
  })

  test('with signature (signature should be ignored)', () => {
    const authorization = KeyAuthorization.from({
      address,
      chainId: 1n,
      expiry,
      type: 'secp256k1',
      limits: [
        {
          token,
          limit: Value.from('10', 6),
        },
      ],
      signature: SignatureEnvelope.from(signature_secp256k1),
    })

    const payload = KeyAuthorization.getSignPayload(authorization)

    // Should be same as without signature
    expect(payload).toMatchInlineSnapshot(
      `"0x5a3a9a67cc6b68eafd00fe3ffb1ea8755ef29cfa1d1c2f655efa61966ef248f7"`,
    )
  })

  test('different key types', () => {
    const auth_secp256k1 = KeyAuthorization.from({
      address,
      chainId: 1n,
      expiry,
      type: 'secp256k1',
      limits: [{ token, limit: Value.from('10', 6) }],
    })

    const auth_p256 = KeyAuthorization.from({
      address,
      chainId: 1n,
      expiry,
      type: 'p256',
      limits: [{ token, limit: Value.from('10', 6) }],
    })

    const auth_webauthn = KeyAuthorization.from({
      address,
      chainId: 1n,
      expiry,
      type: 'webAuthn',
      limits: [{ token, limit: Value.from('10', 6) }],
    })

    const payload_secp256k1 = KeyAuthorization.getSignPayload(auth_secp256k1)
    const payload_p256 = KeyAuthorization.getSignPayload(auth_p256)
    const payload_webauthn = KeyAuthorization.getSignPayload(auth_webauthn)

    // Payloads should be different for different key types
    expect(payload_secp256k1).not.toBe(payload_p256)
    expect(payload_secp256k1).not.toBe(payload_webauthn)
    expect(payload_p256).not.toBe(payload_webauthn)

    expect(payload_secp256k1).toMatchInlineSnapshot(
      `"0x5a3a9a67cc6b68eafd00fe3ffb1ea8755ef29cfa1d1c2f655efa61966ef248f7"`,
    )
    expect(payload_p256).toMatchInlineSnapshot(
      `"0x6807f3a5597cdc334568094bb2a884fd97cfa21ecb7a8034e7fabef32680e5fe"`,
    )
    expect(payload_webauthn).toMatchInlineSnapshot(
      `"0x576f0b2608ac4630b7f40f2b8571828e2e1e6f13ad9aca8c7ce53773407e9ce5"`,
    )
  })
})

describe('deserialize', () => {
  test('default', () => {
    const authorization = KeyAuthorization.from({
      address,
      chainId: 1n,
      expiry,
      type: 'secp256k1',
      limits: [
        {
          token,
          limit: Value.from('10', 6),
        },
      ],
    })

    const serialized = KeyAuthorization.serialize(authorization)
    const deserialized = KeyAuthorization.deserialize(serialized)

    expect(deserialized).toEqual(authorization)
  })

  test('signed (secp256k1)', () => {
    const authorization = KeyAuthorization.from(
      {
        address,
        chainId: 1n,
        expiry,
        type: 'secp256k1',
        limits: [
          {
            token,
            limit: Value.from('10', 6),
          },
        ],
      },
      { signature: signature_secp256k1 },
    )

    const serialized = KeyAuthorization.serialize(authorization)
    const deserialized = KeyAuthorization.deserialize(serialized)

    expect(deserialized).toEqual(authorization)
  })

  test('no limits', () => {
    const authorization = KeyAuthorization.from({
      address,
      chainId: 1n,
      expiry,
      type: 'secp256k1',
    })

    const serialized = KeyAuthorization.serialize(authorization)
    const deserialized = KeyAuthorization.deserialize(serialized)

    expect(deserialized).toEqual(authorization)
  })

  test('no expiry', () => {
    const authorization = KeyAuthorization.from({
      address,
      chainId: 1n,
      type: 'secp256k1',
    })

    const serialized = KeyAuthorization.serialize(authorization)
    const deserialized = KeyAuthorization.deserialize(serialized)

    expect(deserialized).toEqual(authorization)
  })
})

describe('hash', () => {
  test('default', () => {
    const authorization = KeyAuthorization.from({
      address,
      chainId: 1n,
      expiry,
      type: 'secp256k1',
      limits: [
        {
          token,
          limit: Value.from('10', 6),
        },
      ],
    })

    const hash = KeyAuthorization.hash(authorization)

    expect(hash).toMatchInlineSnapshot(
      `"0x5a3a9a67cc6b68eafd00fe3ffb1ea8755ef29cfa1d1c2f655efa61966ef248f7"`,
    )
  })
})

describe('serialize', () => {
  test('default', () => {
    const authorization = KeyAuthorization.from({
      address,
      chainId: 1n,
      expiry,
      type: 'secp256k1',
      limits: [
        {
          token,
          limit: Value.from('10', 6),
        },
      ],
    })

    const serialized = KeyAuthorization.serialize(authorization)

    expect(serialized).toMatchInlineSnapshot(
      `"0xf838f7018094be95c3f554e9fc85ec51be69a3d807a0d55bcf2c84499602d2dad99420c000000000000000000000000000000000000183989680"`,
    )
  })

  test('signed (secp256k1)', () => {
    const authorization = KeyAuthorization.from(
      {
        address,
        chainId: 1n,
        expiry,
        type: 'secp256k1',
        limits: [
          {
            token,
            limit: Value.from('10', 6),
          },
        ],
      },
      { signature: signature_secp256k1 },
    )

    const serialized = KeyAuthorization.serialize(authorization)
    const deserialized = KeyAuthorization.deserialize(serialized)

    expect(deserialized).toEqual(authorization)
  })

  test('no limits', () => {
    const authorization = KeyAuthorization.from({
      address,
      chainId: 1n,
      expiry,
      type: 'secp256k1',
    })

    const serialized = KeyAuthorization.serialize(authorization)

    expect(serialized).toMatchInlineSnapshot(
      `"0xdddc018094be95c3f554e9fc85ec51be69a3d807a0d55bcf2c84499602d2"`,
    )
  })

  test('no expiry', () => {
    const authorization = KeyAuthorization.from({
      address,
      chainId: 1n,
      type: 'secp256k1',
    })

    const serialized = KeyAuthorization.serialize(authorization)

    expect(serialized).toMatchInlineSnapshot(
      `"0xd8d7018094be95c3f554e9fc85ec51be69a3d807a0d55bcf2c"`,
    )
  })
})

describe('toRpc', () => {
  test('secp256k1', () => {
    const authorization = KeyAuthorization.from({
      address,
      chainId: 1n,
      expiry,
      type: 'secp256k1',
      limits: [
        {
          token,
          limit: Value.from('10', 6),
        },
      ],
      signature: SignatureEnvelope.from({
        r: '0x635dc2033e60185bb36709c29c75d64ea51dfbd91c32ef4be198e4ceb169fb4d',
        s: '0x50c2667ac4c771072746acfdcf1f1483336dcca8bd2df47cd83175dbe60f0540',
        yParity: 0,
      }),
    })

    const rpc = KeyAuthorization.toRpc(authorization)

    expect(rpc).toMatchInlineSnapshot(`
      {
        "chainId": "0x1",
        "expiry": "0x499602d2",
        "keyId": "0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c",
        "keyType": "secp256k1",
        "limits": [
          {
            "limit": "0x989680",
            "token": "0x20c0000000000000000000000000000000000001",
          },
        ],
        "signature": {
          "r": "0x635dc2033e60185bb36709c29c75d64ea51dfbd91c32ef4be198e4ceb169fb4d",
          "s": "0x50c2667ac4c771072746acfdcf1f1483336dcca8bd2df47cd83175dbe60f0540",
          "type": "secp256k1",
          "yParity": "0x0",
        },
      }
    `)
  })

  test('p256', () => {
    const authorization = KeyAuthorization.from({
      address,
      chainId: 1n,
      expiry,
      type: 'p256',
      limits: [
        {
          token,
          limit: Value.from('10', 6),
        },
      ],
      signature: signature_p256,
    })

    const rpc = KeyAuthorization.toRpc(authorization)

    expect(rpc).toMatchInlineSnapshot(`
      {
        "chainId": "0x1",
        "expiry": "0x499602d2",
        "keyId": "0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c",
        "keyType": "p256",
        "limits": [
          {
            "limit": "0x989680",
            "token": "0x20c0000000000000000000000000000000000001",
          },
        ],
        "signature": {
          "preHash": true,
          "pubKeyX": "0xad8ac16e167d6992c3e120d7f17d2376bc1cbcf30c46ba6dd00ce07303e742f5",
          "pubKeyY": "0x11edf6ce1c32de66846f56afa7be1cbd729bc35750b6d0cdcf3ec9d75461aba0",
          "r": "0xccbb3485d4726235f13cb15ef394fb7158179fb7b1925eccec0147671090c52e",
          "s": "0x77c3c53373cc1e3b05e7c23f609deb17cea8fe097300c45411237e9fe4166b35",
          "type": "p256",
        },
      }
    `)
  })

  test('webAuthn', () => {
    const authorization = KeyAuthorization.from({
      address,
      chainId: 1n,
      expiry,
      type: 'webAuthn',
      limits: [
        {
          token,
          limit: Value.from('10', 6),
        },
      ],
      signature: signature_webauthn,
    })

    const rpc = KeyAuthorization.toRpc(authorization)

    expect(rpc).toMatchInlineSnapshot(`
      {
        "chainId": "0x1",
        "expiry": "0x499602d2",
        "keyId": "0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c",
        "keyType": "webAuthn",
        "limits": [
          {
            "limit": "0x989680",
            "token": "0x20c0000000000000000000000000000000000001",
          },
        ],
        "signature": {
          "pubKeyX": "0xad8ac16e167d6992c3e120d7f17d2376bc1cbcf30c46ba6dd00ce07303e742f5",
          "pubKeyY": "0x11edf6ce1c32de66846f56afa7be1cbd729bc35750b6d0cdcf3ec9d75461aba0",
          "r": "0xccbb3485d4726235f13cb15ef394fb7158179fb7b1925eccec0147671090c52e",
          "s": "0x77c3c53373cc1e3b05e7c23f609deb17cea8fe097300c45411237e9fe4166b35",
          "type": "webAuthn",
          "webauthnData": "0x49960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d976305000000007b2274797065223a22776562617574686e2e676574222c226368616c6c656e6765223a223371322d3777222c226f726967696e223a22687474703a2f2f6c6f63616c686f7374222c2263726f73734f726967696e223a66616c73657d",
        },
      }
    `)
  })

  test('multiple limits', () => {
    const authorization = KeyAuthorization.from({
      address,
      chainId: 1n,
      expiry,
      type: 'secp256k1',
      limits: [
        {
          token: '0x20c0000000000000000000000000000000000001',
          limit: Value.from('10', 6),
        },
        {
          token: '0x20c0000000000000000000000000000000000002',
          limit: Value.from('20', 6),
        },
      ],
      signature: SignatureEnvelope.from(signature_secp256k1),
    })

    const rpc = KeyAuthorization.toRpc(authorization)

    expect(rpc).toMatchInlineSnapshot(`
      {
        "chainId": "0x1",
        "expiry": "0x499602d2",
        "keyId": "0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c",
        "keyType": "secp256k1",
        "limits": [
          {
            "limit": "0x989680",
            "token": "0x20c0000000000000000000000000000000000001",
          },
          {
            "limit": "0x1312d00",
            "token": "0x20c0000000000000000000000000000000000002",
          },
        ],
        "signature": {
          "r": "0xfa78c5905fb0b9d6066ef531f962a62bc6ef0d5eb59ecb134056d206f75aaed7",
          "s": "0x780926ff2601a935c2c79707d9e1799948c9f19dcdde1e090e903b19a07923d0",
          "type": "secp256k1",
          "yParity": "0x1",
        },
      }
    `)
  })

  test('with non-zero chainId', () => {
    const authorization = KeyAuthorization.from({
      address,
      chainId: 123n,
      expiry,
      type: 'secp256k1',
      limits: [
        {
          token,
          limit: Value.from('10', 6),
        },
      ],
      signature: SignatureEnvelope.from(signature_secp256k1),
    })

    const rpc = KeyAuthorization.toRpc(authorization)

    expect(rpc).toMatchInlineSnapshot(`
      {
        "chainId": "0x7b",
        "expiry": "0x499602d2",
        "keyId": "0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c",
        "keyType": "secp256k1",
        "limits": [
          {
            "limit": "0x989680",
            "token": "0x20c0000000000000000000000000000000000001",
          },
        ],
        "signature": {
          "r": "0xfa78c5905fb0b9d6066ef531f962a62bc6ef0d5eb59ecb134056d206f75aaed7",
          "s": "0x780926ff2601a935c2c79707d9e1799948c9f19dcdde1e090e903b19a07923d0",
          "type": "secp256k1",
          "yParity": "0x1",
        },
      }
    `)
  })

  test('round-trip: toRpc -> fromRpc', () => {
    const authorization = KeyAuthorization.from({
      address,
      chainId: 0n,
      expiry,
      type: 'secp256k1',
      limits: [
        {
          token,
          limit: Value.from('10', 6),
        },
      ],
      signature: SignatureEnvelope.from(signature_secp256k1),
    })

    const rpc = KeyAuthorization.toRpc(authorization)
    const restored = KeyAuthorization.fromRpc(rpc)

    expect(restored).toEqual(authorization)
  })

  test('round-trip: toRpc -> fromRpc (no expiry)', () => {
    const authorization = KeyAuthorization.from({
      address,
      chainId: 0n,
      type: 'secp256k1',
      limits: [
        {
          token,
          limit: Value.from('10', 6),
        },
      ],
      signature: SignatureEnvelope.from(signature_secp256k1),
    })

    const rpc = KeyAuthorization.toRpc(authorization)
    expect(rpc.expiry).toBeNull()

    const restored = KeyAuthorization.fromRpc(rpc)
    expect(restored.expiry).toBeUndefined()
    expect(restored.address).toBe(authorization.address)
    expect(restored.chainId).toBe(authorization.chainId)
  })
})

describe('toTuple', () => {
  test('default', () => {
    const authorization = KeyAuthorization.from({
      address,
      chainId: 1n,
      type: 'secp256k1',
    })

    const tuple = KeyAuthorization.toTuple(authorization)

    expect(tuple).toMatchInlineSnapshot(`
      [
        [
          "0x1",
          "0x",
          "0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c",
        ],
      ]
    `)
  })

  test('with signature (secp256k1)', () => {
    const authorization = KeyAuthorization.from({
      address,
      chainId: 1n,
      expiry,
      type: 'secp256k1',
      limits: [
        {
          token,
          limit: Value.from('10', 6),
        },
      ],
      signature: SignatureEnvelope.from(signature_secp256k1),
    })

    const tuple = KeyAuthorization.toTuple(authorization)

    expect(tuple).toMatchInlineSnapshot(`
      [
        [
          "0x1",
          "0x",
          "0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c",
          "0x499602d2",
          [
            [
              "0x20c0000000000000000000000000000000000001",
              "0x989680",
            ],
          ],
        ],
        "0xfa78c5905fb0b9d6066ef531f962a62bc6ef0d5eb59ecb134056d206f75aaed7780926ff2601a935c2c79707d9e1799948c9f19dcdde1e090e903b19a07923d01c",
      ]
    `)
  })

  test('with signature (p256)', () => {
    const authorization = KeyAuthorization.from({
      address,
      chainId: 1n,
      expiry,
      type: 'p256',
      limits: [
        {
          token,
          limit: Value.from('10', 6),
        },
      ],
      signature: signature_p256,
    })

    const tuple = KeyAuthorization.toTuple(authorization)

    expect(tuple).toMatchInlineSnapshot(`
      [
        [
          "0x1",
          "0x01",
          "0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c",
          "0x499602d2",
          [
            [
              "0x20c0000000000000000000000000000000000001",
              "0x989680",
            ],
          ],
        ],
        "0x01ccbb3485d4726235f13cb15ef394fb7158179fb7b1925eccec0147671090c52e77c3c53373cc1e3b05e7c23f609deb17cea8fe097300c45411237e9fe4166b35ad8ac16e167d6992c3e120d7f17d2376bc1cbcf30c46ba6dd00ce07303e742f511edf6ce1c32de66846f56afa7be1cbd729bc35750b6d0cdcf3ec9d75461aba001",
      ]
    `)
  })

  test('with signature (webAuthn)', () => {
    const authorization = KeyAuthorization.from({
      address,
      chainId: 1n,
      expiry,
      type: 'webAuthn',
      limits: [
        {
          token,
          limit: Value.from('10', 6),
        },
      ],
      signature: signature_webauthn,
    })

    const tuple = KeyAuthorization.toTuple(authorization)

    expect(tuple).toMatchInlineSnapshot(`
      [
        [
          "0x1",
          "0x02",
          "0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c",
          "0x499602d2",
          [
            [
              "0x20c0000000000000000000000000000000000001",
              "0x989680",
            ],
          ],
        ],
        "0x0249960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d976305000000007b2274797065223a22776562617574686e2e676574222c226368616c6c656e6765223a223371322d3777222c226f726967696e223a22687474703a2f2f6c6f63616c686f7374222c2263726f73734f726967696e223a66616c73657dccbb3485d4726235f13cb15ef394fb7158179fb7b1925eccec0147671090c52e77c3c53373cc1e3b05e7c23f609deb17cea8fe097300c45411237e9fe4166b35ad8ac16e167d6992c3e120d7f17d2376bc1cbcf30c46ba6dd00ce07303e742f511edf6ce1c32de66846f56afa7be1cbd729bc35750b6d0cdcf3ec9d75461aba0",
      ]
    `)
  })

  test('multiple limits', () => {
    const authorization = KeyAuthorization.from({
      address,
      chainId: 1n,
      expiry,
      type: 'secp256k1',
      limits: [
        {
          token: '0x20c0000000000000000000000000000000000001',
          limit: Value.from('10', 6),
        },
        {
          token: '0x20c0000000000000000000000000000000000002',
          limit: Value.from('20', 6),
        },
      ],
    })

    const tuple = KeyAuthorization.toTuple(authorization)

    expect(tuple).toMatchInlineSnapshot(`
      [
        [
          "0x1",
          "0x",
          "0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c",
          "0x499602d2",
          [
            [
              "0x20c0000000000000000000000000000000000001",
              "0x989680",
            ],
            [
              "0x20c0000000000000000000000000000000000002",
              "0x1312d00",
            ],
          ],
        ],
      ]
    `)
  })

  test('with non-zero chainId', () => {
    const authorization = KeyAuthorization.from({
      address,
      chainId: 123n,
      expiry,
      type: 'secp256k1',
      limits: [
        {
          token,
          limit: Value.from('10', 6),
        },
      ],
    })

    const tuple = KeyAuthorization.toTuple(authorization)

    expect(tuple).toMatchInlineSnapshot(`
      [
        [
          "0x7b",
          "0x",
          "0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c",
          "0x499602d2",
          [
            [
              "0x20c0000000000000000000000000000000000001",
              "0x989680",
            ],
          ],
        ],
      ]
    `)
  })

  test('zero spending limit roundtrips through RLP', () => {
    const authorization = KeyAuthorization.from({
      address,
      chainId: 0n,
      expiry,
      type: 'secp256k1',
      limits: [
        {
          token,
          limit: 0n,
        },
      ],
    })

    const tuple = KeyAuthorization.toTuple(authorization)

    expect(tuple).toMatchInlineSnapshot(`
      [
        [
          "0x",
          "0x",
          "0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c",
          "0x499602d2",
          [
            [
              "0x20c0000000000000000000000000000000000001",
              "0x",
            ],
          ],
        ],
      ]
    `)

    const [authorizationTuple] = tuple
    const rlpEncoded = Rlp.fromHex(authorizationTuple)
    const rlpDecoded = Rlp.toHex(rlpEncoded)
    expect(rlpDecoded).toEqual(authorizationTuple)

    const restored = KeyAuthorization.fromTuple(tuple)
    expect(restored.limits?.[0]?.limit).toBe(0n)
  })

  test('undefined expiry roundtrips through RLP', () => {
    const authorization = KeyAuthorization.from({
      address,
      chainId: 0n,
      type: 'secp256k1',
      limits: [
        {
          token,
          limit: Value.from('10', 6),
        },
      ],
    })

    const tuple = KeyAuthorization.toTuple(authorization)

    expect(tuple).toMatchInlineSnapshot(`
      [
        [
          "0x",
          "0x",
          "0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c",
          "0x",
          [
            [
              "0x20c0000000000000000000000000000000000001",
              "0x989680",
            ],
          ],
        ],
      ]
    `)

    const [authorizationTuple] = tuple
    const rlpEncoded = Rlp.fromHex(authorizationTuple)
    const rlpDecoded = Rlp.toHex(rlpEncoded)
    expect(rlpDecoded).toEqual(authorizationTuple)

    const restored = KeyAuthorization.fromTuple(tuple)
    expect(restored.expiry).toBeUndefined()
  })

  test('hash works with zero spending limit', () => {
    const authorization = KeyAuthorization.from({
      address,
      chainId: 1n,
      expiry,
      type: 'secp256k1',
      limits: [
        {
          token,
          limit: 0n,
        },
      ],
    })

    expect(() => KeyAuthorization.hash(authorization)).not.toThrow()
  })

  test('periodic spending limit (period > 0)', () => {
    const authorization = KeyAuthorization.from({
      address,
      chainId: 1n,
      expiry,
      type: 'secp256k1',
      limits: [
        {
          token,
          limit: Value.from('10', 6),
          period: Period.months(1),
        },
      ],
    })

    const tuple = KeyAuthorization.toTuple(authorization)

    expect(tuple).toMatchInlineSnapshot(`
      [
        [
          "0x1",
          "0x",
          "0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c",
          "0x499602d2",
          [
            [
              "0x20c0000000000000000000000000000000000001",
              "0x989680",
              "0x278d00",
            ],
          ],
        ],
      ]
    `)

    // Roundtrip
    const serialized = KeyAuthorization.serialize(authorization)
    const restored = KeyAuthorization.deserialize(serialized)
    expect(restored.limits?.[0]?.period).toBe(2592000)
    expect(restored.limits?.[0]?.limit).toBe(Value.from('10', 6))
  })

  test('one-time limit omits period from tuple', () => {
    const authorization = KeyAuthorization.from({
      address,
      chainId: 1n,
      expiry,
      type: 'secp256k1',
      limits: [
        {
          token,
          limit: Value.from('10', 6),
        },
      ],
    })

    const tuple = KeyAuthorization.toTuple(authorization)
    // Canonical 2-field form — no period element
    const [authTuple] = tuple
    const limitTuple = (authTuple as any)[4][0]
    expect(limitTuple).toHaveLength(2)
  })

  test('mixed one-time and periodic limits', () => {
    const authorization = KeyAuthorization.from({
      address,
      chainId: 1n,
      expiry,
      type: 'secp256k1',
      limits: [
        {
          token: '0x20c0000000000000000000000000000000000001',
          limit: Value.from('10', 6),
        },
        {
          token: '0x20c0000000000000000000000000000000000002',
          limit: Value.from('100', 6),
          period: Period.days(1),
        },
      ],
    })

    const serialized = KeyAuthorization.serialize(authorization)
    const restored = KeyAuthorization.deserialize(serialized)
    expect(restored.limits?.[0]?.period).toBeUndefined()
    expect(restored.limits?.[1]?.period).toBe(86400)
  })

  test('call scopes: address-only (any selector)', () => {
    const authorization = KeyAuthorization.from({
      address,
      chainId: 1n,
      type: 'secp256k1',
      scopes: [
        {
          address: '0x1234567890123456789012345678901234567890',
        },
      ],
    })

    const tuple = KeyAuthorization.toTuple(authorization)
    expect(tuple).toMatchInlineSnapshot(`
      [
        [
          "0x1",
          "0x",
          "0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c",
          "0x",
          [],
          [
            [
              "0x1234567890123456789012345678901234567890",
              [],
            ],
          ],
        ],
      ]
    `)

    const serialized = KeyAuthorization.serialize(authorization)
    const restored = KeyAuthorization.deserialize(serialized)
    expect(restored.scopes).toHaveLength(1)
    expect(restored.scopes?.[0]?.address).toBe(
      '0x1234567890123456789012345678901234567890',
    )
    expect(restored.scopes?.[0]?.selector).toBeUndefined()
  })

  test('call scopes: explicit selectors', () => {
    const authorization = KeyAuthorization.from({
      address,
      chainId: 1n,
      type: 'secp256k1',
      scopes: [
        {
          address: '0x1234567890123456789012345678901234567890',
          selector: '0xa9059cbb', // transfer
        },
        {
          address: '0x1234567890123456789012345678901234567890',
          selector: '0x095ea7b3', // approve
        },
      ],
    })

    const serialized = KeyAuthorization.serialize(authorization)
    const restored = KeyAuthorization.deserialize(serialized)
    expect(restored.scopes).toHaveLength(2)
    expect(restored.scopes?.[0]?.selector).toBe('0xa9059cbb')
    expect(restored.scopes?.[1]?.selector).toBe('0x095ea7b3')
    expect(restored.scopes?.[0]?.recipients).toBeUndefined()
  })

  test('call scopes: selectors with recipients', () => {
    const authorization = KeyAuthorization.from({
      address,
      chainId: 1n,
      type: 'secp256k1',
      scopes: [
        {
          address: token,
          selector: '0xa9059cbb',
          recipients: [
            '0x1111111111111111111111111111111111111111',
            '0x2222222222222222222222222222222222222222',
          ],
        },
      ],
    })

    const serialized = KeyAuthorization.serialize(authorization)
    const restored = KeyAuthorization.deserialize(serialized)
    expect(restored.scopes?.[0]?.recipients).toEqual([
      '0x1111111111111111111111111111111111111111',
      '0x2222222222222222222222222222222222222222',
    ])
  })

  test('scopes = undefined (unrestricted, omitted from wire)', () => {
    const authorization = KeyAuthorization.from({
      address,
      chainId: 1n,
      type: 'secp256k1',
    })

    const tuple = KeyAuthorization.toTuple(authorization)
    // No scopes field in tuple
    const [authTuple] = tuple
    expect((authTuple as unknown as any[]).length).toBe(3) // chainId, type, address

    const serialized = KeyAuthorization.serialize(authorization)
    const restored = KeyAuthorization.deserialize(serialized)
    expect(restored.scopes).toBeUndefined()
  })

  test('scopes = [] (scoped, no calls allowed)', () => {
    const authorization = KeyAuthorization.from({
      address,
      chainId: 1n,
      type: 'secp256k1',
      scopes: [],
    })

    const serialized = KeyAuthorization.serialize(authorization)
    const restored = KeyAuthorization.deserialize(serialized)
    expect(restored.scopes).toEqual([])
  })

  test('scopes + limits + expiry combined', () => {
    const authorization = KeyAuthorization.from({
      address,
      chainId: 1n,
      expiry,
      type: 'secp256k1',
      limits: [
        {
          token,
          limit: Value.from('10', 6),
          period: Period.days(1),
        },
      ],
      scopes: [
        {
          address: '0x1234567890123456789012345678901234567890',
          selector: '0xa9059cbb',
          recipients: ['0x1111111111111111111111111111111111111111'],
        },
      ],
    })

    const serialized = KeyAuthorization.serialize(authorization)
    const restored = KeyAuthorization.deserialize(serialized)
    expect(restored.expiry).toBe(expiry)
    expect(restored.limits?.[0]?.period).toBe(86400)
    expect(restored.scopes?.[0]?.recipients).toEqual([
      '0x1111111111111111111111111111111111111111',
    ])
  })

  test('hash consistency with scopes', () => {
    const authorization = KeyAuthorization.from({
      address,
      chainId: 1n,
      type: 'secp256k1',
      scopes: [
        {
          address: '0x1234567890123456789012345678901234567890',
          selector: '0xa9059cbb',
        },
      ],
    })

    const hash1 = KeyAuthorization.hash(authorization)
    const hash2 = KeyAuthorization.hash(authorization)
    expect(hash1).toBe(hash2)

    // Different scopes should produce different hash
    const authorization2 = KeyAuthorization.from({
      address,
      chainId: 1n,
      type: 'secp256k1',
      scopes: [
        {
          address: '0x1234567890123456789012345678901234567890',
          selector: '0x095ea7b3',
        },
      ],
    })
    expect(KeyAuthorization.hash(authorization2)).not.toBe(hash1)
  })

  test('call scopes: selector from function signature string', () => {
    const authorization = KeyAuthorization.from({
      address,
      chainId: 1n,
      type: 'secp256k1',
      scopes: [
        {
          address: '0x1234567890123456789012345678901234567890',
          selector: 'function transfer(address,uint256)',
        },
        {
          address: '0x1234567890123456789012345678901234567890',
          selector: 'function approve(address,uint256)',
        },
      ],
    })

    const serialized = KeyAuthorization.serialize(authorization)
    const restored = KeyAuthorization.deserialize(serialized)
    expect(restored.scopes).toHaveLength(2)
    // transfer(address,uint256) => 0xa9059cbb
    expect(restored.scopes?.[0]?.selector).toBe('0xa9059cbb')
    // approve(address,uint256) => 0x095ea7b3
    expect(restored.scopes?.[1]?.selector).toBe('0x095ea7b3')
  })

  test('call scopes: selector from signature with recipients', () => {
    const authorization = KeyAuthorization.from({
      address,
      chainId: 1n,
      type: 'secp256k1',
      scopes: [
        {
          address: token,
          selector: 'function transfer(address,uint256)',
          recipients: ['0x1111111111111111111111111111111111111111'],
        },
      ],
    })

    const serialized = KeyAuthorization.serialize(authorization)
    const restored = KeyAuthorization.deserialize(serialized)
    expect(restored.scopes?.[0]?.selector).toBe('0xa9059cbb')
    expect(restored.scopes?.[0]?.recipients).toEqual([
      '0x1111111111111111111111111111111111111111',
    ])
  })

  test('call scopes: selector from bare signature (no function prefix)', () => {
    const authorization = KeyAuthorization.from({
      address,
      chainId: 1n,
      type: 'secp256k1',
      scopes: [
        {
          address: '0x1234567890123456789012345678901234567890',
          selector: 'transfer(address,uint256)',
        },
        {
          address: '0x1234567890123456789012345678901234567890',
          selector: 'approve(address,uint256)',
        },
      ],
    })

    const serialized = KeyAuthorization.serialize(authorization)
    const restored = KeyAuthorization.deserialize(serialized)
    expect(restored.scopes).toHaveLength(2)
    expect(restored.scopes?.[0]?.selector).toBe('0xa9059cbb')
    expect(restored.scopes?.[1]?.selector).toBe('0x095ea7b3')
  })

  test('call scopes: mixed hex and signature selectors', () => {
    const authorization = KeyAuthorization.from({
      address,
      chainId: 1n,
      type: 'secp256k1',
      scopes: [
        {
          address: '0x1234567890123456789012345678901234567890',
          selector: '0xa9059cbb',
        },
        {
          address: '0x1234567890123456789012345678901234567890',
          selector: 'function approve(address,uint256)',
        },
      ],
    })

    const serialized = KeyAuthorization.serialize(authorization)
    const restored = KeyAuthorization.deserialize(serialized)
    expect(restored.scopes?.[0]?.selector).toBe('0xa9059cbb')
    expect(restored.scopes?.[1]?.selector).toBe('0x095ea7b3')
  })

  test('toRpc/fromRpc roundtrip with period and scopes', () => {
    const authorization = KeyAuthorization.from(
      {
        address,
        chainId: 1n,
        expiry,
        type: 'secp256k1',
        limits: [
          {
            token,
            limit: Value.from('10', 6),
            period: Period.months(1),
          },
        ],
        scopes: [
          {
            address: token,
            selector: '0xa9059cbb',
            recipients: ['0x1111111111111111111111111111111111111111'],
          },
        ],
      },
      {
        signature: SignatureEnvelope.from(signature_secp256k1),
      },
    )

    const rpc = KeyAuthorization.toRpc(authorization)
    const restored = KeyAuthorization.fromRpc(rpc)

    expect(restored.limits?.[0]?.period).toBe(2592000)
    expect(restored.scopes?.[0]?.address).toBe(token)
    expect(restored.scopes?.[0]?.selector).toBe('0xa9059cbb')
    expect(restored.scopes?.[0]?.recipients).toEqual([
      '0x1111111111111111111111111111111111111111',
    ])
  })
})

describe('witness (TIP-1053)', () => {
  const witness =
    '0x1111111111111111111111111111111111111111111111111111111111111111' as const
  const witness_other =
    '0x2222222222222222222222222222222222222222222222222222222222222222' as const

  test('from: preserves witness', () => {
    const authorization = KeyAuthorization.from({
      address,
      chainId: 1n,
      type: 'secp256k1',
      witness,
    })
    expect(authorization.witness).toBe(witness)
  })

  test('from: throws on non-32-byte witness', () => {
    expect(() =>
      KeyAuthorization.from({
        address,
        chainId: 1n,
        type: 'secp256k1',
        witness: '0xdeadbeef',
      }),
    ).toThrowErrorMatchingInlineSnapshot(
      `[KeyAuthorization.InvalidWitnessSizeError: Witness \`0xdeadbeef\` must be exactly 32 bytes (got 4 bytes).]`,
    )
  })

  test('toTuple: appends witness as trailing field with defaulted earlier fields', () => {
    const authorization = KeyAuthorization.from({
      address,
      chainId: 1n,
      type: 'secp256k1',
      witness,
    })
    const [authTuple] = KeyAuthorization.toTuple(authorization)
    expect(authTuple).toEqual([
      '0x1', // chainId
      '0x', // keyType (secp256k1)
      address,
      '0x', // expiry default (never expires)
      '0x', // limits absent (RLP null placeholder)
      '0x', // scopes absent (RLP null placeholder)
      witness,
    ])
  })

  test('toTuple: omits witness when absent (byte-equivalent to pre-TIP-1053)', () => {
    const withoutWitness = KeyAuthorization.from({
      address,
      chainId: 1n,
      type: 'secp256k1',
    })
    const [authTuple] = KeyAuthorization.toTuple(withoutWitness)
    expect((authTuple as unknown as unknown[]).length).toBe(3)
  })

  test('serialize/deserialize: roundtrip with witness only', () => {
    const authorization = KeyAuthorization.from({
      address,
      chainId: 1n,
      type: 'secp256k1',
      witness,
    })
    const serialized = KeyAuthorization.serialize(authorization)
    const restored = KeyAuthorization.deserialize(serialized)
    expect(restored.witness).toBe(witness)
    expect(restored.expiry).toBeUndefined()
    expect(restored.limits).toBeUndefined()
    expect(restored.scopes).toBeUndefined()
  })

  test('serialize/deserialize: roundtrip with witness + expiry + limits + scopes', () => {
    const authorization = KeyAuthorization.from({
      address,
      chainId: 1n,
      expiry,
      type: 'secp256k1',
      limits: [{ token, limit: Value.from('10', 6) }],
      scopes: [
        {
          address: token,
          selector: '0xa9059cbb',
          recipients: ['0x1111111111111111111111111111111111111111'],
        },
      ],
      witness,
    })
    const serialized = KeyAuthorization.serialize(authorization)
    const restored = KeyAuthorization.deserialize(serialized)
    expect(restored.witness).toBe(witness)
    expect(restored.expiry).toBe(expiry)
    expect(restored.limits?.[0]?.limit).toBe(10000000n)
    expect(restored.scopes?.[0]?.selector).toBe('0xa9059cbb')
  })

  test('toRpc/fromRpc: roundtrip with witness', () => {
    const authorization = KeyAuthorization.from(
      {
        address,
        chainId: 1n,
        type: 'secp256k1',
        witness,
      },
      { signature: SignatureEnvelope.from(signature_secp256k1) },
    )
    const rpc = KeyAuthorization.toRpc(authorization)
    expect(rpc.witness).toBe(witness)
    const restored = KeyAuthorization.fromRpc(rpc)
    expect(restored.witness).toBe(witness)
  })

  test('hash: changes when witness changes', () => {
    const a = KeyAuthorization.from({
      address,
      chainId: 1n,
      type: 'secp256k1',
      witness,
    })
    const b = KeyAuthorization.from({
      address,
      chainId: 1n,
      type: 'secp256k1',
      witness: witness_other,
    })
    expect(KeyAuthorization.hash(a)).not.toBe(KeyAuthorization.hash(b))
  })

  test('hash: witness-less encoding matches pre-TIP-1053 hash', () => {
    const before = KeyAuthorization.from({
      address,
      chainId: 1n,
      type: 'secp256k1',
    })
    // Re-create the same auth (no witness field). Hash must be identical.
    const again = KeyAuthorization.from({
      address,
      chainId: 1n,
      type: 'secp256k1',
    })
    expect(KeyAuthorization.hash(before)).toBe(KeyAuthorization.hash(again))
  })

  test('fromTuple: extracts trailing witness', () => {
    const restored = KeyAuthorization.fromTuple([
      ['0x01', '0x', address, '0x', [], [], witness],
    ])
    expect(restored.witness).toBe(witness)
  })

  test('fromTuple: throws on non-32-byte witness', () => {
    expect(() =>
      KeyAuthorization.fromTuple([
        ['0x01', '0x', address, '0x', [], [], '0xdeadbeef'],
      ]),
    ).toThrowErrorMatchingInlineSnapshot(
      `[KeyAuthorization.InvalidWitnessSizeError: Witness \`0xdeadbeef\` must be exactly 32 bytes (got 4 bytes).]`,
    )
  })

  test('signing flow: signature verifies against witness-bearing hash', () => {
    const authorization = KeyAuthorization.from({
      address,
      chainId: 1n,
      type: 'secp256k1',
      witness,
    })
    const payload = KeyAuthorization.getSignPayload(authorization)
    const sig = Secp256k1.sign({
      payload,
      privateKey: privateKey_secp256k1,
    })
    const signed = KeyAuthorization.from(authorization, {
      signature: SignatureEnvelope.from(sig),
    })
    const serialized = KeyAuthorization.serialize(signed)
    const restored = KeyAuthorization.deserialize(serialized)
    expect(restored.witness).toBe(witness)
    // The hash of the restored payload matches what was signed.
    expect(KeyAuthorization.hash(restored)).toBe(payload)
  })
})

describe('admin keys (TIP-1049)', () => {
  const account = '0x1111111111111111111111111111111111111111' as const

  test('from: preserves isAdmin and account', () => {
    const authorization = KeyAuthorization.from({
      address,
      account,
      chainId: 1n,
      isAdmin: true,
      type: 'secp256k1',
    })
    expect(authorization.isAdmin).toBe(true)
    expect(authorization.account).toBe(account)
  })

  test('toTuple: emits isAdmin + account together', () => {
    const authorization = KeyAuthorization.from({
      address,
      account,
      chainId: 1n,
      isAdmin: true,
      type: 'secp256k1',
    })
    const [authTuple] = KeyAuthorization.toTuple(authorization)
    expect(authTuple).toEqual([
      '0x1',
      '0x',
      address,
      '0x', // expiry placeholder
      '0x', // limits placeholder
      '0x', // scopes placeholder
      '0x', // witness placeholder
      '0x01', // isAdmin = true
      account,
    ])
  })

  test('toTuple: omits both when neither is set (byte-equivalent to pre-TIP-1049)', () => {
    const authorization = KeyAuthorization.from({
      address,
      chainId: 1n,
      type: 'secp256k1',
    })
    const [authTuple] = KeyAuthorization.toTuple(authorization)
    expect((authTuple as unknown as unknown[]).length).toBe(3)
  })

  test('fromTuple: drops orphan isAdmin without account', () => {
    const restored = KeyAuthorization.fromTuple([
      ['0x01', '0x', address, '0x', [], [], '0x', '0x01'],
    ])
    expect(restored.isAdmin).toBeUndefined()
    expect(restored.account).toBeUndefined()
  })

  test('fromTuple: drops orphan account without isAdmin', () => {
    const restored = KeyAuthorization.fromTuple([
      ['0x01', '0x', address, '0x', [], [], '0x', '0x', account],
    ])
    expect(restored.isAdmin).toBeUndefined()
    expect(restored.account).toBeUndefined()
  })

  test('fromTuple: extracts isAdmin + account together', () => {
    const restored = KeyAuthorization.fromTuple([
      ['0x01', '0x', address, '0x', [], [], '0x', '0x01', account],
    ])
    expect(restored.isAdmin).toBe(true)
    expect(restored.account).toBe(account)
  })

  test('fromTuple: throws on invalid admin marker', () => {
    expect(() =>
      KeyAuthorization.fromTuple([
        ['0x01', '0x', address, '0x', [], [], '0x', '0x02'],
      ]),
    ).toThrowErrorMatchingInlineSnapshot(
      `[KeyAuthorization.InvalidAdminMarkerError: Admin marker \`0x02\` is invalid; expected \`0x01\` (TIP-1049).]`,
    )
  })

  test('serialize/deserialize: roundtrip with isAdmin + account', () => {
    const authorization = KeyAuthorization.from({
      address,
      account,
      chainId: 1n,
      isAdmin: true,
      type: 'secp256k1',
    })
    const serialized = KeyAuthorization.serialize(authorization)
    const restored = KeyAuthorization.deserialize(serialized)
    expect(restored.isAdmin).toBe(true)
    expect(restored.account).toBe(account)
  })

  test('toRpc/fromRpc: roundtrip with isAdmin + account', () => {
    const authorization = KeyAuthorization.from(
      {
        address,
        account,
        chainId: 1n,
        isAdmin: true,
        type: 'secp256k1',
      },
      { signature: SignatureEnvelope.from(signature_secp256k1) },
    )
    const rpc = KeyAuthorization.toRpc(authorization)
    expect(rpc.isAdmin).toBe(true)
    expect(rpc.account).toBe(account)
    const restored = KeyAuthorization.fromRpc(rpc)
    expect(restored.isAdmin).toBe(true)
    expect(restored.account).toBe(account)
  })

  test('fromRpc: drops orphan isAdmin without account', () => {
    const authorization = KeyAuthorization.from(
      { address, chainId: 1n, type: 'secp256k1' },
      { signature: SignatureEnvelope.from(signature_secp256k1) },
    )
    const rpc = KeyAuthorization.toRpc(authorization)
    const restored = KeyAuthorization.fromRpc({ ...rpc, isAdmin: true })
    expect(restored.isAdmin).toBeUndefined()
    expect(restored.account).toBeUndefined()
  })

  test('fromRpc: drops orphan account without isAdmin', () => {
    const authorization = KeyAuthorization.from(
      { address, chainId: 1n, type: 'secp256k1' },
      { signature: SignatureEnvelope.from(signature_secp256k1) },
    )
    const rpc = KeyAuthorization.toRpc(authorization)
    const restored = KeyAuthorization.fromRpc({ ...rpc, account })
    expect(restored.isAdmin).toBeUndefined()
    expect(restored.account).toBeUndefined()
  })

  test('hash: changes when admin pair is added', () => {
    const plain = KeyAuthorization.from({
      address,
      chainId: 1n,
      type: 'secp256k1',
    })
    const admin = KeyAuthorization.from({
      address,
      account,
      chainId: 1n,
      isAdmin: true,
      type: 'secp256k1',
    })
    expect(KeyAuthorization.hash(plain)).not.toBe(KeyAuthorization.hash(admin))
  })

  test('hash: changes when account in admin pair changes', () => {
    const a = KeyAuthorization.from({
      address,
      account,
      chainId: 1n,
      isAdmin: true,
      type: 'secp256k1',
    })
    const b = KeyAuthorization.from({
      address,
      account: '0x2222222222222222222222222222222222222222',
      chainId: 1n,
      isAdmin: true,
      type: 'secp256k1',
    })
    expect(KeyAuthorization.hash(a)).not.toBe(KeyAuthorization.hash(b))
  })
})
