import { Bytes, Hex, Rlp } from 'ox'
import { describe, expect, test } from 'vp/test'

test('exports', () => {
  expect(Object.keys(Rlp)).toMatchInlineSnapshot(`
    [
      "toBytes",
      "toHex",
      "to",
      "decodeRlpCursor",
      "readLength",
      "readList",
      "from",
      "fromBytes",
      "fromHex",
      "DepthLimitExceededError",
      "ListBoundaryExceededError",
      "TrailingBytesError",
    ]
  `)
})

const generateBytes = (length: number) => {
  const bytes = new Uint8Array(length)
  for (let i = 0; i < length; i++) bytes[i] = i
  return bytes
}

const generateList = (length: number) => {
  const bytes: Uint8Array[] = []
  for (let i = 0; i < length; i++) bytes.push(generateBytes(i % 8))
  return bytes
}

describe('Rlp.to', () => {
  test('no bytes', () => {
    const rlpHex = '0x'
    const rlpBytes = Uint8Array.from([])

    // hex -> bytes
    expect(Rlp.toBytes(rlpHex)).toStrictEqual(new Uint8Array([]))
    // hex -> hex
    expect(Rlp.toHex(rlpHex)).toEqual('0x')
    // bytes -> bytes
    expect(Rlp.toBytes(rlpBytes)).toStrictEqual(new Uint8Array([]))
    // bytes -> hex
    expect(Rlp.toHex(rlpBytes)).toEqual('0x')
  })

  describe('prefix < 0x80', () => {
    test('bytes -> bytes', () => {
      expect(Rlp.toBytes(Rlp.fromBytes(Bytes.fromHex('0x00')))).toEqual(
        Bytes.fromHex('0x00'),
      )
      expect(Rlp.toBytes(Rlp.fromBytes(Bytes.fromHex('0x01')))).toEqual(
        Bytes.fromHex('0x01'),
      )
      expect(Rlp.toBytes(Rlp.fromBytes(Bytes.fromHex('0x42')))).toEqual(
        Bytes.fromHex('0x42'),
      )
      expect(Rlp.toBytes(Rlp.fromBytes(Bytes.fromHex('0x7f')))).toEqual(
        Bytes.fromHex('0x7f'),
      )
      expect(Rlp.toBytes(Rlp.fromBytes(Bytes.fromString('!')))).toEqual(
        Bytes.fromHex('0x21'),
      )
      expect(Rlp.toBytes(Rlp.fromBytes(Bytes.fromString('a')))).toEqual(
        Bytes.fromHex('0x61'),
      )
      expect(Rlp.toBytes(Rlp.fromBytes(Bytes.fromString('~')))).toEqual(
        Bytes.fromHex('0x7e'),
      )
      expect(Rlp.toBytes(Rlp.fromBytes(Bytes.fromBoolean(true)))).toEqual(
        Bytes.fromHex('0x01'),
      )
      expect(Rlp.toBytes(Rlp.fromBytes(Bytes.fromBoolean(false)))).toEqual(
        Bytes.fromHex('0x00'),
      )
      expect(Rlp.toBytes(Rlp.fromBytes(Bytes.fromNumber(0)))).toEqual(
        Bytes.fromHex('0x00'),
      )
      expect(Rlp.toBytes(Rlp.fromBytes(Bytes.fromNumber(69)))).toEqual(
        Bytes.fromHex('0x45'),
      )
      expect(Rlp.toBytes(Rlp.fromBytes(Bytes.fromNumber(127)))).toEqual(
        Bytes.fromHex('0x7f'),
      )
    })

    test('bytes -> hex', () => {
      expect(Rlp.toHex(Rlp.from(Bytes.fromHex('0x00'), { as: 'Hex' }))).toEqual(
        '0x00',
      )
      expect(Rlp.toHex(Rlp.from(Bytes.fromHex('0x01'), { as: 'Hex' }))).toEqual(
        '0x01',
      )
      expect(Rlp.toHex(Rlp.from(Bytes.fromHex('0x42'), { as: 'Hex' }))).toEqual(
        '0x42',
      )
      expect(Rlp.toHex(Rlp.from(Bytes.fromHex('0x7f'), { as: 'Hex' }))).toEqual(
        '0x7f',
      )
      expect(Rlp.toHex(Rlp.from(Bytes.fromString('!'), { as: 'Hex' }))).toEqual(
        '0x21',
      )
      expect(Rlp.toHex(Rlp.from(Bytes.fromString('a'), { as: 'Hex' }))).toEqual(
        '0x61',
      )
      expect(Rlp.toHex(Rlp.from(Bytes.fromString('~'), { as: 'Hex' }))).toEqual(
        '0x7e',
      )
      expect(
        Rlp.toHex(Rlp.from(Bytes.fromBoolean(true), { as: 'Hex' })),
      ).toEqual('0x01')
      expect(
        Rlp.toHex(Rlp.from(Bytes.fromBoolean(false), { as: 'Hex' })),
      ).toEqual('0x00')
      expect(Rlp.toHex(Rlp.from(Bytes.fromNumber(0), { as: 'Hex' }))).toEqual(
        '0x00',
      )
      expect(Rlp.toHex(Rlp.from(Bytes.fromNumber(69), { as: 'Hex' }))).toEqual(
        '0x45',
      )
      expect(Rlp.toHex(Rlp.from(Bytes.fromNumber(127), { as: 'Hex' }))).toEqual(
        '0x7f',
      )
    })

    test('hex -> hex', () => {
      expect(Rlp.toHex(Rlp.from('0x00', { as: 'Hex' }))).toEqual('0x00')
      expect(Rlp.toHex(Rlp.from('0x01', { as: 'Hex' }))).toEqual('0x01')
      expect(Rlp.toHex(Rlp.from('0x42', { as: 'Hex' }))).toEqual('0x42')
      expect(Rlp.toHex(Rlp.fromHex('0x7f'))).toEqual('0x7f')
      expect(Rlp.toHex(Rlp.fromHex(Hex.fromString('!')))).toEqual('0x21')
      expect(Rlp.toHex(Rlp.fromHex(Hex.fromString('a')))).toEqual('0x61')
      expect(Rlp.toHex(Rlp.fromHex(Hex.fromString('~')))).toEqual('0x7e')
      expect(Rlp.toHex(Rlp.fromHex(Hex.fromBoolean(true)))).toEqual('0x01')
      expect(Rlp.toHex(Rlp.fromHex(Hex.fromBoolean(false)))).toEqual('0x00')
      expect(Rlp.toHex(Rlp.fromHex(Hex.fromNumber(0)))).toEqual('0x00')
      expect(Rlp.toHex(Rlp.fromHex(Hex.fromNumber(69)))).toEqual('0x45')
      expect(Rlp.toHex(Rlp.fromHex(Hex.fromNumber(127)))).toEqual('0x7f')
    })

    test('hex -> bytes', () => {
      expect(Rlp.toBytes(Rlp.from('0x00', { as: 'Bytes' }))).toEqual(
        Bytes.fromHex('0x00'),
      )
      expect(Rlp.toBytes(Rlp.from('0x01', { as: 'Bytes' }))).toEqual(
        Bytes.fromHex('0x01'),
      )
      expect(Rlp.toBytes(Rlp.from('0x42', { as: 'Bytes' }))).toEqual(
        Bytes.fromHex('0x42'),
      )
      expect(Rlp.toBytes(Rlp.from('0x7f', { as: 'Bytes' }))).toEqual(
        Bytes.fromHex('0x7f'),
      )
      expect(
        Rlp.toBytes(Rlp.from(Hex.fromString('!'), { as: 'Bytes' })),
      ).toEqual(Bytes.fromHex('0x21'))
      expect(
        Rlp.toBytes(Rlp.from(Hex.fromString('a'), { as: 'Bytes' })),
      ).toEqual(Bytes.fromHex('0x61'))
      expect(
        Rlp.toBytes(Rlp.from(Hex.fromString('~'), { as: 'Bytes' })),
      ).toEqual(Bytes.fromHex('0x7e'))
      expect(
        Rlp.toBytes(Rlp.from(Hex.fromBoolean(true), { as: 'Bytes' })),
      ).toEqual(Bytes.fromHex('0x01'))
      expect(
        Rlp.toBytes(Rlp.from(Hex.fromBoolean(false), { as: 'Bytes' })),
      ).toEqual(Bytes.fromHex('0x00'))
      expect(Rlp.toBytes(Rlp.from(Hex.fromNumber(0), { as: 'Bytes' }))).toEqual(
        Bytes.fromHex('0x00'),
      )
      expect(
        Rlp.toBytes(Rlp.from(Hex.fromNumber(69), { as: 'Bytes' })),
      ).toEqual(Bytes.fromHex('0x45'))
      expect(
        Rlp.toBytes(Rlp.from(Hex.fromNumber(127), { as: 'Bytes' })),
      ).toEqual(Bytes.fromHex('0x7f'))
    })
  })

  describe('list', () => {
    test('no bytes', () => {
      // bytes -> bytes
      expect(Rlp.toBytes(Rlp.fromBytes([]))).toEqual([])
      // bytes -> hex
      expect(Rlp.toHex(Rlp.fromBytes([]))).toEqual([])
      // hex -> hex
      expect(Rlp.toHex(Rlp.fromHex([]))).toEqual([])
      // hex -> bytes
      expect(Rlp.toBytes(Rlp.fromHex([]))).toEqual([])
    })

    test('inner no bytes', () => {
      // bytes -> bytes
      expect(Rlp.toBytes(Rlp.fromBytes([[]]))).toEqual([[]])
      // bytes -> hex
      expect(Rlp.toHex(Rlp.fromBytes([[]]))).toEqual([[]])
      // hex -> hex
      expect(Rlp.toHex(Rlp.fromHex([[]]))).toEqual([[]])
      // hex -> bytes
      expect(Rlp.toBytes(Rlp.fromHex([[]]))).toEqual([[]])
    })

    describe('prefix < 0xf8', () => {
      test('bytes -> bytes', () => {
        expect(Rlp.toBytes(Rlp.fromBytes([Bytes.fromHex('0x00')]))).toEqual([
          Bytes.fromHex('0x00'),
        ])
        expect(Rlp.toBytes(Rlp.fromBytes([Bytes.fromHex('0x80')]))).toEqual([
          Bytes.fromHex('0x80'),
        ])
        expect(Rlp.toBytes(Rlp.fromBytes(generateList(14)))).toEqual(
          generateList(14),
        )
        expect(
          Rlp.toBytes(
            Rlp.fromBytes([
              generateList(4),
              [generateList(8), [generateList(3), generateBytes(1)]],
            ]),
          ),
        ).toEqual([
          generateList(4),
          [generateList(8), [generateList(3), generateBytes(1)]],
        ])
      })

      test('bytes -> hex', () => {
        expect(Rlp.toHex(Rlp.from(['0x00'], { as: 'Bytes' }))).toEqual(['0x00'])
        expect(Rlp.toHex(Rlp.from(['0x80'], { as: 'Bytes' }))).toEqual(['0x80'])
        expect(Rlp.toHex(Rlp.from(generateList(14), { as: 'Bytes' }))).toEqual(
          generateList(14).map((x) => Hex.fromBytes(x)),
        )
        expect(
          Rlp.toHex(
            Rlp.from(
              [
                generateList(4),
                [generateList(8), [generateList(3), generateBytes(1)]],
              ],
              { as: 'Bytes' },
            ),
          ),
        ).toEqual([
          generateList(4).map((x) => Hex.fromBytes(x)),
          [
            generateList(8).map((x) => Hex.fromBytes(x)),
            [
              generateList(3).map((x) => Hex.fromBytes(x)),
              Hex.fromBytes(generateBytes(1)),
            ],
          ],
        ])
      })

      test('hex -> hex', () => {
        expect(Rlp.toHex(Rlp.fromHex(['0x00']))).toEqual(['0x00'])
        expect(Rlp.toHex(Rlp.fromHex(['0x80']))).toEqual(['0x80'])
        expect(
          Rlp.toHex(Rlp.fromHex(generateList(14).map((x) => Hex.fromBytes(x)))),
        ).toEqual(generateList(14).map((x) => Hex.fromBytes(x)))
        expect(
          Rlp.toHex(
            Rlp.fromHex([
              generateList(4).map((x) => Hex.fromBytes(x)),
              [
                generateList(8).map((x) => Hex.fromBytes(x)),
                [
                  generateList(3).map((x) => Hex.fromBytes(x)),
                  Hex.fromBytes(generateBytes(1)),
                ],
              ],
            ]),
          ),
        ).toEqual([
          generateList(4).map((x) => Hex.fromBytes(x)),
          [
            generateList(8).map((x) => Hex.fromBytes(x)),
            [
              generateList(3).map((x) => Hex.fromBytes(x)),
              Hex.fromBytes(generateBytes(1)),
            ],
          ],
        ])
      })

      test('hex -> bytes', () => {
        expect(Rlp.toBytes(Rlp.from(['0x00'], { as: 'Hex' }))).toEqual([
          Bytes.fromHex('0x00'),
        ])
        expect(Rlp.toBytes(Rlp.from(['0x80'], { as: 'Hex' }))).toEqual([
          Bytes.fromHex('0x80'),
        ])
        expect(Rlp.toBytes(Rlp.from(generateList(14), { as: 'Hex' }))).toEqual(
          generateList(14),
        )
        expect(
          Rlp.toBytes(
            Rlp.from(
              [
                generateList(4),
                [generateList(8), [generateList(3), generateBytes(1)]],
              ],
              { as: 'Hex' },
            ),
          ),
        ).toEqual([
          generateList(4),
          [generateList(8), [generateList(3), generateBytes(1)]],
        ])
      })
    })

    test('prefix === 0xf9', () => {
      expect(Rlp.toBytes(Rlp.fromBytes(generateList(61)))).toEqual(
        generateList(61),
      )
      expect(Rlp.toBytes(Rlp.fromBytes(generateList(12_000)))).toEqual(
        generateList(12_000),
      )
    })

    test('prefix === 0xfa', () => {
      expect(Rlp.toBytes(Rlp.fromBytes(generateList(60_000)))).toEqual(
        generateList(60_000),
      )
    })
  })

  test('error: invalid hex value', () => {
    expect(() => Rlp.toHex('0x010')).toThrowErrorMatchingInlineSnapshot(
      `
    [Hex.InvalidLengthError: Hex value \`"0x010"\` is an odd length (3 nibbles).

    It must be an even length.]
  `,
    )
  })
})

