import { Rlp, Secp256k1, TxEnvelopeEip2930, Value } from 'ox'
import { assertType, describe, expect, expectTypeOf, test } from 'vp/test'
import { accounts } from '../../../test/constants/accounts.js'
import { anvilMainnet } from '../../../test/prool.js'

describe('assert', () => {
  test('fee cap too high', () => {
    expect(() =>
      TxEnvelopeEip2930.assert({
        gasPrice: 2n ** 256n - 1n + 1n,
        chainId: 1,
      }),
    ).toThrowErrorMatchingInlineSnapshot(
      '[TransactionEnvelope.GasPriceTooHighError: The gas price (`gasPrice` = 115792089237316195423570985008687907853269984665640564039457584007913.129639936 gwei) cannot be higher than the maximum allowed value (2^256-1).]',
    )
  })

  test('invalid chainId', () => {
    expect(() =>
      TxEnvelopeEip2930.assert({ chainId: 0 }),
    ).toThrowErrorMatchingInlineSnapshot(
      `[TransactionEnvelope.InvalidChainIdError: Chain ID "0" is invalid.]`,
    )
  })

  test('invalid address', () => {
    expect(() => TxEnvelopeEip2930.assert({ to: '0x123', chainId: 1 }))
      .toThrowErrorMatchingInlineSnapshot(`
    [Address.InvalidAddressError: Address "0x123" is invalid.

    Details: Address is not a 20 byte (40 hexadecimal character) value.]
  `)
  })
})

describe('deserialize', () => {
  const transaction = TxEnvelopeEip2930.from({
    chainId: 1,
    accessList: [
      {
        address: '0x1234512345123451234512345123451234512345',
        storageKeys: [
          '0x60fdd29ff912ce880cd3edaf9f932dc61d3dae823ea77e0323f94adb9f6a72fe',
        ],
      },
    ],
    gasPrice: Value.fromGwei('2'),
    to: accounts[1].address,
    nonce: 785n,
    value: Value.fromEther('1'),
  })

  test('default', () => {
    const serialized = TxEnvelopeEip2930.serialize(transaction)
    const deserialized = TxEnvelopeEip2930.deserialize(serialized)
    assertType<TxEnvelopeEip2930.TxEnvelopeEip2930>(deserialized)
    expect(deserialized).toEqual(transaction)
  })

  test('minimal', () => {
    const transaction = TxEnvelopeEip2930.from({
      chainId: 1,
      gasPrice: 1n,
      nonce: 0n,
      accessList: [
        {
          address: '0x0000000000000000000000000000000000000000',
          storageKeys: [
            '0x0000000000000000000000000000000000000000000000000000000000000001',
          ],
        },
      ],
    })
    const serialized = TxEnvelopeEip2930.serialize(transaction)
    expect(TxEnvelopeEip2930.deserialize(serialized)).toEqual(transaction)
  })

  test('gas', () => {
    const transaction_gas = TxEnvelopeEip2930.from({
      ...transaction,
      gas: 21001n,
    })
    const serialized = TxEnvelopeEip2930.serialize(transaction_gas)
    expect(TxEnvelopeEip2930.deserialize(serialized)).toEqual(transaction_gas)
  })

  test('data', () => {
    const transaction_data = TxEnvelopeEip2930.from({
      ...transaction,
      data: '0x1234',
    })
    const serialized = TxEnvelopeEip2930.serialize(transaction_data)
    expect(TxEnvelopeEip2930.deserialize(serialized)).toEqual(transaction_data)
  })

  test('data', () => {
    const transaction_data = TxEnvelopeEip2930.from({
      ...transaction,
      data: '0x1234',
    })
    const serialized = TxEnvelopeEip2930.serialize(transaction_data)
    expect(TxEnvelopeEip2930.deserialize(serialized)).toEqual(transaction_data)
  })

  test('signature', async () => {
    const signature = Secp256k1.sign({
      payload: TxEnvelopeEip2930.getSignPayload(transaction),
      privateKey: accounts[0].privateKey,
    })
    const serialized = TxEnvelopeEip2930.serialize(transaction, {
      signature,
    })
    expect(TxEnvelopeEip2930.deserialize(serialized)).toEqual({
      ...transaction,
      ...signature,
    })
  })

  describe('errors', () => {
    test('invalid transaction (all missing)', () => {
      expect(() =>
        TxEnvelopeEip2930.deserialize(`0x01${Rlp.fromHex([]).slice(2)}`),
      ).toThrowErrorMatchingInlineSnapshot(`
      [TransactionEnvelope.InvalidSerializedError: Invalid serialized transaction of type "eip2930" was provided.

      Serialized Transaction: "0x01c0"
      Missing Attributes: chainId, nonce, gasPrice, gas, to, value, data, accessList]
    `)
    })

    test('invalid transaction (some missing)', () => {
      expect(() =>
        TxEnvelopeEip2930.deserialize(
          `0x01${Rlp.fromHex(['0x00', '0x01']).slice(2)}`,
        ),
      ).toThrowErrorMatchingInlineSnapshot(`
      [TransactionEnvelope.InvalidSerializedError: Invalid serialized transaction of type "eip2930" was provided.

      Serialized Transaction: "0x01c20001"
      Missing Attributes: gasPrice, gas, to, value, data, accessList]
    `)
    })

    test('invalid transaction (missing signature)', () => {
      expect(() =>
        TxEnvelopeEip2930.deserialize(
          `0x01${Rlp.fromHex([
            '0x',
            '0x',
            '0x',
            '0x',
            '0x',
            '0x',
            '0x',
            '0x',
            '0x',
          ]).slice(2)}`,
        ),
      ).toThrowErrorMatchingInlineSnapshot(`
      [TransactionEnvelope.InvalidSerializedError: Invalid serialized transaction of type "eip2930" was provided.

      Serialized Transaction: "0x01c9808080808080808080"
      Missing Attributes: r, s]
    `)
    })
  })
})

