import { Address as core_Address, Hex, Secp256k1, Signature } from 'ox'
import { decodeFunctionData } from 'viem'
import type { Address } from 'viem/accounts'
import { Abis } from 'viem/tempo'
import { describe, expect, test } from 'vp/test'

import * as Storage from '../Storage.js'
import * as Store from '../Store.js'
import { privy } from './privy.js'

// Deterministic test keys so addresses and signatures are reproducible across
// runs. Real signing is required by upcoming signer-recovery validation, and
// keeps the mocks honest about what the production adapter sees from Privy.
const privateKeyA = Hex.padLeft('0x01', 32)
const privateKeyB = Hex.padLeft('0x02', 32)
const address = core_Address.fromPublicKey(Secp256k1.getPublicKey({ privateKey: privateKeyA }))
const other = core_Address.fromPublicKey(Secp256k1.getPublicKey({ privateKey: privateKeyB }))

function signWithKey(privateKey: Hex.Hex, payload: Hex.Hex): Hex.Hex {
  const signature = Secp256k1.sign({ payload, privateKey })
  return Signature.toHex(signature)
}

function privateKeyForAddress(walletAddress: string): Hex.Hex {
  if (core_Address.from(walletAddress) === address) return privateKeyA
  if (core_Address.from(walletAddress) === other) return privateKeyB
  throw new Error(`No test private key for ${walletAddress}`)
}

