/* eslint-env mocha */

import * as chai from 'chai'

import { decode, encode, encodeInto } from '../cborg.js'
import { fromHex, toHex } from '../lib/byte-utils.js'

const { assert } = chai

// Helper to get encoded bytes from encodeInto (which returns { written })
const encodeIntoBytes = (data, dest) => {
  const { written } = encodeInto(data, dest)
  return dest.subarray(0, written)
}

// TODO: reject duplicate keys from encoded form

const fixtures = [
  { data: 'a0', expected: {}, type: 'map empty' },
  { data: 'a0', expected: new Map(), type: 'map empty (useMaps)', useMaps: true },
  { data: 'a1616101', expected: { a: 1 }, type: 'map 1 pair' },
  { data: 'a161316161', expected: { 1: 'a' }, type: 'map 1 pair (rev)' },
  {
    data: 'a1016161',
    expected: toMap([[1, 'a']]),
    type: 'map 1 pair (int key as Map w/ useMaps)',
    useMaps: true
  },
  {
    data: 'a243010203633132334302030463323334',
    expected: toMap([[Uint8Array.from([1, 2, 3]), '123'], [Uint8Array.from([2, 3, 4]), '234']]),
    type: 'map 2 pair (bytes keys Map w/ useMaps)',
    useMaps: true
  },
  {
    data: 'a1666f626a656374a16477697468a26134666e6573746564676f626a65637473a161216121',
    expected: { object: { with: { 4: 'nested', objects: { '!': '!' } } } },
    type: 'map nested'
  },
  { // same as previous but as a Map and an int key
    data: 'a1666f626a656374a16477697468a204666e6573746564676f626a65637473a161216121',
    expected: toMap([['object', toMap([['with', toMap([[4, 'nested'], ['objects', toMap([['!', '!']])]])]])]]),
    type: 'map nested w/ useMaps',
    useMaps: true
  },
  {
    data: 'ae636f6e651b0016db6db6db6db763736978206374656e3b0016db6db6db6db76374776f1a0001000064666976650064666f757202646e696e653aa5f702b365656967687438ff65736576656e226574687265651901f466656c6576656e426131667477656c76656fc48c6175657320c39f76c49b74652168666f75727465656ea4616664666f7572616f016174026274680368746869727465656e840203046466697665',
    encode: {
      one: Number.MAX_SAFE_INTEGER / 1.4,
      two: 65536,
      three: 500,
      four: 2,
      five: 0,
      six: -1,
      seven: -3,
      eight: -256,
      nine: -2784428724,
      ten: Number.MIN_SAFE_INTEGER / 1.4 - 1,
      eleven: new TextEncoder().encode('a1'),
      twelve: 'Čaues ßvěte!',
      thirteen: [2, 3, 4, 'five'],
      fourteen: { o: 1, t: 2, th: 3, f: 'four' }
    },
    expected: {
      one: Number.MAX_SAFE_INTEGER / 1.4,
      six: -1,
      ten: Number.MIN_SAFE_INTEGER / 1.4 - 1,
      two: 65536,
      five: 0,
      four: 2,
      nine: -2784428724,
      eight: -256,
      seven: -3,
      three: 500,
      eleven: new TextEncoder().encode('a1'),
      twelve: 'Čaues ßvěte!',
      fourteen: { f: 'four', o: 1, t: 2, th: 3 },
      thirteen: [2, 3, 4, 'five']
    },
    type: 'map with complex entries',
    label: '{}'
  },
  {
    data: 'ad01636f6e65026374776f1901f46c666976652068756e647265641902586b7369782068756e647265641a00010000636269671b0016db6db6db6db76662696767657220696d696e7573206f6e6521696d696e75732074776f38ff781f6d696e75732074776f2068756e6472656420616e64206669667479207369783901f4781a6d696e757820666976652068756e6472656420616e64206f6e653901f5781a6d696e757820666976652068756e6472656420616e642074776f3aa5f702b367626967206e65673b0016db6db6db6db76a626967676572206e6567',
    encode: toMap([
      [2, 'two'],
      [1, 'one'],
      [-2, 'minus two'],
      [-1, 'minus one'],
      [600, 'six hundred'],
      [500, 'five hundred'],
      [-256, 'minus two hundred and fifty six'],
      [-502, 'minux five hundred and two'],
      [-501, 'minux five hundred and one'],
      [65536, 'big'],
      [-2784428724, 'big neg'],
      [6433713753386423, 'bigger'],
      [-6433713753386424, 'bigger neg']
    ]),
    expected: toMap([
      [1, 'one'],
      [2, 'two'],
      [500, 'five hundred'],
      [600, 'six hundred'],
      [65536, 'big'],
      [6433713753386423, 'bigger'],
      [-1, 'minus one'],
      [-2, 'minus two'],
      [-256, 'minus two hundred and fifty six'],
      [-501, 'minux five hundred and one'],
      [-502, 'minux five hundred and two'],
      [-2784428724, 'big neg'],
      [-6433713753386424, 'bigger neg']
    ]),
    type: 'map with ints and negints',
    useMaps: true
  },
  {
    data: 'a44104636f6e65430102026374776f430102036574687265654301020464666f7572',
    encode: toMap([
      [Uint8Array.from([1, 2, 3]), 'three'],
      [Uint8Array.from([4]), 'one'],
      [Uint8Array.from([1, 2, 4]), 'four'],
      [Uint8Array.from([1, 2, 2]), 'two']
    ]),
    expected: toMap([
      [Uint8Array.from([4]), 'one'],
      [Uint8Array.from([1, 2, 2]), 'two'],
      [Uint8Array.from([1, 2, 3]), 'three'],
      [Uint8Array.from([1, 2, 4]), 'four']
    ]),
    type: 'map with bytes keys',
    useMaps: true
  },
  // testing lengths encoded as too-large ints
  { data: 'b801616101', expected: { a: 1 }, type: 'map 1 pair, length8', strict: false },
  { data: 'b90001616101', expected: { a: 1 }, type: 'map 1 pair, length16', strict: false },
  { data: 'ba00000001616101', expected: { a: 1 }, type: 'map 1 pair, length32', strict: false },
  { data: 'bb0000000000000001616101', expected: { a: 1 }, type: 'map 1 pair, length64', strict: false }
]

