import { Bytes, Cbor, Hash, Hex, PublicKey } from 'ox'
import { describe, expect, test } from 'vp/test'

describe('encode', () => {
  test.each([
    ['0', [0], 0],
    ['1', [1], 1],
    ['10', [10], 10],
    ['23', [23], 23],
    ['24', [24, 24], 24],
    ['25', [24, 25], 25],
    ['100', [24, 100], 100],
    ['1000', [25, 3, 232], 1000],
    ['1000000', [26, 0, 15, 66, 64], 1000000],
    ['-1', [32], -1],
    ['-10', [41], -10],
    ['-24', [55], -24],
    ['-25', [56, 24], -25],
    ['-26', [56, 25], -26],
    ['-100', [56, 99], -100],
    ['-1000', [57, 3, 231], -1000],
    ['-1000000', [58, 0, 15, 66, 63], -1000000],
    ["''", [96], ''],
    ["'a'", [97, 97], 'a'],
    ["'IETF'", [100, 73, 69, 84, 70], 'IETF'],
    [`'"\\'`, [98, 34, 92], `"\\`],
    ["'\u00fc'", [98, 195, 188], '\u00fc'],
    ["'\u6c34'", [99, 230, 176, 180], '\u6c34'],
    ["'\ud800\udd51'", [100, 240, 144, 133, 145], '\ud800\udd51'],
    ['[]', [128], []],
    [
      "['a', {'b': 'c'}]",
      [130, 97, 97, 161, 97, 98, 97, 99],
      ['a', { b: 'c' }],
    ],
    ['[1,2,3]', [131, 1, 2, 3], [1, 2, 3]],
    [
      '[1, [2, 3], [4, 5]]',
      [131, 1, 130, 2, 3, 130, 4, 5],
      [1, [2, 3], [4, 5]],
    ],
    [
      '[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25]',
      [
        152, 25, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18,
        19, 20, 21, 22, 23, 24, 24, 24, 25,
      ],
      [
        1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
        21, 22, 23, 24, 25,
      ],
    ],
    ['{}', [160], {}],
    [
      "{'a': 1, 'b': [2, 3]}",
      [162, 97, 97, 1, 97, 98, 130, 2, 3],
      { a: 1, b: [2, 3] },
    ],
    [
      '25-length object',
      [
        184, 26, 97, 48, 97, 97, 97, 49, 97, 97, 97, 50, 97, 97, 97, 51, 97, 97,
        97, 52, 97, 97, 97, 53, 97, 97, 97, 54, 97, 97, 97, 55, 97, 97, 97, 56,
        97, 97, 97, 57, 97, 97, 98, 49, 48, 97, 97, 98, 49, 49, 97, 97, 98, 49,
        50, 97, 97, 98, 49, 51, 97, 97, 98, 49, 52, 97, 97, 98, 49, 53, 97, 97,
        98, 49, 54, 97, 97, 98, 49, 55, 97, 97, 98, 49, 56, 97, 97, 98, 49, 57,
        97, 97, 98, 50, 48, 97, 97, 98, 50, 49, 97, 97, 98, 50, 50, 97, 97, 98,
        50, 51, 97, 97, 98, 50, 52, 97, 97, 98, 50, 53, 97, 97,
      ],
      {
        '0': 'a',
        '1': 'a',
        '2': 'a',
        '3': 'a',
        '4': 'a',
        '5': 'a',
        '6': 'a',
        '7': 'a',
        '8': 'a',
        '9': 'a',
        '10': 'a',
        '11': 'a',
        '12': 'a',
        '13': 'a',
        '14': 'a',
        '15': 'a',
        '16': 'a',
        '17': 'a',
        '18': 'a',
        '19': 'a',
        '20': 'a',
        '21': 'a',
        '22': 'a',
        '23': 'a',
        '24': 'a',
        '25': 'a',
      },
    ],
    [
      "{'a': 'A', 'b': 'B', 'c': 'C', 'd': 'D', 'e': 'E'}",
      [
        165, 97, 97, 97, 65, 97, 98, 97, 66, 97, 99, 97, 67, 97, 100, 97, 68,
        97, 101, 97, 69,
      ],
      { a: 'A', b: 'B', c: 'C', d: 'D', e: 'E' },
    ],
    [
      "{ 'Fun': true, 'Amt': -2}",
      [162, 99, 70, 117, 110, 245, 99, 65, 109, 116, 33],
      { Fun: true, Amt: -2 },
    ],
    ['false', [244], false],
    ['true', [245], true],
    ['null', [246], null],
    ['undefined', [247], undefined],
    ['+Infinity', [250, 127, 128, 0, 0], Infinity],
    ['NaN', [250, 127, 192, 0, 0], NaN],
    ['-Infinity', [250, 255, 128, 0, 0], -Infinity],
    ['0.5', [250, 63, 0, 0, 0], 0.5],
    ['9007199254740994', [251, 67, 64, 0, 0, 0, 0, 0, 1], 9007199254740994],
    ['1.0e+300', [251, 126, 55, 228, 60, 136, 0, 117, 156], 1e300],
    ['-9007199254740994', [251, 195, 64, 0, 0, 0, 0, 0, 1], -9007199254740994],
    ["h''", [0x40], new Uint8Array()],
    ["h'01020304'", [68, 1, 2, 3, 4], new Uint8Array([1, 2, 3, 4])],
  ])('encode: %s', (_, output, input) => {
    expect(Cbor.encode(input)).toStrictEqual(Hex.from(output))
    expect(Cbor.encode(input, { as: 'Bytes' })).toStrictEqual(
      Bytes.from(output),
    )
  })

  test('large string (> 0xffff)', () => {
    const str = 'a'.repeat(0x10000)
    const encoded = Cbor.encode(str)
    const decoded = Cbor.decode(encoded)
    expect(decoded).toBe(str)
  })

  test('large array (> 0xff)', () => {
    const arr = Array.from({ length: 256 }, (_, i) => i)
    const encoded = Cbor.encode(arr)
    const decoded = Cbor.decode(encoded)
    expect(decoded).toStrictEqual(arr)
  })

  test('large array (> 0xffff)', () => {
    const arr = Array.from({ length: 0x10000 }, (_, i) => i % 256)
    const encoded = Cbor.encode(arr)
    const decoded = Cbor.decode(encoded)
    expect(decoded).toStrictEqual(arr)
  })

  test('large object (> 0xff)', () => {
    const obj: Record<string, number> = {}
    for (let i = 0; i < 256; i++) {
      obj[`key${i}`] = i
    }
    const encoded = Cbor.encode(obj)
    const decoded = Cbor.decode(encoded)
    expect(decoded).toStrictEqual(obj)
  })

  test('large byte string (> 0xff)', () => {
    const bytes = new Uint8Array(256).fill(42)
    const encoded = Cbor.encode(bytes)
    const decoded = Cbor.decode(encoded)
    expect(decoded).toStrictEqual(bytes)
  })

  test('large byte string (> 0xffff)', () => {
    const bytes = new Uint8Array(0x10000).fill(42)
    const encoded = Cbor.encode(bytes)
    const decoded = Cbor.decode(encoded)
    expect(decoded).toStrictEqual(bytes)
  })

  test('ArrayBuffer encoding', () => {
    const buffer = new ArrayBuffer(4)
    const view = new Uint8Array(buffer)
    view.set([1, 2, 3, 4])
    const encoded = Cbor.encode(buffer)
    const decoded = Cbor.decode(encoded)
    expect(decoded).toStrictEqual(new Uint8Array([1, 2, 3, 4]))
  })

  test('Uint16Array encoding', () => {
    const arr = new Uint16Array([1, 2, 3, 4])
    const encoded = Cbor.encode(arr)
    const decoded = Cbor.decode(encoded)
    expect(decoded).toStrictEqual(new Uint8Array([1, 0, 2, 0, 3, 0, 4, 0]))
  })

  test('Int8Array encoding', () => {
    const arr = new Int8Array([-1, -2, 3, 4])
    const encoded = Cbor.encode(arr)
    const decoded = Cbor.decode(encoded)
    expect(decoded).toStrictEqual(new Uint8Array([255, 254, 3, 4]))
  })

  test('Float32Array encoding', () => {
    const arr = new Float32Array([1.5, 2.5])
    const encoded = Cbor.encode(arr)
    const decoded = Cbor.decode(encoded) as Uint8Array
    expect(decoded).toBeInstanceOf(Uint8Array)
    expect(decoded.length).toBe(8)
  })

  test('DataView encoding', () => {
    const buffer = new ArrayBuffer(2)
    const view = new DataView(buffer)
    view.setUint8(0, 1)
    view.setUint8(1, 2)
    const encoded = Cbor.encode(view)
    const decoded = Cbor.decode(encoded)
    expect(decoded).toStrictEqual(new Uint8Array([1, 2]))
  })

  test('number too large throws', () => {
    const num = 0xfffffffffffff
    expect(() => Cbor.encode(num)).toThrow(Cbor.NumberTooLargeError)
  })

  test('negative number too large throws', () => {
    const num = -0xfffffffffffff
    expect(() => Cbor.encode(num)).toThrow(Cbor.NumberTooLargeError)
  })

  test('bigint throws', () => {
    expect(() => Cbor.encode(BigInt(42))).toThrow(Cbor.UnsupportedBigIntError)
  })

  test('unsupported type throws', () => {
    const symbol = Symbol('test')
    expect(() => Cbor.encode(symbol)).toThrow(Cbor.UnexpectedTokenError)
  })

  test('float64 encoding when value !== float32', () => {
    // A value that can't be represented exactly as float32
    const value = 1e100
    const encoded = Cbor.encode(value)
    const decoded = Cbor.decode(encoded)
    expect(decoded).toBe(value)
  })

  test('Map with integer keys', () => {
    const map = new Map<number, unknown>([
      [1, 2],
      [3, -7],
    ])
    const encoded = Cbor.encode(map)
    // keys should be CBOR integers, not text strings
    // {1: 2, 3: -7} = a2 01 02 03 26
    expect(encoded).toBe(Hex.from([0xa2, 0x01, 0x02, 0x03, 0x26]))
  })

  test('Map with negative integer keys', () => {
    const map = new Map<number, unknown>([
      [-1, 1],
      [-2, new Uint8Array([0xaa, 0xbb])],
    ])
    const encoded = Cbor.encode(map)
    // -1 = 0x20, -2 = 0x21
    const decoded = Cbor.decode<Record<string, unknown>>(encoded)
    expect(decoded).toStrictEqual({
      '-1': 1,
      '-2': new Uint8Array([0xaa, 0xbb]),
    })
  })

  test('Map with mixed key types', () => {
    const map = new Map<number | string, unknown>([
      [1, 'hello'],
      ['foo', 42],
    ])
    const encoded = Cbor.encode(map)
    const decoded = Cbor.decode<Record<string, unknown>>(encoded)
    expect(decoded).toStrictEqual({ '1': 'hello', foo: 42 })
  })

  test('Map roundtrip for COSE_Key structure', () => {
    const x = new Uint8Array(32).fill(0xaa)
    const y = new Uint8Array(32).fill(0xbb)
    const coseKey = new Map<number, unknown>([
      [1, 2],
      [3, -7],
      [-1, 1],
      [-2, x],
      [-3, y],
    ])
    const encoded = Cbor.encode(coseKey)
    const decoded = Cbor.decode<Record<string, unknown>>(encoded)
    expect(decoded['1']).toBe(2)
    expect(decoded['3']).toBe(-7)
    expect(decoded['-1']).toBe(1)
    expect(decoded['-2']).toStrictEqual(x)
    expect(decoded['-3']).toStrictEqual(y)
  })
})