describe('privy', () => {
  test('default: createAccount delegates registration and signs the requested digest', async () => {
    const { adapter, client } = setup()

    const result = await adapter.actions.createAccount(
      { digest: '0x1234', name: 'Ada' },
      { method: 'wallet_connect', params: undefined },
    )

    expect(client.initCalls).toMatchInlineSnapshot(`1`)
    expect(client.signPayloads).toMatchInlineSnapshot(`
      [
        "0x1234",
      ]
    `)
    expect(result).toMatchInlineSnapshot(`
    	{
    	  "accounts": [
    	    {
    	      "address": "0x7e5f4552091a69125d5dfcb7b8c2659029395bdf",
    	      "label": "Ada",
    	    },
    	  ],
    	  "signature": "0xced9d002f487622c7e218274065c327bdfe274ea7da91349bb48fe7c4495baeb71cc6b2f9b3d5f34e5b404cec0ed0dcb085f990a7b7a7f4cb81a5e8abb76aa981b",
    	}
    `)
  })

  test('default: createAccount falls back to loadAccounts when not provided', async () => {
    const { adapter, client } = setup({ createAccount: false })

    const result = await adapter.actions.createAccount(
      { digest: '0x1234', name: 'Ada' },
      { method: 'wallet_connect', params: undefined },
    )

    expect(client.createCalls).toMatchInlineSnapshot(`0`)
    expect(client.loadCalls).toMatchInlineSnapshot(`1`)
    expect(client.signPayloads).toMatchInlineSnapshot(`
      [
        "0x1234",
      ]
    `)
    expect(result.accounts).toMatchInlineSnapshot(`
      [
        {
          "address": "0x7e5f4552091a69125d5dfcb7b8c2659029395bdf",
          "label": "Ada",
        },
      ]
    `)
  })

  test('default: createAccount can select the active embedded wallet', async () => {
    const { adapter, client } = setup({ createAddresses: [other] })
    client.addWallet(other)

    const result = await adapter.actions.createAccount(
      { digest: '0x1234', name: 'Ada' },
      { method: 'wallet_connect', params: undefined },
    )

    expect(client.signWith).toMatchInlineSnapshot(`
      [
        "0x2b5ad5c4795c026514f8317c7a215e218dccd6cf",
      ]
    `)
    expect(result.accounts).toMatchInlineSnapshot(`
      [
        {
          "address": "0x2b5ad5c4795c026514f8317c7a215e218dccd6cf",
          "label": "Ada",
        },
      ]
    `)
  })

  test('default: loadAccounts delegates login and caches embedded wallets for signing', async () => {
    const { adapter, client, store } = setup()

    await connect({ adapter, store })
    const result = await adapter.actions.signPersonalMessage(
      { address, data: '0x68656c6c6f' },
      { method: 'personal_sign', params: ['0x68656c6c6f', address] },
    )

    expect(client.loadCalls).toMatchInlineSnapshot(`1`)
    expect(client.signWith).toMatchInlineSnapshot(`
    	[
    	  "0x7e5f4552091a69125d5dfcb7b8c2659029395bdf",
    	]
    `)
    expect(result).toMatchInlineSnapshot(
      `"0xe5ddc160e4c8f92de507c7db9b982d4f9b7197bfa421864aeadc586bc96b09ae0ba0c5b131650ae4994cff1839341d00f3735ef5abc62ac8fe2cf50f65208e2a1b"`,
    )
  })

  test('default: loadAccounts can select and order embedded wallets', async () => {
    const { adapter, client } = setup({ loadAddresses: [other, address] })
    client.addWallet(other)

    const result = await adapter.actions.loadAccounts(
      { digest: '0x1234' },
      { method: 'wallet_connect', params: undefined },
    )

    expect(client.signWith).toMatchInlineSnapshot(`
      [
        "0x2b5ad5c4795c026514f8317c7a215e218dccd6cf",
      ]
    `)
    expect(result.accounts).toMatchInlineSnapshot(`
      [
        {
          "address": "0x2b5ad5c4795c026514f8317c7a215e218dccd6cf",
        },
        {
          "address": "0x7e5f4552091a69125d5dfcb7b8c2659029395bdf",
        },
      ]
    `)
  })

  test('default: loadAccounts can provision an external access key', async () => {
    const { adapter, client } = setup()

    const result = await adapter.actions.loadAccounts(
      {
        authorizeAccessKey: {
          address: other,
          expiry: 123,
          keyType: 'secp256k1',
        },
      },
      { method: 'wallet_connect', params: undefined },
    )

    expect(client.signPayloads).toMatchInlineSnapshot(`
    	[
    	  "0xe77ac2b1d13a90cbd8c4912ff18d0d044cc89c5c6781941001640b8d251f3783",
    	]
    `)
    expect(result).toMatchInlineSnapshot(`
    	{
    	  "accounts": [
    	    {
    	      "address": "0x7e5f4552091a69125d5dfcb7b8c2659029395bdf",
    	    },
    	  ],
    	  "keyAuthorization": {
    	    "chainId": "0x1",
    	    "expiry": "0x7b",
    	    "keyId": "0x2b5ad5c4795c026514f8317c7a215e218dccd6cf",
    	    "keyType": "secp256k1",
    	    "limits": undefined,
    	    "signature": {
    	      "r": "0xb364cd8e50555239adf9f7d655b018ea386764d44ed9b56e894f4a101f0b1a6b",
    	      "s": "0x4910cc8497358eb73a08df09c9cfb2618e3c949b3847ab310ad7ab0d76a9c624",
    	      "type": "secp256k1",
    	      "yParity": "0x1",
    	    },
    	  },
    	  "signature": undefined,
    	}
    `)
  })

  test('default: signs transactions with a materialized Privy account', async () => {
    const { adapter, client, store } = setup()
    await connect({ adapter, store })

    const result = await adapter.actions.signTransaction(
      {
        chainId: 1,
        from: address,
        gas: 21_000n,
        maxFeePerGas: 1n,
        maxPriorityFeePerGas: 1n,
        nonce: 0,
        to: other,
        value: 1n,
      },
      { method: 'eth_signTransaction', params: [{ from: address }] },
    )

    expect(client.signWith).toMatchInlineSnapshot(`
      [
        "0x7e5f4552091a69125d5dfcb7b8c2659029395bdf",
      ]
    `)
    expect(client.signPayloads).toMatchInlineSnapshot(`
      [
        "0x62f087d34b8a023e0461eb1b9a01267ba5b8400c13d2ffdac615ccec872cc288",
      ]
    `)
    expect(result).toMatchInlineSnapshot(
      `"0x76f86a010101825208d8d7942b5ad5c4795c026514f8317c7a215e218dccd6cf0180c0808080808080c0b8418b0c18077cb78666296a4c0e8149935124f70d7820ec4e4ae81de428659d6c305c098dea43ccb536e813bb1c136eab5e96611de69489be6291169b2bb2318cde1b"`,
    )
  })

  test('default: authorizeAccessKey signs with the connected Privy account', async () => {
    const { adapter, client, store } = setup()
    store.setState({ accounts: [{ address }], activeAccount: 0 })

    const result = await adapter.actions.authorizeAccessKey!(
      {
        address: other,
        expiry: 123,
        keyType: 'secp256k1',
      },
      { method: 'wallet_authorizeAccessKey', params: [{ expiry: 123 }] },
    )

    expect(client.loadCalls).toMatchInlineSnapshot(`0`)
    expect(client.restoreCalls).toMatchInlineSnapshot(`1`)
    expect(result).toMatchInlineSnapshot(`
    	{
    	  "keyAuthorization": {
    	    "chainId": "0x1",
    	    "expiry": "0x7b",
    	    "keyId": "0x2b5ad5c4795c026514f8317c7a215e218dccd6cf",
    	    "keyType": "secp256k1",
    	    "limits": undefined,
    	    "signature": {
    	      "r": "0xb364cd8e50555239adf9f7d655b018ea386764d44ed9b56e894f4a101f0b1a6b",
    	      "s": "0x4910cc8497358eb73a08df09c9cfb2618e3c949b3847ab310ad7ab0d76a9c624",
    	      "type": "secp256k1",
    	      "yParity": "0x1",
    	    },
    	  },
    	  "rootAddress": "0x7e5f4552091a69125d5dfcb7b8c2659029395bdf",
    	}
    `)
  })

  test('error: secp256k1 access key requires external key material', async () => {
    const { adapter, store } = setup()
    store.setState({ accounts: [{ address }], activeAccount: 0 })

    await expect(
      adapter.actions.authorizeAccessKey!(
        { expiry: 123, keyType: 'secp256k1' },
        { method: 'wallet_authorizeAccessKey', params: [{ expiry: 123, keyType: 'secp256k1' }] },
      ),
    ).rejects.toThrowErrorMatchingInlineSnapshot(
      `[RpcResponse.InvalidParamsError: \`keyType: "secp256k1"\` requires externally generated key material; provide \`publicKey\` or \`address\`.]`,
    )
  })

  test('default: revokeAccessKey revokes with the connected Privy account', async () => {
    const { adapter, client, store } = setup()
    store.setState({
      accounts: [{ address }],
      activeAccount: 0,
      accessKeys: [
        {
          access: address,
          address: other,
          chainId: 1,
          keyType: 'secp256k1',
        } as never,
      ],
    })

    await adapter.actions.revokeAccessKey!(
      { accessKeyAddress: other, address },
      { method: 'wallet_revokeAccessKey', params: [{ accessKeyAddress: other, address }] },
    )

    const transaction = client.transactions[0] as
      | { account: { address: Address }; data: Hex.Hex; to: Address }
      | undefined
    const decoded = transaction
      ? decodeFunctionData({ abi: Abis.accountKeychain, data: transaction.data })
      : undefined
    expect(
      transaction &&
        decoded && {
          account: transaction.account.address,
          args: decoded.args,
          functionName: decoded.functionName,
          to: transaction.to,
        },
    ).toMatchInlineSnapshot(`
        {
          "account": "0x7e5f4552091a69125d5dfcb7b8c2659029395bdf",
          "args": [
            "0x2B5AD5c4795c026514f8317c7a215E218DcCD6cF",
          ],
          "functionName": "revokeKey",
          "to": "0xaAAAaaAA00000000000000000000000000000000",
        }
      `)
    expect(store.getState().accessKeys).toMatchInlineSnapshot(`[]`)
  })

  test('behavior: signing silently restores wallet accounts via the Privy SDK', async () => {
    const { adapter, client, store } = setup()
    store.setState({ accounts: [{ address }], activeAccount: 0 })

    await adapter.actions.signPersonalMessage(
      { address, data: '0x68656c6c6f' },
      { method: 'personal_sign', params: ['0x68656c6c6f', address] },
    )

    expect(client.restoreCalls).toMatchInlineSnapshot(`1`)
    expect(client.loadCalls).toMatchInlineSnapshot(`0`)
  })

  test('behavior: silent restore does not connect accounts when the provider store is empty', async () => {
    const { adapter, client } = setup()

    await expect(
      adapter.actions.signPersonalMessage(
        { address, data: '0x68656c6c6f' },
        { method: 'personal_sign', params: ['0x68656c6c6f', address] },
      ),
    ).rejects.toMatchInlineSnapshot('[Provider.DisconnectedError: No Privy account connected.]')

    expect(client.loadCalls).toMatchInlineSnapshot(`0`)
    expect(client.restoreCalls).toMatchInlineSnapshot(`0`)
    expect(client.signPayloads).toMatchInlineSnapshot(`[]`)
  })

  test('behavior: silent restore only reconnects persisted provider accounts', async () => {
    const { adapter, client, store } = setup()
    client.addWallet(other)
    store.setState({ accounts: [{ address: other }], activeAccount: 0 })

    await adapter.actions.signPersonalMessage(
      { address: other, data: '0x68656c6c6f' },
      { method: 'personal_sign', params: ['0x68656c6c6f', other] },
    )

    expect(client.signWith).toMatchInlineSnapshot(`
    	[
    	  "0x2b5ad5c4795c026514f8317c7a215e218dccd6cf",
    	]
    `)
    expect(store.getState().accounts).toMatchInlineSnapshot(`
    	[
    	  {
    	    "address": "0x2b5ad5c4795c026514f8317c7a215e218dccd6cf",
    	  },
    	]
    `)
  })

  test('behavior: expired sessions clear provider accounts', async () => {
    const { adapter, client, store } = setup({ token: null })
    store.setState({ accounts: [{ address }], activeAccount: 0 })

    await expect(
      adapter.actions.signPersonalMessage(
        { address, data: '0x68656c6c6f' },
        { method: 'personal_sign', params: ['0x68656c6c6f', address] },
      ),
    ).rejects.toMatchInlineSnapshot('[Provider.DisconnectedError: Privy session expired.]')

    expect(client.signPayloads).toMatchInlineSnapshot(`[]`)
    expect(store.getState().accounts).toMatchInlineSnapshot(`[]`)
  })

  test('behavior: server session errors clear provider accounts', async () => {
    const { adapter, store } = setup({ signError: { code: 'embedded_wallet_request_error' } })
    store.setState({ accounts: [{ address }], activeAccount: 0 })

    await expect(
      adapter.actions.signPersonalMessage(
        { address, data: '0x68656c6c6f' },
        { method: 'personal_sign', params: ['0x68656c6c6f', address] },
      ),
    ).rejects.toMatchInlineSnapshot('[Provider.DisconnectedError: Privy session expired.]')

    expect(store.getState().accounts).toMatchInlineSnapshot(`[]`)
  })

  test('behavior: session errors are recognized via fuzzy code match', async () => {
    const { adapter, store } = setup({ signError: { code: 'session_invalid_token' } })
    store.setState({ accounts: [{ address }], activeAccount: 0 })

    await expect(
      adapter.actions.signPersonalMessage(
        { address, data: '0x68656c6c6f' },
        { method: 'personal_sign', params: ['0x68656c6c6f', address] },
      ),
    ).rejects.toMatchInlineSnapshot('[Provider.DisconnectedError: Privy session expired.]')

    expect(store.getState().accounts).toMatchInlineSnapshot(`[]`)
  })

  test('behavior: session errors are recognized via nested cause messages', async () => {
    const inner = new Error('User must be logged in to sign.')
    const outer = new Error('Wallet operation failed', { cause: inner })
    const { adapter, store } = setup({ signError: outer })
    store.setState({ accounts: [{ address }], activeAccount: 0 })

    await expect(
      adapter.actions.signPersonalMessage(
        { address, data: '0x68656c6c6f' },
        { method: 'personal_sign', params: ['0x68656c6c6f', address] },
      ),
    ).rejects.toMatchInlineSnapshot('[Provider.DisconnectedError: Privy session expired.]')

    expect(store.getState().accounts).toMatchInlineSnapshot(`[]`)
  })

  test('behavior: session errors are recognized via message fallback', async () => {
    const { adapter, store } = setup({
      signError: Object.assign(new Error('User must be logged in to sign.'), {}),
    })
    store.setState({ accounts: [{ address }], activeAccount: 0 })

    await expect(
      adapter.actions.signPersonalMessage(
        { address, data: '0x68656c6c6f' },
        { method: 'personal_sign', params: ['0x68656c6c6f', address] },
      ),
    ).rejects.toMatchInlineSnapshot('[Provider.DisconnectedError: Privy session expired.]')

    expect(store.getState().accounts).toMatchInlineSnapshot(`[]`)
  })

  test('behavior: silent restore clears stale persisted accounts when Privy no longer has them', async () => {
    const { adapter, client, store } = setup()
    // Persisted address that is NOT linked on the Privy user.
    store.setState({ accounts: [{ address: other }], activeAccount: 0 })

    await expect(
      adapter.actions.signPersonalMessage(
        { address: other, data: '0x68656c6c6f' },
        { method: 'personal_sign', params: ['0x68656c6c6f', other] },
      ),
    ).rejects.toMatchInlineSnapshot(
      '[Provider.DisconnectedError: Privy session no longer matches persisted accounts.]',
    )

    expect(client.signPayloads).toMatchInlineSnapshot(`[]`)
    // Stale persisted accounts are wiped so the adapter and store agree.
    expect(store.getState().accounts).toMatchInlineSnapshot(`[]`)
  })

  test('behavior: failed account selection does not poison silent restore cache', async () => {
    const { adapter, store } = setup({ loadAddresses: [other] })
    store.setState({ accounts: [{ address: other }], activeAccount: 0 })

    await expect(
      adapter.actions.loadAccounts(undefined, { method: 'wallet_connect', params: undefined }),
    ).rejects.toThrowErrorMatchingInlineSnapshot(
      `[Provider.UnauthorizedError: Privy callback returned address "${other}" that was not found in the user's embedded wallets.]`,
    )

    await expect(
      adapter.actions.signPersonalMessage(
        { address: other, data: '0x68656c6c6f' },
        { method: 'personal_sign', params: ['0x68656c6c6f', other] },
      ),
    ).rejects.toMatchInlineSnapshot(
      '[Provider.DisconnectedError: Privy session no longer matches persisted accounts.]',
    )
    expect(store.getState().accounts).toMatchInlineSnapshot(`[]`)
  })

  test('behavior: failed empty createAccount selection does not poison silent restore cache', async () => {
    const { adapter, store } = setup({ createAddresses: [] })
    store.setState({ accounts: [{ address: other }], activeAccount: 0 })

    await expect(
      adapter.actions.createAccount(
        { digest: '0x1234', name: 'Ada' },
        { method: 'wallet_connect', params: undefined },
      ),
    ).rejects.toThrowErrorMatchingInlineSnapshot(
      `[Provider.DisconnectedError: Privy returned no wallet.]`,
    )

    await expect(
      adapter.actions.signPersonalMessage(
        { address: other, data: '0x68656c6c6f' },
        { method: 'personal_sign', params: ['0x68656c6c6f', other] },
      ),
    ).rejects.toMatchInlineSnapshot(
      '[Provider.DisconnectedError: Privy session no longer matches persisted accounts.]',
    )
    expect(store.getState().accounts).toMatchInlineSnapshot(`[]`)
  })

  test('error: silent restore rejects non-hex secp256k1_sign results', async () => {
    const { adapter, store } = setup({ signResult: 'not-hex' })
    store.setState({ accounts: [{ address }], activeAccount: 0 })

    await expect(
      adapter.actions.signPersonalMessage(
        { address, data: '0x68656c6c6f' },
        { method: 'personal_sign', params: ['0x68656c6c6f', address] },
      ),
    ).rejects.toMatchInlineSnapshot(
      '[ProviderRpcError: Privy provider returned a non-hex secp256k1_sign result.]',
    )
  })

  test('error: app-returned wallet with malformed address is rejected at connect', async () => {
    const { adapter, client } = setup()
    client.wallets = [client.makeWallet('0xnot-an-address')]

    await expect(
      adapter.actions.loadAccounts(undefined, { method: 'wallet_connect', params: undefined }),
    ).rejects.toThrowError(/Address.*invalid/i)
  })

  test('error: malformed secp256k1_sign result is rejected by signer recovery', async () => {
    const { adapter, store } = setup({ signResult: '0x1234' })
    await connect({ adapter, store })

    await expect(
      adapter.actions.signPersonalMessage(
        { address, data: '0x68656c6c6f' },
        { method: 'personal_sign', params: ['0x68656c6c6f', address] },
      ),
    ).rejects.toMatchInlineSnapshot(
      `[Provider.UnauthorizedError: Privy provider returned a signature for "unknown" that does not match the requested wallet "${address}".]`,
    )
  })

  test('error: signing for an unconnected address while others are connected throws Unauthorized', async () => {
    const { adapter, store } = setup()
    await connect({ adapter, store })

    await expect(
      adapter.actions.signPersonalMessage(
        { address: other, data: '0x68656c6c6f' },
        { method: 'personal_sign', params: ['0x68656c6c6f', other] },
      ),
    ).rejects.toThrowErrorMatchingInlineSnapshot(
      `[Provider.UnauthorizedError: Account "${other}" not found.]`,
    )
  })

  test('error: unsupported secp256k1_sign maps to UnsupportedMethodError', async () => {
    const { adapter, store } = setup({ signError: { code: 4200, message: 'Method not supported' } })
    store.setState({ accounts: [{ address }], activeAccount: 0 })

    await expect(
      adapter.actions.signPersonalMessage(
        { address, data: '0x68656c6c6f' },
        { method: 'personal_sign', params: ['0x68656c6c6f', address] },
      ),
    ).rejects.toMatchInlineSnapshot(
      '[Provider.UnsupportedMethodError: Privy adapter requires raw secp256k1 hash signing via `secp256k1_sign` for Tempo transactions and access keys.]',
    )
  })

  test('disconnect: clears provider accounts and logs the user out of Privy', async () => {
    const { adapter, client, store } = setup()
    store.setState({ accounts: [{ address }], activeAccount: 0 })

    await adapter.actions.disconnect!()

    expect(client.logoutCalls).toMatchInlineSnapshot(`1`)
    expect(store.getState().accounts).toMatchInlineSnapshot(`[]`)
  })

  test('behavior: restore surfaces silent-restore session errors as `Privy session expired.`', async () => {
    const { adapter, store } = setup({
      restoreError: Object.assign(new Error('boom'), { code: 'session_expired' }),
    })
    store.setState({ accounts: [{ address }], activeAccount: 0 })

    await expect(
      adapter.actions.signPersonalMessage(
        { address, data: '0x68656c6c6f' },
        { method: 'personal_sign', params: ['0x68656c6c6f', address] },
      ),
    ).rejects.toMatchInlineSnapshot('[Provider.DisconnectedError: Privy session expired.]')

    expect(store.getState().accounts).toMatchInlineSnapshot(`[]`)
  })

  test('error: signature recovered from a different key is rejected as Unauthorized', async () => {
    const { adapter, store } = setup({ signWithPrivateKey: privateKeyB })
    await connect({ adapter, store })

    await expect(
      adapter.actions.signPersonalMessage(
        { address, data: '0x68656c6c6f' },
        { method: 'personal_sign', params: ['0x68656c6c6f', address] },
      ),
    ).rejects.toThrowErrorMatchingInlineSnapshot(
      `[Provider.UnauthorizedError: Privy provider returned a signature for "${other}" that does not match the requested wallet "${address}".]`,
    )
  })
})