const fixedDest = new Uint8Array(1024)

function toMap (arr) {
  const m = new Map()
  for (const [key, value] of arr) {
    m.set(key, value)
  }
  return m
}

function entries (map) {
  function nest (a) {
    for (const e of a) {
      e[0] = entries(e[0])
      e[1] = entries(e[1])
    }
    return a
  }

  if (Object.getPrototypeOf(map) === Map.prototype) {
    return nest([...map.entries()])
  }
  if (typeof map === 'object') {
    return nest([...Object.entries(map)])
  }
  return map
}

describe('map', () => {
  describe('decode', () => {
    for (const fixture of fixtures) {
      const data = fromHex(fixture.data)
      it(`should decode ${fixture.type}=${fixture.label || JSON.stringify(fixture.expected)}`, () => {
        let options = fixture.useMaps ? { useMaps: true } : undefined
        const decoded = decode(data, options)

        if (fixture.useMaps) {
          assert.strictEqual(Object.getPrototypeOf(decoded), Map.prototype, 'is Map')
        } else {
          assert.isObject(decoded, 'is object')
        }

        assert.deepStrictEqual(entries(decoded), entries(fixture.expected), `decode ${fixture.type}`)

        options = Object.assign({ strict: true }, options)
        if (fixture.strict === false) {
          assert.throws(() => decode(data, options), Error, 'CBOR decode error: integer encoded in more bytes than necessary (strict decode)')
        } else {
          assert.deepStrictEqual(
            entries(decode(data, options)),
            entries(fixture.expected),
            `decode ${fixture.type}`)
        }
      })

      it('should fail to decode very large length', () => {
        assert.throws(
          () => decode(fromHex('bba5f702b3a5f70201616101')),
          /CBOR decode error: 64-bit integer map lengths not supported/)
      })
    }

    it('errors', () => {
      assert.throws(() => decode(fromHex('a1016161')), /non-string keys not supported \(got number\)/)
    })
  })

  describe('encode', () => {
    for (const fixture of fixtures) {
      it(`should encode ${fixture.type}=${fixture.label || JSON.stringify(fixture.expected)}`, () => {
        const toEncode = fixture.encode || fixture.expected
        if (fixture.unsafe) {
          assert.throws(encode.bind(null, toEncode), Error, /^CBOR encode error: number too large to encode \(\d+\)$/)
        } else if (fixture.strict === false || fixture.roundtrip === false) {
          assert.notDeepEqual(toHex(encodeIntoBytes(toEncode, fixedDest)), fixture.data, `encode ${fixture.type} !strict`)
        } else {
          assert.strictEqual(toHex(encodeIntoBytes(toEncode, fixedDest)), fixture.data, `encode ${fixture.type}`)
        }
      })
    }
  })

  // mostly unnecessary, but feels good
  describe('roundtrip', () => {
    for (const fixture of fixtures) {
      if (!fixture.unsafe && fixture.strict !== false && fixture.roundtrip !== false) {
        it(`should roundtrip ${fixture.type}=${fixture.label || JSON.stringify(fixture.expected)}`, () => {
          const toEncode = fixture.encode || fixture.expected
          const options = fixture.useMaps ? { useMaps: true } : undefined
          const rt = decode(encode(toEncode), options)

          if (fixture.useMaps) {
            assert.strictEqual(Object.getPrototypeOf(rt), Map.prototype, 'is Map')
          } else {
            assert.isObject(rt, 'is object')
          }

          assert.deepStrictEqual(entries(rt), entries(fixture.expected), `roundtrip ${fixture.type}`)
        })
      }
    }
  })

  describe('specials', () => {
    it('can decode indefinite length items', () => {
      assert.deepStrictEqual(decode(fromHex('bf616f01617402ff')), { o: 1, t: 2 })
    })

    it('can switch off indefinite length support', () => {
      assert.throws(() => decode(fromHex('bf616f01617402ff'), { allowIndefinite: false }), /indefinite/)
    })
  })

  describe('sorting', () => {
    it('sorts int map keys', () => {
      assert.strictEqual(toHex(encode(new Map([[1, 1], [2, 2]]))), 'a201010202')
      assert.strictEqual(toHex(encode(new Map([[2, 1], [1, 2]]))), 'a201020201')
    })

    it('sorts negint map keys', () => {
      assert.strictEqual(toHex(encode(new Map([[-1, 1], [-2, 2]]))), 'a220012102')
      assert.strictEqual(toHex(encode(new Map([[-2, 1], [-1, 2]]))), 'a220022101')
    })

    it('sorts bytes map keys', () => {
      assert.strictEqual(toHex(encode(new Map([[Uint8Array.from([1, 2]), 1], [Uint8Array.from([2, 1]), 2]]))), 'a24201020142020102')
      assert.strictEqual(toHex(encode(new Map([[Uint8Array.from([2, 1]), 1], [Uint8Array.from([1, 2]), 2]]))), 'a24201020242020101')
      // shortest first
      assert.strictEqual(toHex(encode(new Map([[Uint8Array.from([1, 2]), 1], [Uint8Array.from([2, 1]), 2], [Uint8Array.from([200]), 3]]))), 'a341c8034201020142020102')
    })

    it('sorts bytes map keys', () => {
      assert.strictEqual(toHex(encode(new Map([[Uint8Array.from([1, 2]), 1], [Uint8Array.from([2, 1]), 2]]))), 'a24201020142020102')
      assert.strictEqual(toHex(encode(new Map([[Uint8Array.from([2, 1]), 1], [Uint8Array.from([1, 2]), 2]]))), 'a24201020242020101')
      // shortest first
      assert.strictEqual(toHex(encode(new Map([[Uint8Array.from([1, 2]), 1], [Uint8Array.from([2, 1]), 2], [Uint8Array.from([200]), 3]]))), 'a341c8034201020142020102')
    })

    it('sorts array map keys (length only)', () => {
      assert.strictEqual(toHex(encode(new Map([[[1], 1], [[1, 1], 2]]))), 'a281010182010102')
      assert.strictEqual(toHex(encode(new Map([[[1, 1], 1], [[1], 2]]))), 'a281010282010101')
    })

    it('sorts map map keys (length only)', () => {
      assert.strictEqual(toHex(encode(new Map([[{ a: 1 }, 1], [{ a: 1, b: 1 }, 2]]))), 'a2a161610101a261610161620102')
      assert.strictEqual(toHex(encode(new Map([[{ a: 1, b: 1 }, 1], [{ a: 1 }, 2]]))), 'a2a161610102a261610161620101')
    })

    it('sorts simple value map keys', () => {
      // false (0xf4), true (0xf5), null (0xf6), undefined (0xf7)
      assert.strictEqual(toHex(encode(new Map([[true, 1], [false, 2]]))), 'a2f402f501')
      assert.strictEqual(toHex(encode(new Map([[false, 2], [true, 1]]))), 'a2f402f501')
      assert.strictEqual(toHex(encode(new Map([[undefined, 1], [null, 2], [true, 3], [false, 4]]))), 'a4f404f503f602f701')
      assert.strictEqual(toHex(encode(new Map([[false, 4], [true, 3], [null, 2], [undefined, 1]]))), 'a4f404f503f602f701')
    })

    it('sorts float and simple value map keys', () => {
      // simple values sort before floats
      assert.strictEqual(toHex(encode(new Map([[1.5, 'a'], [true, 'b']]))), 'a2f56162f93e006161')
      assert.strictEqual(toHex(encode(new Map([[true, 'b'], [1.5, 'a']]))), 'a2f56162f93e006161')
    })

    it('sorts float map keys by encoded bytes, not numeric value', () => {
      // +1.5 (f93e00) sorts before -1.5 (f9be00) even though -1.5 is the smaller
      // number: RFC 7049 3.9 orders by the encoded bytes, not the value
      assert.strictEqual(toHex(encode(new Map([[-1.5, 'a'], [1.5, 'b']]))), 'a2f93e006162f9be006161')
      assert.strictEqual(toHex(encode(new Map([[1.5, 'b'], [-1.5, 'a']]))), 'a2f93e006162f9be006161')
    })

    it('sorts float map keys of different widths shortest-first', () => {
      // Infinity is a 3-byte float16 (f97c00), 1.1 a 9-byte float64
      // (fb3ff199999999999a): shorter encoding sorts first regardless of value
      assert.strictEqual(toHex(encode(new Map([[1.1, 'a'], [Infinity, 'b']]))), 'a2f97c006162fb3ff199999999999a6161')
      assert.strictEqual(toHex(encode(new Map([[Infinity, 'b'], [1.1, 'a']]))), 'a2f97c006162fb3ff199999999999a6161')
    })

    it('sorts NaN float map keys deterministically', () => {
      // NaN compares false against every number, so ordering must not fall back
      // to insertion order; 1.5 (f93e00) sorts before NaN (f97e00) by bytes
      assert.strictEqual(toHex(encode(new Map([[NaN, 'a'], [1.5, 'b']]))), 'a2f93e006162f97e006161')
      assert.strictEqual(toHex(encode(new Map([[1.5, 'b'], [NaN, 'a']]))), 'a2f93e006162f97e006161')
    })

    it('sorts float map keys honouring the float64 option', () => {
      // default: 1.5 is a 3-byte float16, sorts before the 9-byte 1.1
      assert.strictEqual(toHex(encode(new Map([[1.5, 'b'], [1.1, 'a']]))), 'a2f93e006162fb3ff199999999999a6161')
      // float64: both encode to 9 bytes, so bytewise order applies and 1.1
      // (fb3ff1…) now sorts before 1.5 (fb3ff8…) — the sort follows the bytes
      // that are actually written
      assert.strictEqual(toHex(encode(new Map([[1.5, 'b'], [1.1, 'a']]), { float64: true })), 'a2fb3ff199999999999a6161fb3ff80000000000006162')
      assert.strictEqual(toHex(encode(new Map([[1.1, 'a'], [1.5, 'b']]), { float64: true })), 'a2fb3ff199999999999a6161fb3ff80000000000006162')
    })

    // TODO: tag keys .. but why would you do this!?
  })
})