describe('from', () => {
  test('default', () => {
    {
      const envelope = TxEnvelopeEip2930.from({
        accessList: [],
        chainId: 1,
        gasPrice: 69420n,
        nonce: 0n,
      })
      expectTypeOf(
        envelope,
      ).toMatchTypeOf<TxEnvelopeEip2930.TxEnvelopeEip2930>()
      expect(envelope).toMatchInlineSnapshot(`
        {
          "accessList": [],
          "chainId": 1,
          "gasPrice": 69420n,
          "nonce": 0n,
          "type": "eip2930",
        }
      `)
    }

    {
      const envelope = TxEnvelopeEip2930.from({
        chainId: 1,
        gasPrice: 69420n,
        nonce: 0n,
      })
      const serialized = TxEnvelopeEip2930.serialize(envelope)
      expect(TxEnvelopeEip2930.from(serialized)).toEqual(envelope)
    }

    {
      const envelope = TxEnvelopeEip2930.from({
        chainId: 1,
        gasPrice: 69420n,
        r: '0x0000000000000000000000000000000000000000000000000000000000000000',
        s: '0x0000000000000000000000000000000000000000000000000000000000000001',
        yParity: 0,
        nonce: 0n,
      })
      const serialized = TxEnvelopeEip2930.serialize(envelope)
      const envelope2 = TxEnvelopeEip2930.from(serialized)
      expect(envelope2).toEqual(envelope)
    }
  })

  test('options: signature', () => {
    const envelope = TxEnvelopeEip2930.from(
      {
        chainId: 1,
        to: '0x0000000000000000000000000000000000000000',
        nonce: 0n,
        value: 69n,
      },
      {
        signature: {
          r: '0x0000000000000000000000000000000000000000000000000000000000000000',
          s: '0x0000000000000000000000000000000000000000000000000000000000000001',
          yParity: 0,
        },
      },
    )
    expect(envelope).toMatchInlineSnapshot(`
      {
        "chainId": 1,
        "nonce": 0n,
        "r": "0x0000000000000000000000000000000000000000000000000000000000000000",
        "s": "0x0000000000000000000000000000000000000000000000000000000000000001",
        "to": "0x0000000000000000000000000000000000000000",
        "type": "eip2930",
        "value": 69n,
        "yParity": 0,
      }
    `)
    const serialized = TxEnvelopeEip2930.serialize(envelope)
    const envelope2 = TxEnvelopeEip2930.from(serialized)
    expect(envelope2).toEqual(envelope)
  })
})

