import tape, { test } from 'tape'
import * as packet from './index.mjs'
import * as rcodes from './rcodes.mjs'
import * as opcodes from './opcodes.mjs'
import * as optioncodes from './optioncodes.mjs'
import { decode as toUtf8 } from 'utf8-codec'
import { write, toHex, bytelength, writeHex, hexLength } from './buffer_utils.mjs'

tape('unknown', function (t) {
  testEncoder(t, packet.unknown, Buffer.from('hello world'))
  t.end()
})

tape('txt', function (t) {
  testEncoder(t, packet.txt, [])
  testEncoder(t, packet.txt, ['hello world'])
  testEncoder(t, packet.txt, ['hello', 'world'])
  testEncoder(t, packet.txt, [Buffer.from([0, 1, 2, 3, 4, 5])])
  testEncoder(t, packet.txt, ['a', 'b', Buffer.from([0, 1, 2, 3, 4, 5])])
  testEncoder(t, packet.txt, ['', Buffer.allocUnsafe(0)])
  t.end()
})

tape('txt-scalar-string', function (t) {
  const buf = packet.txt.encode('hi')
  const val = packet.txt.decode(buf)
  t.equal(val.length, 1, 'array length')
  t.ok(compare(t, val[0], Buffer.from('hi')), 'streamDecoded type match')
  t.end()
})

tape('txt-scalar-buffer', function (t) {
  const data = Buffer.from([0, 1, 2, 3, 4, 5])
  const buf = packet.txt.encode(data)
  const val = packet.txt.decode(buf)
  t.equal(val.length, 1, 'array length')
  t.ok(compare(t, val[0], data), 'data')
  t.end()
})

tape('txt-invalid-data', function (t) {
  t.throws(function () { packet.txt.encode(null) }, 'null')
  t.throws(function () { packet.txt.encode(undefined) }, 'undefined')
  t.throws(function () { packet.txt.encode(10) }, 'number')
  t.end()
})

tape('null', function (t) {
  testEncoder(t, packet.null, Buffer.from([0, 1, 2, 3, 4, 5]))
  t.end()
})

tape('hinfo', function (t) {
  testEncoder(t, packet.hinfo, { cpu: 'intel', os: 'best one' })
  t.end()
})

tape('ptr', function (t) {
  testEncoder(t, packet.ptr, 'hello.world.com')
  t.end()
})

tape('cname', function (t) {
  testEncoder(t, packet.cname, 'hello.cname.world.com')
  t.end()
})

tape('dname', function (t) {
  testEncoder(t, packet.dname, 'hello.dname.world.com')
  t.end()
})

tape('srv', function (t) {
  testEncoder(t, packet.srv, { port: 9999, target: 'hello.world.com' })
  testEncoder(t, packet.srv, { port: 9999, target: 'hello.world.com', priority: 42, weight: 10 })
  t.end()
})

tape('caa', function (t) {
  testEncoder(t, packet.caa, { flags: 128, tag: 'issue', value: 'letsencrypt.org', issuerCritical: true })
  testEncoder(t, packet.caa, { tag: 'issue', value: 'letsencrypt.org', issuerCritical: true })
  testEncoder(t, packet.caa, { tag: 'issue', value: 'letsencrypt.org' })
  t.end()
})

tape('mx', function (t) {
  testEncoder(t, packet.mx, { preference: 10, exchange: 'mx.hello.world.com' })
  testEncoder(t, packet.mx, { exchange: 'mx.hello.world.com' })
  t.end()
})

tape('ns', function (t) {
  testEncoder(t, packet.ns, 'ns.world.com')
  t.end()
})

tape('soa', function (t) {
  testEncoder(t, packet.soa, {
    mname: 'hello.world.com',
    rname: 'root.hello.world.com',
    serial: 2018010400,
    refresh: 14400,
    retry: 3600,
    expire: 604800,
    minimum: 3600
  })
  t.end()
})