describe('decode', () => {
  test.each([
    ['0', [0], 0],
    ['1', [1], 1],
    ['1', [1], 1],
    ['10', [10], 10],
    ['23', [23], 23],
    ['24', [24, 24], 24],
    ['25', [24, 25], 25],
    ['100', [24, 100], 100],
    ['1000', [25, 3, 232], 1000],
    ['1000000', [26, 0, 15, 66, 64], 1000000],
    ['-1', [32], -1],
    ['-10', [41], -10],
    ['-24', [55], -24],
    ['-25', [56, 24], -25],
    ['-26', [56, 25], -26],
    ['-100', [56, 99], -100],
    ['-1000', [57, 3, 231], -1000],
    ['-1000000', [58, 0, 15, 66, 63], -1000000],
    ["''", [96], ''],
    ["'a'", [97, 97], 'a'],
    ["'IETF'", [100, 73, 69, 84, 70], 'IETF'],
    [`'"\\'`, [98, 34, 92], `"\\`],
    ["'\u00fc'", [98, 195, 188], '\u00fc'],
    ["'\u6c34'", [99, 230, 176, 180], '\u6c34'],
    ["'\ud800\udd51'", [100, 240, 144, 133, 145], '\ud800\udd51'],
    [
      "'strea'+'ming'",
      [127, 101, 115, 116, 114, 101, 97, 100, 109, 105, 110, 103, 255],
      'streaming',
    ],
    [
      '255-length string',
      [
        120, 255, 97, 98, 99, 100, 101, 97, 98, 99, 100, 101, 97, 98, 99, 100,
        101, 97, 98, 99, 100, 101, 97, 98, 99, 100, 101, 97, 98, 99, 100, 101,
        97, 98, 99, 100, 101, 97, 98, 99, 100, 101, 97, 98, 99, 100, 101, 97,
        98, 99, 100, 101, 97, 98, 99, 100, 101, 97, 98, 99, 100, 101, 97, 98,
        99, 100, 101, 97, 98, 99, 100, 101, 97, 98, 99, 100, 101, 97, 98, 99,
        100, 101, 97, 98, 99, 100, 101, 97, 98, 99, 100, 101, 97, 98, 99, 100,
        101, 97, 98, 99, 100, 101, 97, 98, 99, 100, 101, 97, 98, 99, 100, 101,
        97, 98, 99, 100, 101, 97, 98, 99, 100, 101, 97, 98, 99, 100, 101, 97,
        98, 99, 100, 101, 97, 98, 99, 100, 101, 97, 98, 99, 100, 101, 97, 98,
        99, 100, 101, 97, 98, 99, 100, 101, 97, 98, 99, 100, 101, 97, 98, 99,
        100, 101, 97, 98, 99, 100, 101, 97, 98, 99, 100, 101, 97, 98, 99, 100,
        101, 97, 98, 99, 100, 101, 97, 98, 99, 100, 101, 97, 98, 99, 100, 101,
        97, 98, 99, 100, 101, 97, 98, 99, 100, 101, 97, 98, 99, 100, 101, 97,
        98, 99, 100, 101, 97, 98, 99, 100, 101, 97, 98, 99, 100, 101, 97, 98,
        99, 100, 101, 97, 98, 99, 100, 101, 97, 98, 99, 100, 101, 97, 98, 99,
        100, 101, 97, 98, 99, 100, 101, 97, 98, 99, 100, 101, 97, 98, 99, 100,
        101,
      ],
      'abcdeabcdeabcdeabcdeabcdeabcdeabcdeabcdeabcdeabcdeabcdeabcdeabcdeabcdeabcdeabcdeabcdeabcdeabcdeabcdeabcdeabcdeabcdeabcdeabcdeabcdeabcdeabcdeabcdeabcdeabcdeabcdeabcdeabcdeabcdeabcdeabcdeabcdeabcdeabcdeabcdeabcdeabcdeabcdeabcdeabcdeabcdeabcdeabcdeabcdeabcde',
    ],
    ['[]', [128], []],
    [
      "['a', {'b': 'c'}]",
      [130, 97, 97, 161, 97, 98, 97, 99],
      ['a', { b: 'c' }],
    ],
    [
      "['a, {_ 'b': 'c'}]",
      [130, 97, 97, 191, 97, 98, 97, 99, 255],
      ['a', { b: 'c' }],
    ],
    ['[1,2,3]', [131, 1, 2, 3], [1, 2, 3]],
    [
      '[1, [2, 3], [4, 5]]',
      [131, 1, 130, 2, 3, 130, 4, 5],
      [1, [2, 3], [4, 5]],
    ],
    [
      '[1, [2, 3], [_ 4, 5]]',
      [131, 1, 130, 2, 3, 159, 4, 5, 255],
      [1, [2, 3], [4, 5]],
    ],
    [
      '[1, [_ 2, 3], [4, 5]]',
      [131, 1, 159, 2, 3, 255, 130, 4, 5],
      [1, [2, 3], [4, 5]],
    ],
    [
      '[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25]',
      [
        152, 25, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18,
        19, 20, 21, 22, 23, 24, 24, 24, 25,
      ],
      [
        1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
        21, 22, 23, 24, 25,
      ],
    ],
    [
      '[_ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25]',
      [
        159, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19,
        20, 21, 22, 23, 24, 24, 24, 25, 255,
      ],
      [
        1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
        21, 22, 23, 24, 25,
      ],
    ],
    [
      '[_ 1, [2, 3], [4, 5]]',
      [159, 1, 130, 2, 3, 130, 4, 5, 255],
      [1, [2, 3], [4, 5]],
    ],
    [
      '[_ 1, [2, 3], [_ 4, 5]]',
      [159, 1, 130, 2, 3, 159, 4, 5, 255, 255],
      [1, [2, 3], [4, 5]],
    ],
    ['[_ ]', [159, 255], []],
    ['{}', [160], {}],
    [
      "{'a': 1, 'b': [2, 3]}",
      [162, 97, 97, 1, 97, 98, 130, 2, 3],
      { a: 1, b: [2, 3] },
    ],
    [
      '25-length object',
      [
        184, 26, 97, 48, 97, 97, 97, 49, 97, 97, 97, 50, 97, 97, 97, 51, 97, 97,
        97, 52, 97, 97, 97, 53, 97, 97, 97, 54, 97, 97, 97, 55, 97, 97, 97, 56,
        97, 97, 97, 57, 97, 97, 98, 49, 48, 97, 97, 98, 49, 49, 97, 97, 98, 49,
        50, 97, 97, 98, 49, 51, 97, 97, 98, 49, 52, 97, 97, 98, 49, 53, 97, 97,
        98, 49, 54, 97, 97, 98, 49, 55, 97, 97, 98, 49, 56, 97, 97, 98, 49, 57,
        97, 97, 98, 50, 48, 97, 97, 98, 50, 49, 97, 97, 98, 50, 50, 97, 97, 98,
        50, 51, 97, 97, 98, 50, 52, 97, 97, 98, 50, 53, 97, 97,
      ],
      {
        '0': 'a',
        '1': 'a',
        '2': 'a',
        '3': 'a',
        '4': 'a',
        '5': 'a',
        '6': 'a',
        '7': 'a',
        '8': 'a',
        '9': 'a',
        '10': 'a',
        '11': 'a',
        '12': 'a',
        '13': 'a',
        '14': 'a',
        '15': 'a',
        '16': 'a',
        '17': 'a',
        '18': 'a',
        '19': 'a',
        '20': 'a',
        '21': 'a',
        '22': 'a',
        '23': 'a',
        '24': 'a',
        '25': 'a',
      },
    ],
    [
      "{'a': 'A', 'b': 'B', 'c': 'C', 'd': 'D', 'e': 'E'}",
      [
        165, 97, 97, 97, 65, 97, 98, 97, 66, 97, 99, 97, 67, 97, 100, 97, 68,
        97, 101, 97, 69,
      ],
      { a: 'A', b: 'B', c: 'C', d: 'D', e: 'E' },
    ],
    [
      "{_ 'a': 1, 'b': [_ 2, 3]}",
      [191, 97, 97, 1, 97, 98, 159, 2, 3, 255, 255],
      { a: 1, b: [2, 3] },
    ],
    [
      "{_ 'Fun': true, 'Amt': -2}",
      [191, 99, 70, 117, 110, 245, 99, 65, 109, 116, 33, 255],
      { Fun: true, Amt: -2 },
    ],
    ['false', [244], false],
    ['true', [245], true],
    ['null', [246], null],
    ['undefined', [247], undefined],
    ['Simple value 255', [248, 255], undefined],
    ['+Infinity', [250, 127, 128, 0, 0], Infinity],
    ['NaN', [250, 127, 192, 0, 0], NaN],
    ['-Infinity', [250, 255, 128, 0, 0], -Infinity],
    ['0.5', [250, 63, 0, 0, 0], 0.5],
    ['9007199254740994', [251, 67, 64, 0, 0, 0, 0, 0, 1], 9007199254740994],
    ['1.0e+300', [251, 126, 55, 228, 60, 136, 0, 117, 156], 1e300],
    ['-9007199254740994', [251, 195, 64, 0, 0, 0, 0, 0, 1], -9007199254740994],
    ["h''", [0x40], new Uint8Array()],
    ["h'01020304'", [68, 1, 2, 3, 4], new Uint8Array([1, 2, 3, 4])],
    [
      "(_ h'0102', h'030405')",
      [95, 66, 1, 2, 67, 3, 4, 5, 255],
      new Uint8Array([1, 2, 3, 4, 5]),
    ],
  ])('decode: %s', (_, input, output) => {
    const decoded = Cbor.decode(Hex.from(input))
    expect(decoded).toStrictEqual(output)
  })

  test('from Bytes', () => {
    const bytes = Bytes.from([0x18, 0x2a])
    const decoded = Cbor.decode(bytes)
    expect(decoded).toBe(42)
  })

  test('invalid hex length throws', () => {
    expect(() => Cbor.decode('0x123')).toThrow()
  })

  test('large string (> 0xffff)', () => {
    const str = 'a'.repeat(0x10000)
    const encoded = Cbor.encode(str)
    const decoded = Cbor.decode(encoded)
    expect(decoded).toBe(str)
  })

  test('large array (> 0xffff)', () => {
    const arr = Array.from({ length: 0x10000 }, (_, i) => i % 256)
    const encoded = Cbor.encode(arr)
    const decoded = Cbor.decode(encoded)
    expect(decoded).toStrictEqual(arr)
  })

  test('large byte string (> 0xffff)', () => {
    const bytes = new Uint8Array(0x10000).fill(42)
    const encoded = Cbor.encode(bytes)
    const decoded = Cbor.decode(encoded)
    expect(decoded).toStrictEqual(bytes)
  })

  test('float16: zero', () => {
    // Float16 zero: 0x0000
    const encoded = Hex.from([0xf9, 0x00, 0x00])
    const decoded = Cbor.decode(encoded)
    expect(decoded).toBe(0)
  })

  test('float16: negative zero', () => {
    // Float16 negative zero: 0x8000
    const encoded = Hex.from([0xf9, 0x80, 0x00])
    const decoded = Cbor.decode(encoded)
    expect(Object.is(decoded, -0)).toBe(true)
  })

  test('float16: infinity', () => {
    // Float16 infinity: 0x7c00
    const encoded = Hex.from([0xf9, 0x7c, 0x00])
    const decoded = Cbor.decode(encoded)
    expect(decoded).toBe(Infinity)
  })

  test('float16: negative infinity', () => {
    // Float16 negative infinity: 0xfc00
    const encoded = Hex.from([0xf9, 0xfc, 0x00])
    const decoded = Cbor.decode(encoded)
    expect(decoded).toBe(-Infinity)
  })

  test('float16: NaN', () => {
    // Float16 NaN: 0x7e00
    const encoded = Hex.from([0xf9, 0x7e, 0x00])
    const decoded = Cbor.decode(encoded)
    expect(Number.isNaN(decoded)).toBe(true)
  })

  test('float16: subnormal', () => {
    // Float16 smallest subnormal: 0x0001
    const encoded = Hex.from([0xf9, 0x00, 0x01])
    const decoded = Cbor.decode(encoded)
    // Smallest positive subnormal float16 value
    expect(decoded).toBeGreaterThan(0)
    expect(decoded).toBeLessThan(1e-7)
  })

  test('float16: normal number', () => {
    // Float16 1.0: 0x3c00
    const encoded = Hex.from([0xf9, 0x3c, 0x00])
    const decoded = Cbor.decode(encoded)
    expect(decoded).toBe(1)
  })

  test('invalid major type throws', () => {
    // Actually, let's create a truly invalid major type
    const invalid = new Uint8Array([0xe0]) // 11100000 = major type 7, but invalid additional info
    expect(() => Cbor.decode(invalid)).toThrow()
  })

  test('unsupported 64-bit integer throws', () => {
    // Additional info 27 = 64-bit integer
    const encoded = Hex.from([
      0x1b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    ])
    expect(() => Cbor.decode(encoded)).toThrow(
      Cbor.Unsupported64BitIntegerError,
    )
  })

  test('unsupported tag throws', () => {
    // Major type 6 = tag
    const encoded = Hex.from([0xc0, 0x01])
    expect(() => Cbor.decode(encoded)).toThrow(Cbor.UnsupportedTagError)
  })

  test('invalid additional info throws', () => {
    // Additional info 28-30 are invalid
    const encoded = Hex.from([0x1c]) // 00011100 = major type 0, additional info 28
    expect(() => Cbor.decode(encoded)).toThrow(Cbor.InvalidAdditionalInfoError)
  })

  test('map with integer key converts to string', () => {
    // Map with integer key (converted to string for COSE_Key compatibility)
    const encoded = Hex.from([0xa1, 0x01, 0x02]) // {1: 2}
    const decoded = Cbor.decode<Record<string, number>>(encoded)
    expect(decoded).toEqual({ '1': 2 })
  })

  test('invalid simple value throws', () => {
    // Simple value < 32 (reserved)
    const encoded = Hex.from([0xf8, 0x10]) // Simple value 16
    expect(() => Cbor.decode(encoded)).toThrow(Cbor.InvalidSimpleValueError)
  })

  test('invalid indefinite length byte string chunk throws', () => {
    // Indefinite-length byte string with non-byte-string chunk
    const encoded = Hex.from([0x5f, 0x61, 0x61, 0xff]) // (_ 'a')
    expect(() => Cbor.decode(encoded)).toThrow(
      Cbor.InvalidIndefiniteLengthChunkError,
    )
  })

  test('invalid indefinite length text string chunk throws', () => {
    // Indefinite-length text string with non-text-string chunk
    const encoded = Hex.from([0x7f, 0x40, 0xff]) // (_ h'')
    expect(() => Cbor.decode(encoded)).toThrow(
      Cbor.InvalidIndefiniteLengthChunkError,
    )
  })

  test('readLength: additional info < 24', () => {
    const encoded = Hex.from([0x15]) // 21 directly encoded
    const decoded = Cbor.decode(encoded)
    expect(decoded).toBe(21)
  })

  test('readLength: additional info 24 (uint8)', () => {
    const encoded = Hex.from([0x18, 0xff]) // 255
    const decoded = Cbor.decode(encoded)
    expect(decoded).toBe(255)
  })

  test('readLength: additional info 25 (uint16)', () => {
    const encoded = Hex.from([0x19, 0x01, 0x00]) // 256
    const decoded = Cbor.decode(encoded)
    expect(decoded).toBe(256)
  })

  test('readLength: additional info 26 (uint32)', () => {
    const encoded = Hex.from([0x1a, 0x00, 0x01, 0x00, 0x00]) // 65536
    const decoded = Cbor.decode(encoded)
    expect(decoded).toBe(65536)
  })
})