describe('decode depth limit', () => {
  // Builds an RLP-encoded value of `depth` nested empty lists without recursing
  // (so we can exceed the decoder's depth limit without overflowing the encoder).
  const nestedEmptyLists = (depth: number) => {
    let bytes = Uint8Array.from([0xc0])
    for (let i = 0; i < depth; i++) {
      const length = bytes.length
      const prefix =
        length <= 55
          ? [0xc0 + length]
          : (() => {
              const lengthBytes: number[] = []
              let n = length
              while (n > 0) {
                lengthBytes.unshift(n & 0xff)
                n >>= 8
              }
              return [0xf7 + lengthBytes.length, ...lengthBytes]
            })()
      const next = new Uint8Array(prefix.length + bytes.length)
      next.set(prefix, 0)
      next.set(bytes, prefix.length)
      bytes = next
    }
    return bytes
  }

  test('decodes up to the depth limit', () => {
    expect(() => Rlp.toBytes(nestedEmptyLists(1_023))).not.toThrow()
  })

  test('throws when the depth limit is exceeded', () => {
    expect(() =>
      Rlp.toBytes(nestedEmptyLists(2_000)),
    ).toThrowErrorMatchingInlineSnapshot(
      `[Rlp.DepthLimitExceededError: RLP depth limit of \`1024\` exceeded.]`,
    )
  })
})