tape('sshfp', function (t) {
  testEncoder(t, packet.sshfp, {
    algorithm: 1,
    hash: 1,
    fingerprint: 'a108c9f834354d5b37af988141c9294822f5bc00'
  })
  testEncoder(t, packet.sshfp, {
    algorithm: 1,
    hash: 2,
    fingerprint: 'a108c9f834354d5b37af988141c9294822f5bc00afa0dfafa2dfa1dfafa5dfa1'
  })
  t.end()
})

tape('a', function (t) {
  testEncoder(t, packet.a, '127.0.0.1')
  t.end()
})

tape('aaaa', function (t) {
  testEncoder(t, packet.aaaa, 'fe80::1')
  t.end()
})

tape('query', function (t) {
  testEncoder(t, packet, {
    type: 'query',
    questions: [{
      type: 'A',
      name: 'hello.a.com'
    }, {
      type: 'SRV',
      name: 'hello.srv.com'
    }]
  })

  testEncoder(t, packet, {
    type: 'query',
    id: 42,
    questions: [{
      type: 'A',
      class: 'IN',
      name: 'hello.a.com'
    }, {
      type: 'SRV',
      name: 'hello.srv.com'
    }]
  })

  testEncoder(t, packet, {
    type: 'query',
    id: 42,
    questions: [{
      type: 'A',
      class: 'CH',
      name: 'hello.a.com'
    }, {
      type: 'SRV',
      name: 'hello.srv.com'
    }]
  })

  t.end()
})

tape('response', function (t) {
  testEncoder(t, packet, {
    type: 'response',
    answers: [{
      type: 'A',
      class: 'IN',
      flush: true,
      name: 'hello.a.com',
      data: '127.0.0.1'
    }]
  })

  testEncoder(t, packet, {
    type: 'response',
    flags: packet.TRUNCATED_RESPONSE,
    answers: [{
      type: 'A',
      class: 'IN',
      name: 'hello.a.com',
      data: '127.0.0.1'
    }, {
      type: 'SRV',
      class: 'IN',
      name: 'hello.srv.com',
      data: {
        port: 9090,
        target: 'hello.target.com'
      }
    }, {
      type: 'CNAME',
      class: 'IN',
      name: 'hello.cname.com',
      data: 'hello.other.domain.com'
    }]
  })

  testEncoder(t, packet, {
    type: 'response',
    id: 100,
    flags: 0,
    additionals: [{
      type: 'AAAA',
      name: 'hello.a.com',
      data: 'fe80::1'
    }, {
      type: 'PTR',
      name: 'hello.ptr.com',
      data: 'hello.other.ptr.com'
    }, {
      type: 'SRV',
      name: 'hello.srv.com',
      ttl: 42,
      data: {
        port: 9090,
        target: 'hello.target.com'
      }
    }],
    answers: [{
      type: 'NULL',
      name: 'hello.null.com',
      data: Buffer.from([1, 2, 3, 4, 5])
    }]
  })

  testEncoder(t, packet, {
    type: 'response',
    answers: [{
      type: 'TXT',
      name: 'emptytxt.com',
      data: ''
    }]
  })

  t.end()
})