describe('roundtrip', () => {
  test.each([
    ['number: 0', 0],
    ['number: 42', 42],
    ['number: 1000', 1000],
    ['number: -1', -1],
    ['number: -100', -100],
    ['float: 0.5', 0.5],
    ['float: Infinity', Infinity],
    ['float: -Infinity', -Infinity],
    ['string: empty', ''],
    ['string: hello', 'hello'],
    ['string: unicode', '\u6c34'],
    ['boolean: true', true],
    ['boolean: false', false],
    ['null', null],
    ['undefined', undefined],
    ['array: empty', []],
    ['array: numbers', [1, 2, 3]],
    ['array: nested', [1, [2, 3], [4, 5]]],
    ['object: empty', {}],
    ['object: simple', { a: 1, b: 2 }],
    ['object: nested', { a: 1, b: [2, 3] }],
    ['complex', { a: 'hello', b: [1, 2, { c: true }], d: null }],
  ])('roundtrip: %s', (_, value) => {
    const encoded = Cbor.encode(value)
    const decoded = Cbor.decode(encoded)
    if (typeof value === 'number' && Number.isNaN(value)) {
      expect(Number.isNaN(decoded)).toBe(true)
    } else {
      expect(decoded).toStrictEqual(value)
    }
  })

  test('roundtrip: byte string', () => {
    const value = new Uint8Array([1, 2, 3, 4])
    const encoded = Cbor.encode(value)
    const decoded = Cbor.decode(encoded)
    expect(decoded).toStrictEqual(value)
  })
})