describe('getSignPayload', () => {
  test('default', () => {
    const envelope = TxEnvelopeEip2930.from({
      chainId: 1,
      gas: 21000n,
      maxFeePerGas: 13000000000n,
      maxPriorityFeePerGas: 1000000000n,
      nonce: 665n,
      value: 1000000000000000000n,
      to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8',
      type: 'eip2930',
    })

    const hash = TxEnvelopeEip2930.hash(envelope, { presign: true })
    expect(hash).toMatchInlineSnapshot(
      `"0x0ec6ac926e256ac597c5e6a2e635019ccd07974365f970f479589ace534db44a"`,
    )

    const signature = Secp256k1.sign({
      payload: TxEnvelopeEip2930.getSignPayload(envelope),
      privateKey: accounts[0].privateKey,
    })

    const envelope_signed = TxEnvelopeEip2930.from(envelope, {
      signature,
    })

    {
      const hash = TxEnvelopeEip2930.hash(envelope_signed)
      expect(hash).toMatchInlineSnapshot(
        `"0x70bbd82fb81c02a945125319cb58a42a69f38426d0870f9be2b196437b3dd177"`,
      )
    }
    {
      const hash_presign = TxEnvelopeEip2930.getSignPayload(envelope_signed)
      expect(hash_presign).toEqual(hash)
    }
  })
})

describe('hash', () => {
  test('default', () => {
    const envelope = TxEnvelopeEip2930.from({
      accessList: [
        {
          address: '0x0000000000000000000000000000000000000000',
          storageKeys: [
            '0x0000000000000000000000000000000000000000000000000000000000000001',
          ],
        },
      ],
      chainId: 1,
      gas: 21000n,
      gasPrice: 1000000000n,
      nonce: 665n,
      value: 1000000000000000000n,
      to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8',
      type: 'eip2930',
      r: '0xacf664dcd984d082b68c434feb66ac684711babdeefe6f101bf8df88fc367a37',
      s: '0x5e0800058a9b5c2250bed60ee969a45b7445e562a8298c2d222d114e6dfbfcb9',
      v: 27,
    })

    const hash = TxEnvelopeEip2930.hash(envelope)
    expect(hash).toMatchInlineSnapshot(
      `"0xfcf39cca082499fb8b96317cc525a6697d65cf7bc8d7fc30d9e8d9b4c45d9a51"`,
    )
  })
})