tape('rcode', function (t) {
  const errors = ['NOERROR', 'FORMERR', 'SERVFAIL', 'NXDOMAIN', 'NOTIMP', 'REFUSED', 'YXDOMAIN', 'YXRRSET', 'NXRRSET', 'NOTAUTH', 'NOTZONE', 'RCODE_11', 'RCODE_12', 'RCODE_13', 'RCODE_14', 'RCODE_15']
  for (const i in errors) {
    const code = rcodes.toRcode(errors[i])
    t.ok(errors[i] === rcodes.toString(code), 'rcode conversion from/to string matches: ' + rcodes.toString(code))
  }

  const ops = ['QUERY', 'IQUERY', 'STATUS', 'OPCODE_3', 'NOTIFY', 'UPDATE', 'OPCODE_6', 'OPCODE_7', 'OPCODE_8', 'OPCODE_9', 'OPCODE_10', 'OPCODE_11', 'OPCODE_12', 'OPCODE_13', 'OPCODE_14', 'OPCODE_15']
  for (const j in ops) {
    const ocode = opcodes.toOpcode(ops[j])
    t.ok(ops[j] === opcodes.toString(ocode), 'opcode conversion from/to string matches: ' + opcodes.toString(ocode))
  }

  const buf = packet.encode({
    type: 'response',
    id: 45632,
    flags: 0x8480,
    answers: [{
      type: 'A',
      name: 'hello.example.net',
      data: '127.0.0.1'
    }]
  })
  const val = packet.decode(buf)
  t.ok(val.type === 'response', 'decode type')
  t.ok(val.opcode === 'QUERY', 'decode opcode')
  t.ok(val.flag_qr === true, 'decode flag_qr')
  t.ok(val.flag_aa === true, 'decode flag_aa')
  t.ok(val.flag_tc === false, 'decode flag_tc')
  t.ok(val.flag_rd === false, 'decode flag_rd')
  t.ok(val.flag_ra === true, 'decode flag_ra')
  t.ok(val.flag_z === false, 'decode flag_z')
  t.ok(val.flag_ad === false, 'decode flag_ad')
  t.ok(val.flag_cd === false, 'decode flag_cd')
  t.ok(val.rcode === 'NOERROR', 'decode rcode')
  t.end()
})

tape('name_encoding', function (t) {
  let data = 'foo.example.com'
  const buf = Buffer.allocUnsafe(255)
  let offset = 0
  packet.name.encode(data, buf, offset)
  t.ok(packet.name.encode.bytes === 17, 'name encoding length matches')
  let dd = packet.name.decode(buf, offset)
  t.ok(data === dd, 'encode/decode matches')
  offset += packet.name.encode.bytes

  data = 'com'
  packet.name.encode(data, buf, offset)
  t.ok(packet.name.encode.bytes === 5, 'name encoding length matches')
  dd = packet.name.decode(buf, offset)
  t.ok(data === dd, 'encode/decode matches')
  offset += packet.name.encode.bytes

  data = 'example.com.'
  packet.name.encode(data, buf, offset)
  t.ok(packet.name.encode.bytes === 13, 'name encoding length matches')
  dd = packet.name.decode(buf, offset)
  t.ok(data.slice(0, -1) === dd, 'encode/decode matches')
  offset += packet.name.encode.bytes

  data = '.'
  packet.name.encode(data, buf, offset)
  t.ok(packet.name.encode.bytes === 1, 'name encoding length matches')
  dd = packet.name.decode(buf, offset)
  t.ok(data === dd, 'encode/decode matches')
  t.end()
})

