/* eslint-env mocha,es2020 */

import * as chai from 'chai'

import { decode, encode, encodeInto } from '../cborg.js'
import * as taglib from 'cborg/taglib'
import { fromHex, toHex } from '../lib/byte-utils.js'
// fixtures from https://github.com/cbor/test-vectors
import { fixtures } from './appendix_a.js'

const { assert } = chai

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

const fixedDest = new Uint8Array(1024)

const tags = []
const typeEncoders = {}

tags[0] = function (decode) {
  const obj = decode()
  if (typeof obj !== 'string') {
    throw new Error('expected string for tag 1')
  }
  return `0("${new Date(obj).toISOString().replace(/\.000Z$/, 'Z')}")`
}

tags[1] = function (decode) {
  const obj = decode()
  if (typeof obj !== 'number') {
    throw new Error('expected number for tag 1')
  }
  return `1(${obj})`
}

tags[2] = taglib.bigIntDecoder
typeEncoders.bigint = taglib.bigIntEncoder
tags[3] = taglib.bigNegIntDecoder

tags[23] = function (decode) {
// expected conversion to base16
  const obj = decode()
  if (!(obj instanceof Uint8Array)) {
    throw new Error('expected byte array for tag 23')
  }
  return `23(h'${toHex(obj)}')`
}

tags[24] = function (decode) { // embedded cbor, oh my
  const obj = decode()
  if (!(obj instanceof Uint8Array)) {
    throw new Error('expected byte array for tag 24')
  }
  return `24(h'${toHex(obj)}')`
}

tags[32] = function (decode) { // url
  const obj = decode()
  if (typeof obj !== 'string') {
    throw new Error('expected string for tag 32')
  }
  ;(() => new URL(obj))() // will throw if not a url
  return `32("${obj}")`
}

describe('cbor/test-vectors', () => {
  let i = 0
  for (const fixture of fixtures) {
    const u8a = fromHex(fixture.hex)
    let expected = fixture.decoded !== undefined ? fixture.decoded : fixture.diagnostic

    if (typeof expected === 'string' && expected.startsWith('h\'')) {
      expected = fromHex(expected.replace(/(^h)'|('$)/g, ''))
    }

    it(`test vector #${i}: ${inspect(expected).replace(/\n\s*/g, '')}`, () => {
      if (fixture.error) {
        assert.throws(() => decode(u8a, { tags }), fixture.error)
      } else {
        if (fixture.noTagDecodeError) {
          assert.throws(() => decode(u8a), fixture.noTagDecodeError)
        }
        let actual = decode(u8a, { tags })
        if (typeof actual === 'bigint') {
          actual = inspect(actual)
        }
        if (typeof expected === 'bigint') {
          expected = inspect(expected)
        }
        assert.deepEqual(actual, expected)

        if (fixture.roundtrip) {
          if (fixture.noTagEncodeError) {
            assert.throws(() => encode(decode(u8a, { tags })), fixture.noTagEncodeError)
            assert.throws(() => encodeInto(decode(u8a, { tags }), fixedDest), fixture.noTagEncodeError)
          }
          let reencoded = encode(decode(u8a, { tags }), { typeEncoders })
          assert.equal(toHex(reencoded), fixture.hex)
          reencoded = encodeIntoBytes(decode(u8a, { tags }), fixedDest, { typeEncoders })
          assert.equal(toHex(reencoded), fixture.hex)
        }
      }
    })
    i++
  }

  it.skip('encode w/ tags', () => {
  })
})

function inspect (o) {
  if (typeof o === 'string') {
    return `'${o}'`
  }
  if (o instanceof Uint8Array) {
    return `Uint8Array<${o.join(',')}>`
  }
  if (o == null || typeof o !== 'object') {
    return String(o)
  }
  return JSON.stringify(o)
}