describe('serialize', () => {
  const transaction = TxEnvelopeEip2930.from({
    gasPrice: Value.fromGwei('2'),
    nonce: 785n,
    to: accounts[1].address,
    value: Value.fromEther('1'),
    chainId: 1,
    accessList: [
      {
        address: '0x1234512345123451234512345123451234512345',
        storageKeys: [
          '0x60fdd29ff912ce880cd3edaf9f932dc61d3dae823ea77e0323f94adb9f6a72fe',
        ],
      },
    ],
  })

  test('default', () => {
    const serialized = TxEnvelopeEip2930.serialize(transaction)
    assertType<TxEnvelopeEip2930.Serialized>(serialized)
    expect(serialized).toEqual(
      '0x01f863018203118477359400809470997970c51812dc3a010c7d01b50e0d17dc79c8880de0b6b3a764000080f838f7941234512345123451234512345123451234512345e1a060fdd29ff912ce880cd3edaf9f932dc61d3dae823ea77e0323f94adb9f6a72fe',
    )
    expect(TxEnvelopeEip2930.deserialize(serialized)).toEqual(transaction)
  })

  test('default (all zeros)', () => {
    const transaction = TxEnvelopeEip2930.from({
      to: accounts[1].address,
      nonce: 0n,
      chainId: 1,
      value: 0n,
      gasPrice: 0n,
      accessList: [],
    })

    const serialized = TxEnvelopeEip2930.serialize(transaction)

    expect(serialized).toEqual(
      '0x01dc018080809470997970c51812dc3a010c7d01b50e0d17dc79c88080c0',
    )
    expect(TxEnvelopeEip2930.deserialize(serialized)).toEqual({
      chainId: 1,
      nonce: 0n,
      to: accounts[1].address,
      type: 'eip2930',
    })
  })

  test('minimal', () => {
    const transaction = TxEnvelopeEip2930.from({
      chainId: 1,
      nonce: 0n,
    })
    const serialized = TxEnvelopeEip2930.serialize(transaction)
    expect(serialized).toEqual('0x01c801808080808080c0')
    expect(TxEnvelopeEip2930.deserialize(serialized)).toEqual({
      chainId: 1,
      nonce: 0n,
      type: 'eip2930',
    })
  })

  test('minimal (w/ accessList & gasPrice)', () => {
    const transaction = TxEnvelopeEip2930.from({
      chainId: 1,
      accessList: [
        {
          address: '0x0000000000000000000000000000000000000000',
          storageKeys: [
            '0x0000000000000000000000000000000000000000000000000000000000000001',
          ],
        },
      ],
      gasPrice: Value.fromGwei('2'),
      nonce: 0n,
    })
    const serialized = TxEnvelopeEip2930.serialize(transaction)
    expect(serialized).toEqual(
      '0x01f8450180847735940080808080f838f7940000000000000000000000000000000000000000e1a00000000000000000000000000000000000000000000000000000000000000001',
    )
    expect(TxEnvelopeEip2930.deserialize(serialized)).toEqual(transaction)
  })

  test('gas', () => {
    const transaction_gas = TxEnvelopeEip2930.from({
      ...transaction,
      gas: 21001n,
    })
    const serialized = TxEnvelopeEip2930.serialize(transaction_gas)
    expect(serialized).toEqual(
      '0x01f8650182031184773594008252099470997970c51812dc3a010c7d01b50e0d17dc79c8880de0b6b3a764000080f838f7941234512345123451234512345123451234512345e1a060fdd29ff912ce880cd3edaf9f932dc61d3dae823ea77e0323f94adb9f6a72fe',
    )
    expect(TxEnvelopeEip2930.deserialize(serialized)).toEqual(transaction_gas)
  })

  test('data', () => {
    const transaction_data = TxEnvelopeEip2930.from({
      ...transaction,
      data: '0x1234',
    })
    const serialized = TxEnvelopeEip2930.serialize(transaction_data)
    expect(serialized).toEqual(
      '0x01f865018203118477359400809470997970c51812dc3a010c7d01b50e0d17dc79c8880de0b6b3a7640000821234f838f7941234512345123451234512345123451234512345e1a060fdd29ff912ce880cd3edaf9f932dc61d3dae823ea77e0323f94adb9f6a72fe',
    )
    expect(TxEnvelopeEip2930.deserialize(serialized)).toEqual(transaction_data)
  })

  test('options: signature', async () => {
    const signature = Secp256k1.sign({
      payload: TxEnvelopeEip2930.getSignPayload(transaction),
      privateKey: accounts[0].privateKey,
    })
    const serialized = TxEnvelopeEip2930.serialize(transaction, {
      signature,
    })
    expect(serialized).toEqual(
      '0x01f8a6018203118477359400809470997970c51812dc3a010c7d01b50e0d17dc79c8880de0b6b3a764000080f838f7941234512345123451234512345123451234512345e1a060fdd29ff912ce880cd3edaf9f932dc61d3dae823ea77e0323f94adb9f6a72fe01a0dc7b3483c0b183823f07d77247c60678d861080acdc4fb8b9fd131770b475c40a040f16567391132746735aff4d5a3fa5ae42ff3d5d538e341870e0259dc40741a',
    )
    expect(TxEnvelopeEip2930.deserialize(serialized)).toEqual({
      ...transaction,
      ...signature,
    })
  })

  test('options: signature', () => {
    expect(
      TxEnvelopeEip2930.serialize(transaction, {
        signature: {
          r: '0x60fdd29ff912ce880cd3edaf9f932dc61d3dae823ea77e0323f94adb9f6a72fe',
          s: '0x60fdd29ff912ce880cd3edaf9f932dc61d3dae823ea77e0323f94adb9f6a72fe',
          yParity: 1,
        },
      }),
    ).toEqual(
      '0x01f8a6018203118477359400809470997970c51812dc3a010c7d01b50e0d17dc79c8880de0b6b3a764000080f838f7941234512345123451234512345123451234512345e1a060fdd29ff912ce880cd3edaf9f932dc61d3dae823ea77e0323f94adb9f6a72fe01a060fdd29ff912ce880cd3edaf9f932dc61d3dae823ea77e0323f94adb9f6a72fea060fdd29ff912ce880cd3edaf9f932dc61d3dae823ea77e0323f94adb9f6a72fe',
    )
    expect(
      TxEnvelopeEip2930.serialize(transaction, {
        signature: {
          r: '0x60fdd29ff912ce880cd3edaf9f932dc61d3dae823ea77e0323f94adb9f6a72fe',
          s: '0x60fdd29ff912ce880cd3edaf9f932dc61d3dae823ea77e0323f94adb9f6a72fe',
          yParity: 0,
        },
      }),
    ).toEqual(
      '0x01f8a6018203118477359400809470997970c51812dc3a010c7d01b50e0d17dc79c8880de0b6b3a764000080f838f7941234512345123451234512345123451234512345e1a060fdd29ff912ce880cd3edaf9f932dc61d3dae823ea77e0323f94adb9f6a72fe80a060fdd29ff912ce880cd3edaf9f932dc61d3dae823ea77e0323f94adb9f6a72fea060fdd29ff912ce880cd3edaf9f932dc61d3dae823ea77e0323f94adb9f6a72fe',
    )
  })
})