tape('name_decoding', function (t) {
  // The two most significant bits of a valid label header must be either both zero or both one
  t.throws(function () { packet.name.decode(Buffer.from([0x80])) }, /Cannot decode name \(bad label\)$/)
  t.throws(function () { packet.name.decode(Buffer.from([0xb0])) }, /Cannot decode name \(bad label\)$/)

  // Ensure there's enough buffer to read
  t.throws(function () { packet.name.decode(Buffer.from([])) }, /Cannot decode name \(buffer overflow\)$/)
  t.throws(function () { packet.name.decode(Buffer.from([0x01, 0x00])) }, /Cannot decode name \(buffer overflow\)$/)
  t.throws(function () { packet.name.decode(Buffer.from([0x01])) }, /Cannot decode name \(buffer overflow\)$/)
  t.throws(function () { packet.name.decode(Buffer.from([0xc0])) }, /Cannot decode name \(buffer overflow\)$/)

  // Allow only pointers backwards
  t.throws(function () { packet.name.decode(Buffer.from([0xc0, 0x00])) }, /Cannot decode name \(bad pointer\)$/)
  t.throws(function () { packet.name.decode(Buffer.from([0xc0, 0x01])) }, /Cannot decode name \(bad pointer\)$/)

  // A name can be only 253 characters (when connected with dots)
  const maxLength = Buffer.alloc(255)
  maxLength.fill(Buffer.from([0x01, 0x61]), 0, 254)
  t.ok(packet.name.decode(maxLength) === new Array(127).fill('a').join('.'))

  const tooLong = Buffer.alloc(256)
  tooLong.fill(Buffer.from([0x01, 0x61]))
  t.throws(function () { packet.name.decode(tooLong) }, /Cannot decode name \(name too long\)$/)

  // Ensure jumps don't reset the total length counter
  const tooLongWithJump = Buffer.alloc(403)
  tooLongWithJump.fill(Buffer.from([0x01, 0x61]), 0, 200)
  tooLongWithJump.fill(Buffer.from([0x01, 0x61]), 201, 401)
  tooLongWithJump.set([0xc0, 0x00], 401)
  t.throws(function () { packet.name.decode(tooLongWithJump, 201) }, /Cannot decode name \(name too long\)$/)

  // Ensure a jump to a null byte doesn't add extra dots
  t.ok(packet.name.decode(Buffer.from([0x00, 0x01, 0x61, 0xc0, 0x00]), 1) === 'a')

  // Ensure deeply nested pointers don't cause "Maximum call stack size exceeded" errors
  const buf = Buffer.alloc(16386)
  for (let i = 0; i < 16384; i += 2) {
    buf.writeUInt16BE(0xc000 | i, i + 2)
  }
  t.ok(packet.name.decode(buf, 16384) === '.')

  t.end()
})

tape('stream', function (t) {
  const val = {
    type: 'query',
    id: 45632,
    flags: 0x8480,
    answers: [{
      type: 'A',
      name: 'test2.example.net',
      data: '198.51.100.1'
    }]
  }
  const buf = packet.streamEncode(val)
  const val2 = packet.streamDecode(buf)

  t.same(buf.length, packet.streamEncode.bytes, 'streamEncode.bytes was set correctly')
  t.ok(compare(t, val2.type, val.type), 'streamDecoded type match')
  t.ok(compare(t, val2.id, val.id), 'streamDecoded id match')
  t.ok(parseInt(val2.flags) === parseInt(val.flags & 0x7FFF), 'streamDecoded flags match')
  const answer = val.answers[0]
  const answer2 = val2.answers[0]
  t.ok(compare(t, answer.type, answer2.type), 'streamDecoded RR type match')
  t.ok(compare(t, answer.name, answer2.name), 'streamDecoded RR name match')
  t.ok(compare(t, answer.data, answer2.data), 'streamDecoded RR rdata match')
  t.end()
})