describe('webauthn', () => {
  const rpId = 'localhost'
  const attestationObject =
    '0xa363666d74646e6f6e656761747453746d74a0686175746844617461589849960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97635d00000000fbfc3007154e4ecc8c0b6e020557d7bd0014f55a2aef110f9c55f4343dc5e73ab1d291ae72cca5010203262001215820eee72c4fc66e2670b0f606fcb54ce453b8ae09d0661303dd4ad8d4063be212872258202093aff8b065c247c742e6833937ba987fb0f871e61fcfc6763fd9144611210f'
  const publicKey = PublicKey.from({
    x: '0xeee72c4fc66e2670b0f606fcb54ce453b8ae09d0661303dd4ad8d4063be21287',
    y: '0x2093aff8b065c247c742e6833937ba987fb0f871e61fcfc6763fd9144611210f',
  })

  test('behavior: verify authData', () => {
    const decoded = Cbor.decode<{ fmt: string; authData: Bytes.Bytes }>(
      attestationObject,
    )
    const authDataBytes = decoded.authData
    const authData = Bytes.toHex(authDataBytes)

    // Extract rpIdHash (bytes 0-31)
    const rpIdHash_expected = Hex.slice(authData, 0, 32)
    const rpIdHash_actual = Hash.sha256(Hex.fromString(rpId))
    expect(rpIdHash_actual).toBe(rpIdHash_expected)

    // Extract flags (byte 32)
    const flags = authDataBytes[32]!
    expect(flags & 0x01).toBe(0x01) // UP bit should be set
    expect(flags & 0x40).toBe(0x40) // AT bit should be set (attested credential data present)

    // Extract signCount (bytes 33-36)
    const signCount = authDataBytes.slice(33, 37)
    expect(signCount).toStrictEqual(Bytes.from([0, 0, 0, 0]))

    // Extract attested credential data (when AT bit is set)
    // Structure: aaguid (16 bytes) + credentialIdLength (2 bytes) + credentialId + credentialPublicKey
    const aaguid = authDataBytes.slice(37, 53)
    expect(aaguid.length).toBe(16)

    const credentialIdLength = (authDataBytes[53]! << 8) | authDataBytes[54]!
    const credentialId = authDataBytes.slice(55, 55 + credentialIdLength)
    expect(credentialId.length).toBe(credentialIdLength)

    // Extract credentialPublicKey (starts after credentialId)
    // The credentialPublicKey is CBOR-encoded as a COSE_Key map
    const credentialPublicKeyStart = 55 + credentialIdLength
    const credentialPublicKeyCbor = authDataBytes.slice(
      credentialPublicKeyStart,
    )
    const credentialPublicKey = Cbor.decode<{
      1: number // kty
      3: number // alg
      '-1': number // crv
      '-2': Bytes.Bytes // x
      '-3': Bytes.Bytes // y
    }>(credentialPublicKeyCbor)

    expect(credentialPublicKey['3']).toBe(-7)
    expect(credentialPublicKey['-1']).toBe(1)

    const x = Hex.fromBytes(credentialPublicKey['-2'], { size: 32 })
    const y = Hex.fromBytes(credentialPublicKey['-3'], { size: 32 })
    expect(PublicKey.from({ x, y })).toStrictEqual(publicKey)
  })
})