describe('toRpc', () => {
  test('default', () => {
    const transaction = TxEnvelopeEip2930.toRpc({
      chainId: 1,
      nonce: 0n,
      gas: 21000n,
      gasPrice: Value.fromGwei('10'),
      to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8',
      value: 1000000000000000000n,
      r: '0x0000000000000000000000000000000000000000000000000000000000000001',
      s: '0x0000000000000000000000000000000000000000000000000000000000000002',
      yParity: 0,
    })
    expect(transaction).toMatchInlineSnapshot(`
      {
        "chainId": "0x1",
        "data": undefined,
        "gas": "0x5208",
        "gasPrice": "0x2540be400",
        "nonce": "0x0",
        "r": "0x0000000000000000000000000000000000000000000000000000000000000001",
        "s": "0x0000000000000000000000000000000000000000000000000000000000000002",
        "to": "0x70997970c51812dc3a010c7d01b50e0d17dc79c8",
        "type": "0x1",
        "value": "0xde0b6b3a7640000",
        "yParity": "0x0",
      }
    `)
  })

  test('numberish inputs', () => {
    const fromBigint = TxEnvelopeEip2930.toRpc({
      chainId: 1,
      nonce: 0n,
      gas: 21000n,
      gasPrice: 1000n,
      to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8',
      value: 1000n,
      r: '0x0000000000000000000000000000000000000000000000000000000000000001',
      s: '0x0000000000000000000000000000000000000000000000000000000000000002',
      yParity: 0,
    })
    const fromNumber = TxEnvelopeEip2930.toRpc({
      chainId: 1,
      nonce: 0,
      gas: 21000,
      gasPrice: 1000,
      to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8',
      value: 1000,
      r: '0x0000000000000000000000000000000000000000000000000000000000000001',
      s: '0x0000000000000000000000000000000000000000000000000000000000000002',
      yParity: 0,
    })
    const fromHex = TxEnvelopeEip2930.toRpc({
      chainId: '0x1',
      nonce: '0x0',
      gas: '0x5208',
      gasPrice: '0x3e8',
      to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8',
      value: '0x3e8',
      r: '0x0000000000000000000000000000000000000000000000000000000000000001',
      s: '0x0000000000000000000000000000000000000000000000000000000000000002',
      yParity: '0x0',
    })
    expect(fromBigint).toEqual(fromNumber)
    expect(fromBigint).toEqual(fromHex)
  })

  test('behavior: nullish', () => {
    const transaction = TxEnvelopeEip2930.toRpc({
      chainId: 1,
      to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8',
    })
    expect(transaction).toMatchInlineSnapshot(`
      {
        "chainId": "0x1",
        "data": undefined,
        "to": "0x70997970c51812dc3a010c7d01b50e0d17dc79c8",
        "type": "0x1",
      }
    `)
  })

  test('behavior: network', async () => {
    const nonce = await anvilMainnet.request({
      method: 'eth_getTransactionCount',
      params: [accounts[0].address, 'pending'],
    })

    const transaction = TxEnvelopeEip2930.from({
      chainId: 1,
      nonce: BigInt(nonce),
      gas: 21000n,
      gasPrice: Value.fromGwei('10'),
      from: accounts[0].address,
      to: accounts[1].address,
      value: Value.fromEther('1'),
    })

    const rpc = TxEnvelopeEip2930.toRpc(transaction)

    const hash = await anvilMainnet.request({
      method: 'eth_sendTransaction',
      params: [rpc],
    })

    expect(hash).toBeDefined()

    const tx = await anvilMainnet.request({
      method: 'eth_getTransactionByHash',
      params: [hash],
    })

    expect(tx).toMatchObject({
      chainId: '0x1',
      from: '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266',
      gas: '0x5208',
      gasPrice: '0x2540be400',
      input: '0x',
      to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8',
      type: '0x0',
      value: '0xde0b6b3a7640000',
    })
  })
})

describe('validate', () => {
  test('default', () => {
    expect(
      TxEnvelopeEip2930.validate({
        chainId: 1,
        gasPrice: 69n,
      }),
    ).toBe(true)
    expect(
      TxEnvelopeEip2930.validate({
        chainId: 1,
        gasPrice: 2n ** 257n,
      }),
    ).toBe(false)
  })
})

test('exports', () => {
  expect(Object.keys(TxEnvelopeEip2930)).toMatchInlineSnapshot(`
    [
      "serializedType",
      "type",
      "assert",
      "deserialize",
      "from",
      "getSignPayload",
      "hash",
      "serialize",
      "toRpc",
      "validate",
    ]
  `)
})