tape('opt', function (t) {
  const val = {
    type: 'query',
    questions: [{
      type: 'A',
      name: 'hello.a.com'
    }],
    additionals: [{
      type: 'OPT',
      name: '.',
      udpPayloadSize: 1024
    }]
  }
  testEncoder(t, packet, val)
  let buf = packet.encode(val)
  let val2 = packet.decode(buf)
  const additional1 = val.additionals[0]
  let additional2 = val2.additionals[0]
  t.ok(compare(t, additional1.name, additional2.name), 'name matches')
  t.ok(compare(t, additional1.udpPayloadSize, additional2.udpPayloadSize), 'udp payload size matches')
  t.ok(compare(t, 0, additional2.flags), 'flags match')
  additional1.flags = packet.DNSSEC_OK
  additional1.extendedRcode = 0x80
  additional1.options = [{
    code: 'CLIENT_SUBNET', // edns-client-subnet, see RFC 7871
    ip: 'fe80::',
    sourcePrefixLength: 64
  }, {
    code: 8, // still ECS
    ip: '5.6.0.0',
    sourcePrefixLength: 16,
    scopePrefixLength: 16
  }, {
    code: 'padding',
    length: 31
  }, {
    code: 'TCP_KEEPALIVE'
  }, {
    code: 'tcp_keepalive',
    timeout: 150
  }, {
    code: 'KEY_TAG',
    tags: [1, 82, 987]
  }]
  buf = packet.encode(val)
  val2 = packet.decode(buf)
  additional2 = val2.additionals[0]
  t.ok(compare(t, 1 << 15, additional2.flags), 'DO bit set in flags')
  t.ok(compare(t, true, additional2.flag_do), 'DO bit set')
  t.ok(compare(t, additional1.extendedRcode, additional2.extendedRcode), 'extended rcode matches')
  t.ok(compare(t, 8, additional2.options[0].code))
  t.ok(compare(t, 'fe80::', additional2.options[0].ip))
  t.ok(compare(t, 64, additional2.options[0].sourcePrefixLength))
  t.ok(compare(t, '5.6.0.0', additional2.options[1].ip))
  t.ok(compare(t, 16, additional2.options[1].sourcePrefixLength))
  t.ok(compare(t, 16, additional2.options[1].scopePrefixLength))
  t.ok(compare(t, additional1.options[2].length, additional2.options[2].data.length))
  t.ok(compare(t, additional1.options[3].timeout, undefined))
  t.ok(compare(t, additional1.options[4].timeout, additional2.options[4].timeout))
  t.ok(compare(t, additional1.options[5].tags, additional2.options[5].tags))
  t.end()
})

tape('dnskey', function (t) {
  testEncoder(t, packet.dnskey, {
    flags: packet.dnskey.SECURE_ENTRYPOINT | packet.dnskey.ZONE_KEY,
    algorithm: 1,
    key: Buffer.from([0, 1, 2, 3, 4, 5])
  })
  t.end()
})

tape('rrsig', function (t) {
  const testRRSIG = {
    typeCovered: 'A',
    algorithm: 1,
    labels: 2,
    originalTTL: 3600,
    expiration: 1234,
    inception: 1233,
    keyTag: 2345,
    signersName: 'foo.com',
    signature: Buffer.from([0, 1, 2, 3, 4, 5])
  }
  testEncoder(t, packet.rrsig, testRRSIG)

  // Check the signature length is correct with extra junk at the end
  const buf = Buffer.allocUnsafe(packet.rrsig.encodingLength(testRRSIG) + 4)
  packet.rrsig.encode(testRRSIG, buf)
  const val2 = packet.rrsig.decode(buf)
  t.ok(compare(t, testRRSIG, val2))

  t.end()
})

tape('rrp', function (t) {
  testEncoder(t, packet.rp, {
    mbox: 'foo.bar.com',
    txt: 'baz.bar.com'
  })
  testEncoder(t, packet.rp, {
    mbox: 'foo.bar.com'
  })
  testEncoder(t, packet.rp, {
    txt: 'baz.bar.com'
  })
  testEncoder(t, packet.rp, {})
  t.end()
})

tape('nsec', function (t) {
  testEncoder(t, packet.nsec, {
    nextDomain: 'foo.com',
    rrtypes: ['A', 'DNSKEY', 'CAA', 'DLV']
  })
  testEncoder(t, packet.nsec, {
    nextDomain: 'foo.com',
    rrtypes: ['TXT'] // 16
  })
  testEncoder(t, packet.nsec, {
    nextDomain: 'foo.com',
    rrtypes: ['TKEY'] // 249
  })
  testEncoder(t, packet.nsec, {
    nextDomain: 'foo.com',
    rrtypes: ['RRSIG', 'NSEC']
  })
  testEncoder(t, packet.nsec, {
    nextDomain: 'foo.com',
    rrtypes: ['TXT', 'RRSIG']
  })
  testEncoder(t, packet.nsec, {
    nextDomain: 'foo.com',
    rrtypes: ['TXT', 'NSEC']
  })

  // Test with the sample NSEC from https://tools.ietf.org/html/rfc4034#section-4.3
  const sampleNSEC = new Uint8Array(Buffer.from('003704686f7374076578616d706c6503636f6d00' +
      '0006400100000003041b000000000000000000000000000000000000000000000' +
      '000000020', 'hex'))
  const decoded = packet.nsec.decode(sampleNSEC)
  t.ok(compare(t, decoded, {
    nextDomain: 'host.example.com',
    rrtypes: ['A', 'MX', 'RRSIG', 'NSEC', 'UNKNOWN_1234']
  }))
  const reencoded = packet.nsec.encode(decoded)
  t.same(sampleNSEC.length, reencoded.length)
  t.same(sampleNSEC, reencoded)
  t.end()
})