describe('trailing bytes', () => {
  // RLP payloads encode exactly one item (Yellow Paper, Appendix B).
  test('after string item', () => {
    expect(() => Rlp.toHex('0x80deadbeef')).toThrowErrorMatchingInlineSnapshot(
      `[Rlp.TrailingBytesError: RLP payload encodes a single item, but \`4\` trailing bytes remain.]`,
    )
  })

  test('after list item', () => {
    expect(() => Rlp.toHex('0xc2010203')).toThrowErrorMatchingInlineSnapshot(
      `[Rlp.TrailingBytesError: RLP payload encodes a single item, but \`1\` trailing byte remains.]`,
    )
  })

  test('after empty list', () => {
    expect(() => Rlp.toHex('0xc000')).toThrowErrorMatchingInlineSnapshot(
      `[Rlp.TrailingBytesError: RLP payload encodes a single item, but \`1\` trailing byte remains.]`,
    )
  })

  test('bytes input', () => {
    expect(() =>
      Rlp.toHex(Uint8Array.from([0x80, 0xde, 0xad])),
    ).toThrowErrorMatchingInlineSnapshot(
      `[Rlp.TrailingBytesError: RLP payload encodes a single item, but \`2\` trailing bytes remain.]`,
    )
  })

  test('valid single items still decode', () => {
    expect(Rlp.toHex('0x80')).toEqual('0x')
    expect(Rlp.toHex('0xc0')).toEqual([])
    expect(Rlp.toHex('0xc1c0')).toEqual([[]])
    expect(Rlp.toHex('0x83646f67')).toEqual('0x646f67')
  })
})

describe('list boundary', () => {
  // Items must consume exactly the declared list length.
  test('item extends beyond list', () => {
    // `0xc1` declares 1 payload byte; `0x82aabb` consumes 3.
    expect(() => Rlp.toHex('0xc182aabb')).toThrowErrorMatchingInlineSnapshot(
      `[Rlp.ListBoundaryExceededError: RLP list items consumed \`3\` bytes but the list declared a length of \`1\`.]`,
    )
  })

  test('item extends beyond nested list', () => {
    expect(() => Rlp.toHex('0xc3c182aabb')).toThrowErrorMatchingInlineSnapshot(
      `[Rlp.ListBoundaryExceededError: RLP list items consumed \`3\` bytes but the list declared a length of \`1\`.]`,
    )
  })

  test('item extends beyond list with trailing bytes', () => {
    expect(() => Rlp.toHex('0xc182aabbff')).toThrowErrorMatchingInlineSnapshot(
      `[Rlp.ListBoundaryExceededError: RLP list items consumed \`3\` bytes but the list declared a length of \`1\`.]`,
    )
  })
})