function setup(options: setup.Options = {}) {
  const storage = Storage.memory()
  const store = Store.create({ chainId: 1, storage })
  const client = createClient(options)
  const adapter = privy({
    client,
    ...(options.createAccount === false
      ? {}
      : {
          createAccount: async () => {
            client.createCalls++
            return options.createAddresses
          },
        }),
    loadAccounts: async () => {
      client.loadCalls++
      return options.loadAddresses
    },
  })({
    getAccount: (() => {
      throw new Error('not implemented')
    }) as never,
    getClient: (() => ({
      chain: { id: 1 },
      sendTransaction: async (parameters: unknown) => {
        client.transactions.push(parameters)
        return Hex.padLeft('0x1', 32)
      },
    })) as never,
    storage,
    store,
  })
  return { adapter, client, store }
}

async function connect(options: Pick<ReturnType<typeof setup>, 'adapter' | 'store'>) {
  const { adapter, store } = options
  const loaded = await adapter.actions.loadAccounts(undefined, {
    method: 'wallet_connect',
    params: undefined,
  })
  store.setState({ accounts: loaded.accounts, activeAccount: 0 })
  return loaded
}

declare namespace setup {
  type Options = {
    /** Pass `false` to omit the adapter's `createAccount` callback (tests fallback to `loadAccounts`). */
    createAccount?: false | undefined
    createAddresses?: readonly Address[] | undefined
    loadAddresses?: readonly Address[] | undefined
    /** Make the mock client's `user.get` throw, to test restore-side session errors. */
    restoreError?: unknown
    token?: string | null | undefined
    signError?: unknown
    /** Override the value returned by the embedded provider's `secp256k1_sign`. */
    signResult?: unknown
    /** Force the test wallet to sign with this private key (for wrong-signer tests). */
    signWithPrivateKey?: Hex.Hex | undefined
  }
}