tape('nsec3', function (t) {
  testEncoder(t, packet.nsec3, {
    algorithm: 1,
    flags: 0,
    iterations: 257,
    salt: Buffer.from([42, 42, 42]),
    nextDomain: Buffer.from([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]),
    rrtypes: ['A', 'DNSKEY', 'CAA', 'DLV']
  })
  t.end()
})

tape('ds', function (t) {
  testEncoder(t, packet.ds, {
    keyTag: 1234,
    algorithm: 1,
    digestType: 1,
    digest: Buffer.from([0, 1, 2, 3, 4, 5])
  })
  t.end()
})

tape('unpack', function (t) {
  const buf = Buffer.from([
    0x00, 0x79,
    0xde, 0xad, 0x85, 0x00, 0x00, 0x01, 0x00, 0x01,
    0x00, 0x02, 0x00, 0x02, 0x02, 0x6f, 0x6a, 0x05,
    0x62, 0x61, 0x6e, 0x67, 0x6a, 0x03, 0x63, 0x6f,
    0x6d, 0x00, 0x00, 0x01, 0x00, 0x01, 0xc0, 0x0c,
    0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x0e, 0x10,
    0x00, 0x04, 0x81, 0xfa, 0x0b, 0xaa, 0xc0, 0x0f,
    0x00, 0x02, 0x00, 0x01, 0x00, 0x00, 0x0e, 0x10,
    0x00, 0x05, 0x02, 0x63, 0x6a, 0xc0, 0x0f, 0xc0,
    0x0f, 0x00, 0x02, 0x00, 0x01, 0x00, 0x00, 0x0e,
    0x10, 0x00, 0x02, 0xc0, 0x0c, 0xc0, 0x3a, 0x00,
    0x01, 0x00, 0x01, 0x00, 0x00, 0x0e, 0x10, 0x00,
    0x04, 0x45, 0x4d, 0x9b, 0x9c, 0xc0, 0x0c, 0x00,
    0x1c, 0x00, 0x01, 0x00, 0x00, 0x0e, 0x10, 0x00,
    0x10, 0x20, 0x01, 0x04, 0x18, 0x00, 0x00, 0x50,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xf9
  ])
  const val = packet.streamDecode(buf)
  const answer = val.answers[0]
  const authority = val.authorities[1]
  t.ok(val.rcode === 'NOERROR', 'decode rcode')
  t.ok(compare(t, answer.type, 'A'), 'streamDecoded RR type match')
  t.ok(compare(t, answer.name, 'oj.bangj.com'), 'streamDecoded RR name match')
  t.ok(compare(t, answer.data, '129.250.11.170'), 'streamDecoded RR rdata match')
  t.ok(compare(t, authority.type, 'NS'), 'streamDecoded RR type match')
  t.ok(compare(t, authority.name, 'bangj.com'), 'streamDecoded RR name match')
  t.ok(compare(t, authority.data, 'oj.bangj.com'), 'streamDecoded RR rdata match')
  t.end()
})

tape('optioncodes', function (t) {
  const opts = [
    [0, 'OPTION_0'],
    [1, 'LLQ'],
    [2, 'UL'],
    [3, 'NSID'],
    [4, 'OPTION_4'],
    [5, 'DAU'],
    [6, 'DHU'],
    [7, 'N3U'],
    [8, 'CLIENT_SUBNET'],
    [9, 'EXPIRE'],
    [10, 'COOKIE'],
    [11, 'TCP_KEEPALIVE'],
    [12, 'PADDING'],
    [13, 'CHAIN'],
    [14, 'KEY_TAG'],
    [26946, 'DEVICEID'],
    [65535, 'OPTION_65535'],
    [64000, 'OPTION_64000'],
    [65002, 'OPTION_65002'],
    [-1, null]
  ]
  for (const [code, str] of opts) {
    const s = optioncodes.toString(code)
    t.ok(compare(t, s, str), `${code} => ${str}`)
    t.ok(compare(t, optioncodes.toCode(s), code), `${str} => ${code}`)
  }
  t.ok(compare(t, optioncodes.toCode('INVALIDINVALID'), -1))
  t.end()
})

tape('packet exported codec', function (t) {
  const input = {
    type: 'query',
    questions: [{
      type: 'A',
      name: 'hello.a.com',
      class: 'IN'
    }]
  }
  packet.encode.bytes = 0
  t.equals(packet.packet.encode.bytes, 0)
  t.equals(packet.packet.encode.bytes, packet.encode.bytes)
  const buf = packet.packet.encode(input)
  t.equals(packet.packet.encode.bytes, 29)
  t.equals(packet.packet.encode.bytes, packet.encode.bytes)
  t.deepEqual(
    buf,
    packet.encode(input)
  )
  packet.decode.bytes = 0
  t.equals(packet.packet.decode.bytes, 0)
  t.equals(packet.packet.decode.bytes, packet.decode.bytes)
  const obj = packet.packet.decode(buf)
  t.equals(packet.packet.decode.bytes, 29)
  t.equals(packet.packet.decode.bytes, packet.decode.bytes)
  t.deepEqual(
    obj,
    packet.decode(buf)
  )
  t.end()
})

tape('single query error with multiple questions', function (t) {
  t.throws(() => {
    packet.query.encode({
      questions: []
    })
  }, /Only one .question object expected instead of a .questions array!/)
  t.end()
})

tape('single query -> response encoding', function (t) {
  const question = {
    type: 'A',
    name: 'hello.a.com',
    class: 'IN'
  }
  const length = packet.query.encodingLength({ question })
  t.equals(length, 29)
  t.equals(packet.query.encode.bytes, 0)
  const queryBytes = packet.query.encode({
    question
  })
  const decodedQuestion = packet.decode(queryBytes)
  t.equals(packet.query.encode.bytes, length)
  t.equal(decodedQuestion.type, 'query')
  t.deepEqual(decodedQuestion.questions, [question])
  t.equals(packet.query.decode.bytes, 0)
  decodedQuestion.question = decodedQuestion.questions[0]
  delete decodedQuestion.questions

  t.deepEqual(packet.query.decode(queryBytes), decodedQuestion)
  const responseBytes = packet.encode({
    type: 'response',
    questions: [question]
  })
  const decodedResponse = packet.response.decode(responseBytes)
  t.equal(packet.response.encodingLength(decodedResponse), length)
  t.equals(packet.response.decode.bytes, length)
  t.deepEqual(decodedResponse.question, question)
  t.deepEqual(packet.response.encode(decodedResponse), responseBytes)
  t.end()
})