type MockClient = privy.Client & {
  createCalls: number
  initCalls: number
  loadCalls: number
  logoutCalls: number
  logoutWith: (string | undefined)[]
  restoreCalls: number
  signPayloads: Hex.Hex[]
  signWith: string[]
  transactions: unknown[]
  wallets: privy.EmbeddedWallet[]
  makeWallet: (address: string) => privy.EmbeddedWallet
  addWallet: (address: string) => void
}

function createClient(options: setup.Options = {}) {
  const client: MockClient = {
    createCalls: 0,
    initCalls: 0,
    loadCalls: 0,
    logoutCalls: 0,
    logoutWith: [] as (string | undefined)[],
    restoreCalls: 0,
    signPayloads: [] as Hex.Hex[],
    signWith: [] as string[],
    transactions: [] as unknown[],
    wallets: [] as privy.EmbeddedWallet[],
    makeWallet(address: string): privy.EmbeddedWallet {
      return {
        address,
        provider: {
          async request(req: {
            method: string
            params?: readonly unknown[] | undefined
          }): Promise<unknown> {
            if (req.method !== 'secp256k1_sign') throw new Error(`unexpected method: ${req.method}`)
            if (options.signError) throw options.signError
            const hash = (req.params as readonly Hex.Hex[])[0] as Hex.Hex
            client.signPayloads.push(hash)
            client.signWith.push(address)
            if (options.signResult !== undefined) return options.signResult
            const privateKey =
              options.signWithPrivateKey ??
              (() => {
                try {
                  return privateKeyForAddress(address)
                } catch {
                  return privateKeyA
                }
              })()
            return signWithKey(privateKey, hash)
          },
        },
      }
    },
    /** Adds an embedded wallet so silent restore (`user.get`) returns it. */
    addWallet(address: string) {
      client.wallets.push(client.makeWallet(address))
    },
    auth: {
      logout(parameters?: { userId: string } | undefined) {
        client.logoutCalls++
        client.logoutWith.push(parameters?.userId)
      },
    },
    embeddedWallet: {
      async getEthereumProvider({ wallet }) {
        const existing = client.wallets.find(
          (w) => core_Address.from(w.address) === core_Address.from(wallet.address as string),
        )
        return (existing ?? client.makeWallet(wallet.address as string)).provider
      },
    },
    async getAccessToken() {
      return options.token === undefined ? 'token' : options.token
    },
    initialize() {
      client.initCalls++
    },
    user: {
      async get() {
        client.restoreCalls++
        if (options.restoreError) throw options.restoreError
        return {
          user: {
            id: 'user_1',
            linked_accounts: client.wallets.map((wallet, index) => ({
              address: wallet.address,
              chain_type: 'ethereum',
              connector_type: 'embedded',
              type: 'wallet',
              wallet_client_type: 'privy',
              wallet_index: index,
            })),
          },
        }
      },
    },
  }

  client.addWallet(address)

  return client
}