test('buffer utf8', function (sub) {
  for (const [index, fixture] of [
    // '', // empty
    'basic: hi',
    'japanese: 日本語',
    'mixed: 日本語 hi',
    'min 1 byte: \x00',
    'odd 1 byte: \x4c',
    'max 1 byte: \x7f',
    'min 2 byte: \x80',
    'odd 2 byte: \xd941',
    'max 2 byte: \x7fff',
    'min 3 byte: \x8000',
    'odd 3 byte: \xa158',
    'max 3 byte: \xffff',
    `4 byte: ${String.fromCodePoint(100000)}`
  ].entries()) {
    sub.test(`fixture #${index}`, t => {
      const check = Buffer.from(fixture)
      const checkHex = check.toString('hex')
      const len = check.length
      t.equals(bytelength(fixture), len, `fixture ${fixture} length`)
      const buf = new Uint8Array(len)
      t.equals(write(buf, fixture, 0), len, 'write.num')
      t.equals(toHex(buf, 0, len), checkHex, `write: ${fixture}`)
      t.equals(check.compare(Buffer.from(writeHex(buf, checkHex, 0, hexLength(checkHex)))), 0)
      t.equals(toUtf8(check, 0, check.length), check.toString(), `toUtf8: ${fixture}`)
      t.end()
    })
  }

  sub.test('surrogate pairs', function (t) {
    [
      [0xd821, 0xdea0],
      [0xd800, 0xdc00],
      [0xd801, 0xdc01],
      [0xddff, 0xdfff],
      [0xd821, 0x0000],
      [0xd821, 0xd821, 0xdea0]
    ].forEach(function (bytes, index) {
      const str = String.fromCharCode(...bytes)
      const buf = new Uint8Array(bytelength(str))
      const check = Buffer.from(str)
      t.equal(buf.length, check.length)
      write(buf, str, 0)
      t.equal(toHex(buf, 0, buf.length), check.toString('hex'), `#${index} [${bytes}].toHex() ... ${check.toString('hex')}`)
      t.equal(toUtf8(buf, 0, buf.length), check.toString(), `#${index} [${bytes}].toUtf8`)
    })
    t.end()
  })

  sub.test('all code points', function (t) {
    const blockSize = 2048
    const blocks = 65536 / blockSize
    let code = 0
    for (let block = 0; block < blocks; block += 1) {
      const expected = {}
      const actual = {}
      for (let i = 0; i < blockSize; i += 1, code += 1) {
        const str = String.fromCharCode(code)
        const buf = new Uint8Array(bytelength(str))
        write(buf, str, 0)
        const exp = Buffer.from(str)
        expected[code] = exp.toString('hex')
        actual[code] = toHex(buf, 0, buf.length)
      }
      t.same(actual, expected)
    }
    t.end()
  })
})

function testEncoder (t, rpacket, val) {
  const buf = rpacket.encode(val)
  const val2 = rpacket.decode(buf)

  t.same(buf.length, rpacket.encode.bytes, 'encode.bytes was set correctly')
  t.same(buf.length, rpacket.encodingLength(val), 'encoding length matches')
  t.ok(compare(t, val, val2), 'decoded object match')

  const buf2 = rpacket.encode(val2)
  const val3 = rpacket.decode(buf2)

  t.same(buf2.length, rpacket.encode.bytes, 'encode.bytes was set correctly on re-encode')
  t.same(buf2.length, rpacket.encodingLength(val), 'encoding length matches on re-encode')

  t.ok(compare(t, val, val3), 'decoded object match on re-encode')
  t.ok(compare(t, val2, val3), 're-encoded decoded object match on re-encode')

  const bigger = Buffer.allocUnsafe(buf2.length + 10)

  const buf3 = rpacket.encode(val, bigger, 10)
  const val4 = rpacket.decode(buf3, 10)

  t.ok(buf3 === bigger, 'echoes buffer on external buffer')
  t.same(rpacket.encode.bytes, buf.length, 'encode.bytes is the same on external buffer')
  t.ok(compare(t, val, val4), 'decoded object match on external buffer')
}

function compare (t, a, b) {
  if (a instanceof Uint8Array) return toHex(a, 0, a.length) === toHex(b, 0, b.length)
  if (typeof a === 'object' && a && b) {
    const keys = Object.keys(a)
    for (let i = 0; i < keys.length; i++) {
      if (!compare(t, a[keys[i]], b[keys[i]])) {
        return false
      }
    }
  } else if (Array.isArray(b) && !Array.isArray(a)) {
    // TXT always decode as array
    return a.toString() === b[0].toString()
  } else {
    return a === b
  }
  return true
}
