/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
'use strict';
const { assert } = require('chai');
const sinon = require('sinon');
const util = require('util');
const utils = require('../../lib/utils');
const tokenizer = require('../../lib/tokenizer');
const token = require('../../lib/token');
const Vector = require('../../lib/types/vector');
const Encoder = require('../../lib/encoder');
const { types } = require('../../index.js');
const ExecutionOptions = require('../../lib/execution-options').ExecutionOptions;
const dataTypes = types.dataTypes;
const helper = require('../test-helper');

const zeroLengthTypesSupported = new Set([
  dataTypes.text,
  dataTypes.ascii,
  dataTypes.varchar,
  dataTypes.custom,
  dataTypes.blob
]);

describe('encoder', function () {
  describe('Encoder.guessDataType()', function () {
    it('should guess the native types', function () {
      assertGuessed(1, dataTypes.double, 'Guess type for an integer (double) number failed');
      assertGuessed(1.01, dataTypes.double, 'Guess type for a double number failed');
      assertGuessed(true, dataTypes.boolean, 'Guess type for a boolean value failed');
      assertGuessed([1,2,3], dataTypes.list, 'Guess type for an Array value failed');
      assertGuessed('a string', dataTypes.text, 'Guess type for an string value failed');
      assertGuessed(utils.allocBufferFromString('bip bop'), dataTypes.blob, 'Guess type for a buffer value failed');
      assertGuessed(new Date(), dataTypes.timestamp, 'Guess type for a Date value failed');
      assertGuessed(new types.Long(10), dataTypes.bigint, 'Guess type for a Int 64 value failed');
      assertGuessed(types.Uuid.random(), dataTypes.uuid, 'Guess type for a UUID value failed');
      assertGuessed(types.TimeUuid.now(), dataTypes.uuid, 'Guess type for a TimeUuid value failed');
      assertGuessed(types.TimeUuid.now().toString(), dataTypes.uuid, 'Guess type for a string uuid value failed');
      assertGuessed(types.timeuuid(), dataTypes.uuid, 'Guess type for a Timeuuid value failed');
      assertGuessed(types.Integer.fromNumber(1), dataTypes.varint, 'Guess type for a varint value failed');
      assertGuessed(types.BigDecimal.fromString('1.01'), dataTypes.decimal, 'Guess type for a varint value failed');
      assertGuessed(types.Integer.fromBuffer(utils.allocBufferFromArray([0xff])), dataTypes.varint, 'Guess type for a varint value failed');
      assertGuessed(new types.InetAddress(utils.allocBufferFromArray([10, 10, 10, 2])), dataTypes.inet, 'Guess type for a inet value failed');
      assertGuessed(new types.Tuple(1, 2, 3), dataTypes.tuple, 'Guess type for a tuple value failed');
      assertGuessed(new types.LocalDate(2010, 4, 29), dataTypes.date, 'Guess type for a date value failed');
      assertGuessed(new types.LocalTime(types.Long.fromString('6331999999911')), dataTypes.time, 'Guess type for a time value failed');
      assertGuessed(new Float32Array([1.2, 3.4, 5.6]), dataTypes.custom, 'Guess type for a Float32 TypedArray value failed');
      assertGuessed({}, null, 'Objects must not be guessed');
    });

    function assertGuessed(value, expectedType, message) {
      const type = Encoder.guessDataType(value);
      if (type === null) {
        if (expectedType !== null) {
          assert.ok(false, 'Type not guessed for value ' + value);
        }
        return;
      }
      assert.strictEqual(type.code, expectedType, message + ': ' + value);
    }
  });

  describe('Encoder.isTypedArray()', function () {
    it('should return true for TypedArray subclasses', function () {
      assert.ok(Encoder.isTypedArray(new Float32Array([])));
      assert.ok(Encoder.isTypedArray(new Float32Array([1.2, 3.4, 5.6])));
      assert.ok(Encoder.isTypedArray(new Float64Array([])));
      assert.ok(Encoder.isTypedArray(new Float64Array([1.2, 3.4, 5.6])));
      assert.ok(Encoder.isTypedArray(new Int8Array([])));
      assert.ok(Encoder.isTypedArray(new Int8Array([1, 2, 3])));
      assert.ok(Encoder.isTypedArray(new Uint8Array([])));
      assert.ok(Encoder.isTypedArray(new Uint8Array([1, 2, 3])));
    });

    it('should return false for other types', function () {
      assert.notOk(Encoder.isTypedArray(100));
      assert.notOk(Encoder.isTypedArray([]));
      assert.notOk(Encoder.isTypedArray([1,2,3]));
      assert.notOk(Encoder.isTypedArray([1.2, 3.4, 5.6]));
    });
  });

  describe('#encode() and #decode()', function () {
    const typeEncoder = new Encoder(2, {});
    it('should encode and decode a guessed double', function () {
      const value = 1111.1;
      const encoded = typeEncoder.encode(value);
      const decoded = typeEncoder.decode(encoded, {code: dataTypes.double});
      assert.strictEqual(decoded, value);
    });

    it('should encode and decode a guessed string', function () {
      const value = 'Pennsatucky';
      const encoded = typeEncoder.encode(value);
      const decoded = typeEncoder.decode(encoded, {code: dataTypes.text});
      assert.strictEqual(decoded, value);
    });

    it('should encode stringified uuids for backward-compatibility', function () {
      let uuid = types.Uuid.random();
      let encoded = typeEncoder.encode(uuid.toString(), types.dataTypes.uuid);
      assert.strictEqual(encoded.toString('hex'), uuid.getBuffer().toString('hex'));

      uuid = types.TimeUuid.now();
      encoded = typeEncoder.encode(uuid.toString(), types.dataTypes.uuid);
      assert.strictEqual(encoded.toString('hex'), uuid.getBuffer().toString('hex'));
    });

    it('should throw when string is not an uuid', function () {
      assert.throws(function () {
        typeEncoder.encode('', types.dataTypes.uuid);
      }, TypeError);
    });

    it('should encode undefined as null', function () {
      const hinted = typeEncoder.encode(undefined, 'set<text>');
      const unHinted = typeEncoder.encode();
      assert.strictEqual(hinted, null);
      assert.strictEqual(unHinted, null);
    });

    it('should throw on unknown types', function () {
      assert.throws(function () {
        typeEncoder.encode({});
      }, TypeError);
    });

    it('should throw when the typeInfo and the value source type does not match', function () {
      assert.throws(function () {
        typeEncoder.encode('hello', 'int');
      }, TypeError);
      assert.throws(function () {
        typeEncoder.encode(100, dataTypes.uuid);
      }, TypeError);
      assert.throws(function () {
        typeEncoder.encode(200, dataTypes.timeuuid);
      }, TypeError);
      assert.throws(function () {
        typeEncoder.encode('Its anybody in there? I know that you can hear me', dataTypes.blob);
      }, TypeError);
      assert.throws(function () {
        typeEncoder.encode(100, dataTypes.blob);
      }, TypeError);
      assert.throws(function () {
        typeEncoder.encode({}, dataTypes.list);
      }, TypeError);
    });

    it('should encode Long/Date/Number/String as timestamps', function () {
      const encoder = new Encoder(2, {});
      let buffer = encoder.encode(types.Long.fromBits(0x00fafafa, 0x07090909), dataTypes.timestamp);
      assert.strictEqual(buffer.toString('hex'), '0709090900fafafa');
      buffer = encoder.encode(1421755130012, dataTypes.timestamp);
      assert.strictEqual(buffer.toString('hex'), '0000014b0735a09c');
      buffer = encoder.encode(new Date(1421755130012), dataTypes.timestamp);
      assert.throws(function () {
        encoder.encode(new Date('This is an invalid date string'), dataTypes.timestamp);
      }, TypeError);
      assert.strictEqual(buffer.toString('hex'), '0000014b0735a09c');
      buffer = encoder.encode(new Date(1421755130012), dataTypes.timestamp);
      assert.strictEqual(buffer.toString('hex'), '0000014b0735a09c');
      buffer = encoder.encode('Tue Jan 20 2015 13:00:35 GMT+0100 (CET)', dataTypes.timestamp);
      assert.strictEqual(buffer.toString('hex'), '0000014b07373ab8');
      assert.throws(function () {
        encoder.encode('This is an invalid date string', dataTypes.timestamp);
      }, TypeError);
    });

    it('should encode String/Number (not NaN) as int', function () {
      const encoder = new Encoder(2, {});
      let buffer = encoder.encode(0x071272ab, dataTypes.int);
      assert.strictEqual(buffer.toString('hex'), '071272ab');
      buffer = encoder.encode('0x071272ab', dataTypes.int);
      assert.strictEqual(buffer.toString('hex'), '071272ab');
      buffer = encoder.encode(-1, 'int');
      assert.strictEqual(buffer.toString('hex'), 'ffffffff');
      buffer = encoder.encode('-1', 'int');
      assert.strictEqual(buffer.toString('hex'), 'ffffffff');
      buffer = encoder.encode(0, 'int');
      assert.strictEqual(buffer.toString('hex'), '00000000');
      buffer = encoder.encode('0', 'int');
      assert.strictEqual(buffer.toString('hex'), '00000000');
      assert.throws(function () {
        encoder.encode(NaN, 'int');
      }, TypeError);
    });

    it('should encode String/Long/Number as bigint', function () {
      const encoder = new Encoder(2, {});
      let buffer = encoder.encode(types.Long.fromString('506946367331695353'), dataTypes.bigint);
      assert.strictEqual(buffer.toString('hex'), '0709090900fafaf9');
      buffer = encoder.encode('506946367331695353', dataTypes.bigint);
      assert.strictEqual(buffer.toString('hex'), '0709090900fafaf9');
      buffer = encoder.encode(0, dataTypes.bigint);
      assert.strictEqual(buffer.toString('hex'), '0000000000000000');
      buffer = encoder.encode(255, dataTypes.bigint);
      assert.strictEqual(buffer.toString('hex'), '00000000000000ff');
    });

    it('should encode String/Integer/Number as varint', function () {
      const encoder = new Encoder(2, {});
      let buffer = encoder.encode(types.Integer.fromString('33554433'), dataTypes.varint);
      assert.strictEqual(buffer.toString('hex'), '02000001');
      buffer = encoder.encode('-150012', dataTypes.varint);
      assert.strictEqual(buffer.toString('hex'), 'fdb604');
      buffer = encoder.encode('-1000000000012', dataTypes.varint);
      assert.strictEqual(buffer.toString('hex'), 'ff172b5aeff4');
      buffer = encoder.encode(-128, dataTypes.varint);
      assert.strictEqual(buffer.toString('hex'), '80');
      buffer = encoder.encode(-100, dataTypes.varint);
      assert.strictEqual(buffer.toString('hex'), '9c');
    });

    it('should encode String/BigDecimal/Number as decimal', function () {
      const encoder = new Encoder(2, {});
      let buffer = encoder.encode(types.BigDecimal.fromString('0.00256'), dataTypes.decimal);
      assert.strictEqual(buffer.toString('hex'), '000000050100');
      buffer = encoder.encode('-0.01', dataTypes.decimal);
      assert.strictEqual(buffer.toString('hex'), '00000002ff');
      buffer = encoder.encode('-25.5', dataTypes.decimal);
      assert.strictEqual(buffer.toString('hex'), '00000001ff01');
      buffer = encoder.encode(0.004, dataTypes.decimal);
      assert.strictEqual(buffer.toString('hex'), '0000000304');
      buffer = encoder.encode(-25.5, dataTypes.decimal);
      assert.strictEqual(buffer.toString('hex'), '00000001ff01');
    });

    it('should encode/decode InetAddress/Buffer as inet', function () {
      const InetAddress = types.InetAddress;
      const encoder = new Encoder(2, {});
      let val1 = new InetAddress(utils.allocBufferFromArray([15, 15, 15, 1]));
      let encoded = encoder.encode(val1, dataTypes.inet);
      assert.strictEqual(encoded.toString('hex'), '0f0f0f01');
      let val2 = encoder.decode(encoded, {code: dataTypes.inet});
      assert.strictEqual(val2.toString(), '15.15.15.1');
      assert.ok(val1.equals(val2));
      val1 = new InetAddress(utils.allocBufferFromString('00000000000100112233445500aa00bb', 'hex'));
      encoded = encoder.encode(val1, dataTypes.inet);
      val2 = encoder.decode(encoded, {code: dataTypes.inet});
      assert.ok(val1.equals(val2));
      //Buffers are valid InetAddress
      encoded = encoder.encode(val1.getBuffer(), dataTypes.inet);
      assert.strictEqual(encoded.toString('hex'), val1.getBuffer().toString('hex'));
    });

    it('should decode uuids into Uuid', function () {
      const uuid = types.Uuid.random();
      const decoded = typeEncoder.decode(uuid.getBuffer(), {code: dataTypes.uuid});
      helper.assertInstanceOf(decoded, types.Uuid);
      assert.strictEqual(uuid.toString(), decoded.toString());
      assert.ok(uuid.equals(decoded));
      const decoded2 = typeEncoder.decode(types.Uuid.random().getBuffer(), {code: dataTypes.uuid});
      assert.ok(!decoded.equals(decoded2));
    });

    it('should decode timeuuids into TimeUuid', function () {
      const uuid = types.TimeUuid.now();
      const decoded = typeEncoder.decode(uuid.getBuffer(), {code: dataTypes.timeuuid});
      helper.assertInstanceOf(decoded, types.TimeUuid);
      assert.strictEqual(uuid.toString(), decoded.toString());
      assert.ok(uuid.equals(decoded));
      const decoded2 = typeEncoder.decode(types.TimeUuid.now().getBuffer(), {code: dataTypes.timeuuid});
      assert.ok(!decoded.equals(decoded2));
    });

    [2, 3].forEach(function (version) {
      const encoder = new Encoder(version, {});
      it(util.format('should encode and decode maps for protocol v%d', version), function () {
        let value = {value1: 'Surprise', value2: 'Madafaka'};
        //Minimum info, guessed
        let encoded = encoder.encode(value, dataTypes.map);
        let decoded = encoder.decode(encoded, {code: dataTypes.map, info: [{code: dataTypes.text}, {code: dataTypes.text}]});
        assert.strictEqual(util.inspect(decoded), util.inspect(value));
        //Minimum info, guessed
        value = {value1: 1.1, valueN: 1.2};
        encoded = encoder.encode(value, dataTypes.map);
        decoded = encoder.decode(encoded, {code: dataTypes.map, info: [{code: dataTypes.text}, {code: dataTypes.double}]});
        assert.strictEqual(util.inspect(decoded), util.inspect(value));
        //Minimum info string, guessed
        value = {value1: new Date(9999999), valueN: new Date(5555555)};
        encoded = encoder.encode(value, 'map');
        decoded = encoder.decode(encoded, {code: dataTypes.map, info: [{code: dataTypes.text}, {code: dataTypes.timestamp}]});
        assert.strictEqual(util.inspect(decoded), util.inspect(value));
        //Minimum info string, guessed
        value = {};
        value[types.uuid()] = 0;
        value[types.uuid()] = 2;
        encoded = encoder.encode(value, 'map');
        decoded = encoder.decode(encoded, {code: dataTypes.map, info: [{code: dataTypes.uuid}, {code: dataTypes.double}]});
        assert.strictEqual(util.inspect(decoded), util.inspect(value));
        //full info string
        value = {value1: 1, valueN: -3};
        encoded = encoder.encode(value, 'map<text,int>');
        decoded = encoder.decode(encoded, {code: dataTypes.map, info: [{code: dataTypes.text}, {code: dataTypes.int}]});
        assert.strictEqual(util.inspect(decoded), util.inspect(value));
        //full info typeInfo
        value = {value1: 1, valueN: -33892};
        encoded = encoder.encode(value, {code: dataTypes.map, info: [dataTypes.string, dataTypes.int]});
        decoded = encoder.decode(encoded, {code: dataTypes.map, info: [{code: dataTypes.text}, {code: dataTypes.int}]});
        assert.strictEqual(util.inspect(decoded), util.inspect(value));
      });

      it(util.format('should encode and decode maps with stringified keys for protocol v%d', version), function () {
        let value = {};
        value[new Date(1421756675488)] = 'date1';
        value[new Date(1411756633461)] = 'date2';

        let encoded = encoder.encode(value, {code: dataTypes.map, info: [{code: dataTypes.timestamp}, {code: dataTypes.text}]});
        let decoded = encoder.decode(encoded, {code: dataTypes.map, info: [{code: dataTypes.timestamp}, {code: dataTypes.text}]});
        assert.strictEqual(util.inspect(decoded), util.inspect(value));

        value = {};
        value[101] = 'number1';
        value[102] = 'number2';
        encoded = encoder.encode(value, 'map<int, text>');
        decoded = encoder.decode(encoded, {code: dataTypes.map, info: [{code: dataTypes.int}, {code: dataTypes.text}]});
        assert.strictEqual(util.inspect(decoded), util.inspect(value));

        value = {};
        value[types.Long.fromBits(0x12002001, 0x7f999299)] = 'bigint1';
        value[types.Long.fromBits(0x12002000, 0x7f999299)] = 'bigint2';
        encoded = encoder.encode(value, 'map<bigint, text>');
        decoded = encoder.decode(encoded, {code: dataTypes.map, info: [{code: dataTypes.bigint}, {code: dataTypes.text}]});
        assert.strictEqual(util.inspect(decoded), util.inspect(value));

        value = {};
        value['201'] = 'bigint1_1';
        value['202'] = 'bigint2_1';
        encoded = encoder.encode(value, 'map<bigint, text>');
        decoded = encoder.decode(encoded, {code: dataTypes.map, info: [{code: dataTypes.bigint}, {code: dataTypes.text}]});
        assert.strictEqual(util.inspect(decoded), util.inspect(value));

        value = {};
        value['2d5db74c-c2da-4e59-b5ec-d8ad3d0aefb9'] = 'uuid1';
        value['651b5c17-5357-4764-ae2d-21c409288822'] = 'uuid2';
        encoded = encoder.encode(value, 'map<uuid, text>');
        decoded = encoder.decode(encoded, {code: dataTypes.map, info: [{code: dataTypes.uuid}, {code: dataTypes.text}]});
        assert.strictEqual(util.inspect(decoded), util.inspect(value));

        value = {};
        value['1ab50440-a0ab-11e4-9d01-1dc0e727b460'] = 'timeuuid1';
        value['1820c4d0-a0ab-11e4-9d01-1dc0e727b460'] = 'timeuuid2';
        encoded = encoder.encode(value, 'map<timeuuid, text>');
        decoded = encoder.decode(encoded, {code: dataTypes.map, info: [{code: dataTypes.timeuuid}, {code: dataTypes.text}]});
        assert.strictEqual(util.inspect(decoded), util.inspect(value));

        value = {};
        value['988229782938247303441911118'] = 'varint1';
        value['988229782938247303441911119'] = 'varint2';
        encoded = encoder.encode(value, 'map<varint, text>');
        decoded = encoder.decode(encoded, {code: dataTypes.map, info: [{code: dataTypes.varint}, {code: dataTypes.text}]});
        assert.strictEqual(util.inspect(decoded), util.inspect(value));

        value = {};
        value['12.1'] = 'decimal1';
        value['12.90'] = 'decimal2';
        encoded = encoder.encode(value, 'map<decimal, text>');
        decoded = encoder.decode(encoded, {code: dataTypes.map, info: [{code: dataTypes.decimal}, {code: dataTypes.text}]});
        assert.strictEqual(util.inspect(decoded), util.inspect(value));

        value = {};
        value['127.0.0.1'] = 'inet1';
        value['12.10.10.2'] = 'inet2';
        encoded = encoder.encode(value, 'map<inet, text>');
        decoded = encoder.decode(encoded, {code: dataTypes.map, info: [{code: dataTypes.inet}, {code: dataTypes.text}]});
        assert.strictEqual(util.inspect(decoded), util.inspect(value));

        value = {};
        value['::1'] = 'inet1';
        value['::2233:0:0:b1'] = 'inet2';
        value['aabb::11:2233:4455:6677:88ff'] = 'inet3';
        encoded = encoder.encode(value, 'map<inet, text>');
        decoded = encoder.decode(encoded, {code: dataTypes.map, info: [{code: dataTypes.inet}, {code: dataTypes.text}]});
        assert.strictEqual(util.inspect(decoded), util.inspect(value));

        value = {};
        value['12:59:56'] = 'time1';
        value['15:01:02.1234'] = 'time2';
        value['06:01:02.000000213'] = 'time3';
        encoded = encoder.encode(value, 'map<time, text>');
        decoded = encoder.decode(encoded, {code: dataTypes.map, info: [{code: dataTypes.time}, {code: dataTypes.text}]});
        assert.strictEqual(util.inspect(decoded), util.inspect(value));

        value = {};
        value['2015-01-30'] = 'date1';
        value['1999-11-12'] = 'date2';
        value['-0001-11-12'] = 'date3';
        encoded = encoder.encode(value, 'map<date, text>');
        decoded = encoder.decode(encoded, {code: dataTypes.map, info: [{code: dataTypes.date}, {code: dataTypes.text}]});
        assert.strictEqual(util.inspect(decoded), util.inspect(value));
      });

      it(util.format('should encode and decode list<int> for protocol v%d', version), function () {
        const value = [1, 2, 3, 4];
        const encoded = encoder.encode(value, 'list<int>');
        const decoded = encoder.decode(encoded, {code: dataTypes.list, info: {code: dataTypes.int}});
        assert.strictEqual(util.inspect(decoded), util.inspect(value));
      });

      it(util.format('should encode and decode list<double> for protocol v%d', version), function () {
        const value = [1, 2, 3, 100];
        const encoded = encoder.encode(value, 'list<double>');
        const decoded = encoder.decode(encoded, {code: dataTypes.list, info: {code: dataTypes.double}});
        assert.strictEqual(util.inspect(decoded), util.inspect(value));
      });

      it(util.format('should encode and decode list<double> without hint for protocol v%d', version), function () {
        const value = [1, 2, 3, 100.1];
        const encoded = encoder.encode(value);
        const decoded = encoder.decode(encoded, {code: dataTypes.list, info: {code: dataTypes.double}});
        assert.strictEqual(util.inspect(decoded), util.inspect(value));
      });

      it(util.format('should encode and decode set<text> for protocol v%d', version), function () {
        const value = ['Alex Vause', 'Piper Chapman', '3', '4'];
        let encoded = encoder.encode(value, 'set<text>');
        let decoded = encoder.decode(encoded, {code: dataTypes.set, info: {code: dataTypes.text}});
        assert.strictEqual(util.inspect(decoded), util.inspect(value));
        //with type info
        encoded = encoder.encode(value, {code: dataTypes.set, info: {code: dataTypes.text}});
        decoded = encoder.decode(encoded, {code: dataTypes.set, info: {code: dataTypes.text}});
        assert.strictEqual(util.inspect(decoded), util.inspect(value));
      });

      it(util.format('should encode and decode list<float> with typeInfo for protocol v%d', version), function () {
        const value = [1.1122000217437744, 2.212209939956665, 3.3999900817871094, 4.412120819091797, -1000, 1];
        const encoded = encoder.encode(value, {code: dataTypes.list, info: {code: dataTypes.float}});
        const decoded = encoder.decode(encoded, {code: dataTypes.list, info: {code: dataTypes.float}});
        assert.strictEqual(util.inspect(decoded), util.inspect(value));
      });

      it(util.format('should encode/decode ES6 Set as maps for protocol v%d', version), function () {
        if (typeof Set !== 'function') {
          //Set not supported in Node.js runtime
          return;
        }
        // eslint-disable-next-line no-undef
        const Es6Set = Set;
        const encoder = new Encoder(version, { encoding: { set: Es6Set}});
        let m = new Es6Set(['k1', 'k2', 'k3']);
        let encoded = encoder.encode(m, 'set<text>');
        if (version === 2) {
          assert.strictEqual(encoded.toString('hex'), '000300026b3100026b3200026b33');
        }
        let decoded = encoder.decode(encoded, {code: dataTypes.set, info: {code: dataTypes.text}});
        helper.assertInstanceOf(decoded, Es6Set);
        assert.strictEqual(decoded.toString(), m.toString());

        m = new Es6Set([1, 2, 1000]);
        encoded = encoder.encode(m, 'set<int>');
        if (version === 2) {
          assert.strictEqual(encoded.toString('hex'), '00030004000000010004000000020004000003e8');
        }
        decoded = encoder.decode(encoded, {code: dataTypes.set, info: {code: dataTypes.text}});
        assert.strictEqual(decoded.toString(), m.toString());
      });

      it(util.format('should encode/decode Map polyfills as maps for protocol v%d', version), function () {
        const encoder = new Encoder(version, { encoding: { map: helper.Map}});
        let m = new helper.Map();
        m.set('k1', 'v1');
        m.set('k2', 'v2');
        let encoded = encoder.encode(m, 'map<text,text>');
        if (version === 2) {
          assert.strictEqual(encoded.toString('hex'), '000200026b310002763100026b3200027632');
        }
        let decoded = encoder.decode(encoded, {code: dataTypes.map, info: [{code: dataTypes.text}, {code: dataTypes.text}]});
        helper.assertInstanceOf(decoded, helper.Map);
        assert.strictEqual(decoded.arr.toString(), m.arr.toString());

        m = new helper.Map();
        m.set('k1', 1);
        m.set('k2', 2);
        m.set('k3', 3);
        encoded = encoder.encode(m, 'map<text,int>');
        if (version === 2) {
          assert.strictEqual(encoded.toString('hex'), '000300026b3100040000000100026b3200040000000200026b33000400000003');
        }
        decoded = encoder.decode(encoded, {code: dataTypes.map, info: [{code: dataTypes.text}, {code: dataTypes.int}]});
        assert.strictEqual(decoded.arr.toString(), m.arr.toString());
      });

      it(util.format('should encode/decode ES6 Map as maps for protocol v%d', version), function () {
        function getValues(m) {
          const arr = [];
          m.forEach(function (val, key) {
            arr.push([key, val]);
          });
          return arr.toString();
        }
        // eslint-disable-next-line no-undef
        const Es6Map = Map;
        const encoder = new Encoder(version, { encoding: { map: Es6Map}});
        let m = new Es6Map();
        m.set('k1', 'v1');
        m.set('k2', 'v2');
        let encoded = encoder.encode(m, 'map<text,text>');
        if (version === 2) {
          assert.strictEqual(encoded.toString('hex'), '000200026b310002763100026b3200027632');
        }
        let decoded = encoder.decode(encoded, {code: dataTypes.map, info: [{code: dataTypes.text}, {code: dataTypes.text}]});
        helper.assertInstanceOf(decoded, Es6Map);
        assert.strictEqual(getValues(decoded), getValues(m));

        m = new Es6Map();
        m.set('k1', 1);
        m.set('k2', 2);
        m.set('k3', 3);
        encoded = encoder.encode(m, 'map<text,int>');
        if (version === 2) {
          assert.strictEqual(encoded.toString('hex'), '000300026b3100040000000100026b3200040000000200026b33000400000003');
        }
        decoded = encoder.decode(encoded, {code: dataTypes.map, info: [{code: dataTypes.text}, {code: dataTypes.int}]});
        assert.strictEqual(getValues(decoded), getValues(m));

        m = new Es6Map();
        m.set(new Date('2005-08-05'), 10);
        m.set(new Date('2010-04-29'), 2);
        encoded = encoder.encode(m, 'map<timestamp,int>');
        decoded = encoder.decode(encoded, {code: dataTypes.map, info: [{code: dataTypes.timestamp}, {code: dataTypes.int}]});
        assert.strictEqual(getValues(decoded), getValues(m));
      });

      it(util.format('should encode/decode Set polyfills as maps for protocol v%d', version), function () {
        const encoder = new Encoder(version, { encoding: { set: helper.Set}});
        let m = new helper.Set(['k1', 'k2', 'k3']);
        let encoded = encoder.encode(m, 'set<text>');
        if (version === 2) {
          assert.strictEqual(encoded.toString('hex'), '000300026b3100026b3200026b33');
        }
        let decoded = encoder.decode(encoded, {code: dataTypes.set, info: {code: dataTypes.text}});
        helper.assertInstanceOf(decoded, helper.Set);
        assert.strictEqual(decoded.toString(), m.toString());

        m = new helper.Set([1, 2, 1000]);
        encoded = encoder.encode(m, 'set<int>');
        if (version === 2) {
          assert.strictEqual(encoded.toString('hex'), '00030004000000010004000000020004000003e8');
        }
        decoded = encoder.decode(encoded, {code: dataTypes.set, info: {code: dataTypes.int}});
        assert.strictEqual(decoded.toString(), m.toString());
      });
    });

    it('should encode/decode udts', function () {
      const encoder = new Encoder(3, {});
      const type = { code: dataTypes.udt, info: { fields:[
        {name: 'alias', type:{code:dataTypes.text}},
        {name: 'number', type:{code:dataTypes.text}}] }};
      const encoded = encoder.encode({ alias: 'zeta'}, type);
      const decoded = encoder.decode(encoded, type);
      assert.strictEqual(decoded['alias'], 'zeta');
      assert.strictEqual(decoded['number'], null);
    });

    it('should encode/decode nested collections', function () {
      const encoder = new Encoder(3, {});
      const type = { code: dataTypes.map, info: [{code: dataTypes.text}, {code: dataTypes.set, info: {code: dataTypes.text}}]};
      const encoded = encoder.encode({ key1: ['first', 'second', 'third'], key2: ['2-first']}, type);
      const decoded = encoder.decode(encoded, type);
      assert.ok(decoded.key1);
      assert.strictEqual(decoded.key1.length, 3);
      assert.strictEqual(decoded.key1[0], 'first');
      assert.strictEqual(decoded.key1[1], 'second');
      assert.strictEqual(decoded.key1[2], 'third');
      assert.ok(decoded.key2);
      assert.strictEqual(decoded.key2.length, 1);
      assert.strictEqual(decoded.key2[0], '2-first');
    });

    it('should encode/decode tuples', function () {
      const encoder = new Encoder(3, {});
      const type = { code: dataTypes.tuple, info: [ { code: dataTypes.text}, { code: dataTypes.timestamp }]};
      const encoded = encoder.encode(new types.Tuple('one', new Date(1429259123607)), type);
      const decoded = encoder.decode(encoded, type);
      assert.strictEqual(decoded.length, 2);
      assert.strictEqual(decoded.get(0), 'one');
      assert.strictEqual(decoded.get(1).getTime(), 1429259123607);
    });

    it('should encode/decode LocalDate as date', function () {
      const encoder = new Encoder(4, {});
      const type = {code: dataTypes.date};

      const year1day1 = new Date(Date.UTC(1970, 0, 1));
      year1day1.setUTCFullYear(1);

      const year0day1 = new Date(Date.UTC(1970, 0, 1));
      year0day1.setUTCFullYear(0);

      const dates = [
        // At epoch.
        {ldate: new types.LocalDate(1970, 1, 1), string: '1970-01-01', date: new Date(Date.UTC(1970, 0, 1))},
        // 10 days after epoch.
        {ldate: new types.LocalDate(1970, 1, 11), string: '1970-01-11', date: new Date(Date.UTC(1970, 0, 11))},
        // -10 days from epoch.
        {ldate: new types.LocalDate(1969, 12, 22), string: '1969-12-22', date: new Date(Date.UTC(1969, 11, 22))},
        // Year after 0.
        {ldate: new types.LocalDate(1, 1, 1), string: '0001-01-01', date: year1day1},
        // 0th year.
        {ldate: new types.LocalDate(0, 1, 1), string: '0000-01-01', date: year0day1},
        // Year before 0.
        {ldate: new types.LocalDate(-1, 1, 1), string: '-0001-01-01', date: new Date(Date.UTC(-1, 0, 1))},
        // Minimum possible ES5 date.
        {ldate: new types.LocalDate(-271821, 4, 20), string: '-271821-04-20', date: new Date(Date.UTC(-271821, 3, 20))},
        // Maximum possible ES5 date.
        {ldate: new types.LocalDate(275760, 9, 13), string: '275760-09-13', date: new Date(Date.UTC(275760, 8, 13))},
        // Minimum possible C* date.
        {ldate: new types.LocalDate(-2147483648), string: '-2147483648', date: new Date(NaN)},
        // Maximum possible C* date.
        {ldate: new types.LocalDate(2147483647), string: '2147483647', date: new Date(NaN)}
      ];

      dates.forEach(function(item) {
        const encoded = encoder.encode(item.ldate, type);
        const decoded = encoder.decode(encoded, type);
        helper.assertInstanceOf(decoded, types.LocalDate);
        assert.ok(decoded.equals(item.ldate));
        assert.strictEqual(decoded.toString(), item.string, "String mismatch for " + item.date);
        if(isNaN(item.date.getTime())) {
          assert.ok(isNaN(decoded.date.getTime()));
        }
        else {
          assert.equal(decoded.date.getTime(), item.date.getTime(), decoded.date + " != " + item.date);
        }
      });
    });

    it('should refuse to encode invalid values as LocalDate.', function () {
      const encoder = new Encoder(4, {});
      const type = {code: dataTypes.date};
      // Non Date/String/LocalDate
      assert.throws(function () { encoder.encode(23.0, type);}, TypeError);
      assert.throws(function () { encoder.encode('zzz', type);}, TypeError);
      assert.throws(function () { encoder.encode('', type);}, TypeError);
    });

    it('should encode/decode LocalTime as time', function () {
      const encoder = new Encoder(3, {});
      const type = {code: dataTypes.time};
      /* eslint-disable no-multi-spaces */
      [
        //Long value         |     string representation
        ['2000000501',             '00:00:02.000000501'],
        ['0',                      '00:00:00'],
        ['3600000006001',          '01:00:00.000006001'],
        ['61000000000',            '00:01:01'],
        ['610000136000',           '00:10:10.000136'],
        ['52171800000000',         '14:29:31.8'],
        ['52171800600000',         '14:29:31.8006']
      ].forEach(function (item) {
        const encoded = encoder.encode(new types.LocalTime(types.Long.fromString(item[0])), type);
        const decoded = encoder.decode(encoded, type);
        helper.assertInstanceOf(decoded, types.LocalTime);
        assert.strictEqual(decoded.toString(), item[1]);
      });
      /* eslint-enable no-multi-spaces */
    });

    it('should refuse to encode invalid values as LocalTime.', function () {
      const encoder = new Encoder(4, {});
      const type = {code: dataTypes.time};
      // Negative value string.
      assert.throws(function () { encoder.encode('-1:00:00', type);}, TypeError);
      // Non string/LocalTime value.
      assert.throws(function () { encoder.encode(23.0, type);}, TypeError);
    });

    it('should encode/decode FloatArray as vector, encoder guesses type', function () {
      const encoder = new Encoder(4, {});
      const refVal = new Float32Array([1.2, 3.4, 5.6]);
      const guessedTypeObj = Encoder.guessDataType(refVal);
      if (guessedTypeObj == null){
        assert.fail();
      }
      const encoded = encoder.encode(refVal, guessedTypeObj);
      const decoded = encoder.decode(encoded, guessedTypeObj);
      helper.assertInstanceOf(decoded, Vector);
      for (let i = 0; i < decoded.length; i++) {
        assert.strictEqual(decoded[i],refVal[i]);
      }
    });

    it('should encode/decode FloatArray as vector, encoder provided with full type', function () {
      const encoder = new Encoder(4, {});
      const refVal = new Float32Array([1.2, 3.4, 5.6]);
      /** @type {import('../../lib/encoder').VectorColumnInfo} */
      const typeObj = {code: dataTypes.custom, info: [{code : dataTypes.float},3], customTypeName : 'vector'};
      const encoded = encoder.encode(refVal, typeObj);
      const decoded = encoder.decode(encoded, typeObj);
      helper.assertInstanceOf(decoded, Vector);
      for (let i = 0; i < decoded.length; i++) {
        assert.strictEqual(decoded[i],refVal[i]);
      }
    });

    it('should encode/decode Vector of texts as vector, encoder provided with full type', function () {
      const encoder = new Encoder(4, {});
      const refVal = new Vector(['a', 'bc', 'de']);
      /** @type {import('../../lib/encoder').VectorColumnInfo} */
      const typeObj = {code: dataTypes.custom, info: [{code : dataTypes.ascii},3], customTypeName : 'vector'};
      const encoded = encoder.encode(refVal, typeObj);
      const decoded = encoder.decode(encoded, typeObj);
      helper.assertInstanceOf(decoded, Vector);
      for (let k = 0; k < decoded.length; k++) {
        assert.strictEqual(decoded[k],refVal[k]);
      }
    });


    it('should fail to encode if full type provided and input vector fails to match dimensions of type', function () {
      const encoder = new Encoder(4, {});
      const refVal = new Float32Array([1.2, 3.4, 5.6, 7.8]);
      const typeName = 'org.apache.cassandra.db.marshal.VectorType(org.apache.cassandra.db.marshal.FloatType,3)';
      assert.throws(function() { encoder.encode(refVal, {code: dataTypes.custom, info: typeName}); }, TypeError);
    });

    it('should fail to encode if input vector is not Float32Array, encoder guesses type', function () {
      const encoder = new Encoder(4, {});
      const refVal = new Int32Array([1, 2, 3]);
      const guessedTypeObj = Encoder.guessDataType(refVal);
      assert.throws(function() { encoder.encode(refVal, {code: dataTypes.custom, info: guessedTypeObj}); }, TypeError);
    });

    it('should fail to encode if input vector is not Float32Array, encoder provided with full type', function () {
      const encoder = new Encoder(4, {});
      const refVal = new Int32Array([1, 2, 3]);
      const typeName = 'org.apache.cassandra.db.marshal.VectorType(org.apache.cassandra.db.marshal.FloatType,3)';
      assert.throws(function() { encoder.encode(refVal, {code: dataTypes.custom, info: typeName}); }, TypeError);
    });

    it('should encode/decode nested type as vector, encoder guesses type', function () {
      const encoder = new Encoder(4, {});
      const refVal = new Vector([new Float32Array([1.2, 3.4, 5.6]), new Float32Array([7.8, 9.0, 11.2])]);
      const guessedTypeObj = Encoder.guessDataType(refVal);
      if (guessedTypeObj == null){
        assert.fail();
        return;
      }
      const encoded = encoder.encode(refVal, guessedTypeObj);
      const decoded = encoder.decode(encoded, guessedTypeObj);
      helper.assertInstanceOf(decoded, Vector);
      for (let i = 0; i < decoded.length; i++) {
        for (let j = 0; j < decoded[i].length; j++) {
          assert.strictEqual(decoded[i][j],refVal[i][j]);
        }
      }
    });

  });

  describe('#encode()', function () {
    it('should return null when value is null', function () {
      const encoder = new Encoder(2, {});
      assert.strictEqual(encoder.encode(null), null);
    });

    it('should return unset when value is unset', function () {
      const encoder = new Encoder(4, {});
      assert.strictEqual(encoder.encode(types.unset), types.unset);
    });

    it('should return null when value is undefined', function () {
      const encoder = new Encoder(2, {});
      assert.strictEqual(encoder.encode(undefined), null);
    });

    it('should return unset when value is undefined and flag set', function () {
      const encoder = new Encoder(4, { encoding: { useUndefinedAsUnset: true}});
      assert.strictEqual(encoder.encode(undefined), types.unset);
    });

    it('should throw TypeError when value is unset with low protocol version', function () {
      const encoder = new Encoder(2, {});
      assert.throws(function () {
        encoder.encode(types.unset);
      }, TypeError);
    });

    it('should return null when value is undefined and flag set with low protocol version', function () {
      const encoder = new Encoder(2, { encoding: { useUndefinedAsUnset: true}});
      assert.strictEqual(encoder.encode(undefined), null);
    });

    it('should decode 0-length map values of supported types into 0-length values', function () {
      const input = {'key1': Buffer.from([])};
      [2, 3].forEach(v => {
        const encoder = new Encoder(v, {});
        const buffer = encoder.encode(input, dataTypes.map);
        Array.from(zeroLengthTypesSupported).forEach(function(t){
          const type = { code: types.dataTypes.map, info: [ { code: types.dataTypes.text }, { code: t } ]};
          const value = encoder.decode(buffer, type);
          assert.ok(value);
          assert.deepEqual(Object.keys(value), Object.keys(input));
          assert.strictEqual(value['key1'].length, 0);
        });
      });
    });

    it('should decode 0-length map values of unsupported types into null values', function () {
      // For some unperceivable reason the server can give us an empty buffer as a map value for any type.
      // We must be able to handle this behaviour.
      const input = {'key1': Buffer.from([])};
      [2, 3].forEach(v => {
        const encoder = new Encoder(v, {});
        const buffer = encoder.encode(input, dataTypes.map);

        utils.objectValues(types.dataTypes)
          .filter(t => typeof t === "number" && !zeroLengthTypesSupported.has(t))
          .forEach(t => {
            const type = { code: types.dataTypes.map, info: [ { code: types.dataTypes.text }, { code: t } ]};
            const value = encoder.decode(buffer, type);
            assert.ok(value);
            assert.deepEqual(Object.keys(value), Object.keys(input));
            assert.strictEqual(value['key1'], null);
          });
      });
    });

    it('should decode empty buffers as nulls for most types', () => {
      const encoder = new Encoder(4, {});
      const emptyBuffer = utils.allocBufferUnsafe(0);

      utils.objectValues(types.dataTypes)
        .filter(t => typeof t === "number" && !zeroLengthTypesSupported.has(t))
        .forEach(code => assert.strictEqual(encoder.decode(emptyBuffer, { code }), null));
    });

    it('should decode empty buffers as empty strings', () => {
      const encoder = new Encoder(4, {});
      const emptyBuffer = utils.allocBufferUnsafe(0);

      [
        dataTypes.text,
        dataTypes.ascii,
        dataTypes.varchar
      ]
        .forEach(code => assert.strictEqual(encoder.decode(emptyBuffer, { code }), ''));
    });

    it('should decode empty buffers for blobs and custom types', () => {
      const encoder = new Encoder(4, {});
      const emptyBuffer = utils.allocBufferUnsafe(0);
      [
        dataTypes.blob,
        dataTypes.custom
      ]
        .forEach(t => assert.deepStrictEqual(encoder.decode(emptyBuffer, {code: t}), utils.allocBufferUnsafe(0)));
    });

    it('should decode null map values', function () {
      // technically this should not be possible as nulls are not allowed in collections.
      // at a protocol level this is only possible with v3+ as v2 uses unsigned short for collection element size.
      const encoder = new Encoder(3, {});
      const buffer = utils.allocBufferFromString('00000001000000046b657931FFFFFFFF', 'hex');
      const value = encoder.decode(buffer,
        { code: types.dataTypes.map, info: [ { code: types.dataTypes.text }, { code: types.dataTypes.text } ]});
      assert.ok(value);
      assert.deepEqual(Object.keys(value), ['key1']);
      assert.strictEqual(value['key1'], null);
    });

    it('should return the raw buffer when a buffer is provided', () => {
      const encoder = new Encoder(3, {});
      const buffer = utils.allocBufferFromArray([ 0, 1, 2, 3 ]);

      Object.keys(dataTypes).forEach(typeName => {
        assert.strictEqual(encoder.encode(buffer, dataTypes[typeName]), buffer);
      });
    });

    it('should encode string representations of numeric values', () => {
      const encoder = newInstance();
      assertBuffer(encoder.encode('3', getType(dataTypes.bigint)), 8);
      assertBuffer(encoder.encode('3', getType(dataTypes.varint)), 1);
      assertBuffer(encoder.encode('3', getType(dataTypes.int)), 4);
      assertBuffer(encoder.encode('3.375', getType(dataTypes.float)), 4);
      assertBuffer(encoder.encode('3.375', getType(dataTypes.double)), 8);
      assertBuffer(encoder.encode('3.375', getType(dataTypes.decimal)), 6);
      assertBuffer(encoder.encode(NaN, getType(dataTypes.double)), 8);
    });

    it('should encode NaN', () => {
      const encoder = newInstance();
      assertBuffer(encoder.encode(NaN, getType(dataTypes.double)), 8);
      assertBuffer(encoder.encode(NaN, getType(dataTypes.float)), 4);
    });

    it('should throw an error for invalid string representations of numeric values', () => {
      const encoder = newInstance();
      assert.throws(() => encoder.encode('hello!', getType(dataTypes.int)), TypeError);
      assert.throws(() => encoder.encode('hello!', getType(dataTypes.double)), TypeError);
      assert.throws(() => encoder.encode('hello!', getType(dataTypes.float)), TypeError);
    });
  });

  describe('#setRoutingKeyFromUser()', function () {
    const encoder = new Encoder(2, {});
    it('should concat Array of buffers in the correct format',function () {
      let options = getExecOptions({
        //Its an array of 3 values
        /** @type {Array|Buffer} */
        routingKey: [
          utils.allocBufferFromArray([1]), utils.allocBufferFromArray([2]), utils.allocBufferFromArray([3, 3])]
      });
      encoder.setRoutingKeyFromUser([1, 'text'], options);
      assert.ok(options.getRoutingKey());
      //The routingKey should have the form: [2-byte-length] + key + [0]
      // eslint-disable-next-line no-useless-concat
      assert.strictEqual(options.getRoutingKey().toString('hex'), '00010100' + '00010200' + '0002030300');

      options = getExecOptions({
        //Its an array of 1 value
        routingKey: [utils.allocBufferFromArray([1])]
      });
      encoder.setRoutingKeyFromUser([], options);
      //the result should be a single value
      assert.strictEqual(options.getRoutingKey().toString('hex'), '01');
    });

    it('should not affect Buffer routing keys', function () {
      let options = getExecOptions({
        routingKey: utils.allocBufferFromArray([1, 2, 3, 4])
      });
      const initialRoutingKey = options.getRoutingKey().toString('hex');
      encoder.setRoutingKeyFromUser([1, 'text'], options);
      assert.strictEqual(options.getRoutingKey().toString('hex'), initialRoutingKey);

      options = getExecOptions({
        routingIndexes: [1],
        routingKey: utils.allocBufferFromArray([1, 2, 3, 4])
      });
      encoder.setRoutingKeyFromUser([1, 'text'], options);
      //The routing key should take precedence over routingIndexes
      assert.strictEqual(options.getRoutingKey().toString('hex'), initialRoutingKey);
    });

    it('should not affect Token routing keys', function () {
      const token = new tokenizer.Murmur3Tokenizer().hash('4611686018427387904');
      const options = getExecOptions({
        routingIndexes: [1],
        routingKey: token
      });
      encoder.setRoutingKeyFromUser([1, 'text'], options);
      assert.strictEqual(options.getRoutingKey(), token);
    });

    it('should not affect TokenRange routing keys', function () {
      const murmur3 = new tokenizer.Murmur3Tokenizer();
      const start = murmur3.hash('-9223372036854775808');
      const end = murmur3.hash('4611686018427387904');
      const range = new token.TokenRange(start, end, murmur3);
      const options = getExecOptions({
        routingIndexes: [1],
        routingKey: range
      });
      encoder.setRoutingKeyFromUser([1, 'text'], options);
      assert.strictEqual(options.getRoutingKey(), range);
    });

    it('should build routing key based on routingIndexes', function () {
      let options = getExecOptions({
        hints: ['int'],
        routingIndexes: [0]
      });
      encoder.setRoutingKeyFromUser([1], options);
      assert.strictEqual(options.getRoutingKey().toString('hex'), '00000001');

      options = getExecOptions({
        hints: ['int', 'string', 'int'],
        routingIndexes: [0, 2]
      });
      encoder.setRoutingKeyFromUser([1, 'yeah', 2], options);
      //length1 + buffer1 + 0 + length2 + buffer2 + 0
      // eslint-disable-next-line no-useless-concat
      assert.strictEqual(options.getRoutingKey().toString('hex'), '0004' + '00000001' + '00' + '0004' + '00000002' + '00');

      options = getExecOptions({
        //less hints
        hints: ['int'],
        routingIndexes: [0, 2]
      });
      encoder.setRoutingKeyFromUser([1, 'yeah', utils.allocBufferFromArray([1, 1, 1, 1])], options);
      //length1 + buffer1 + 0 + length2 + buffer2 + 0
      // eslint-disable-next-line no-useless-concat
      assert.strictEqual(options.getRoutingKey().toString('hex'), '0004' + '00000001' + '00' + '0004' + '01010101' + '00');

      options = getExecOptions({
        //no hints
        routingIndexes: [1, 2]
      });
      encoder.setRoutingKeyFromUser([1, 'yeah', utils.allocBufferFromArray([1, 1, 1, 1])], options);
      assert.strictEqual(options.getRoutingKey().toString('hex'),
        //length1 + buffer1 + 0 + length2 + buffer2 + 0
        // eslint-disable-next-line no-useless-concat
        '0004' + utils.allocBufferFromString('yeah').toString('hex') + '00' + '0004' + '01010101' + '00');
    });

    it('should allow null/undefined routingIndexes', function () {
      const execOptions = getExecOptions({
        hints: ['int', 'text'],
        routingIndexes: [0, null, 2]
      });
      encoder.setRoutingKeyFromUser([1], execOptions);
      // It doesn't fail, it bypass routing logic
      assert.strictEqual(execOptions.getRoutingKey(), undefined);
    });

    it('should allow null or undefined routingKey parts', function () {
      let options = getExecOptions({
        routingKey: [utils.allocBufferFromArray([0]), null, utils.allocBufferFromArray([1])]
      });
      encoder.setRoutingKeyFromUser([], options);
      assert.strictEqual(options.getRoutingKey(), null);

      options = getExecOptions({
        routingKey: [utils.allocBufferFromArray([0]), undefined, utils.allocBufferFromArray([1])]
      });
      encoder.setRoutingKeyFromUser([], options);
      assert.strictEqual(options.getRoutingKey(), null);
    });

    it('should throw TypeError if invalid routingKey type is provided', function () {
      assert.throws(() => {
        encoder.setRoutingKeyFromUser([1, 'text'], getExecOptions({ routingKey: 123 }));
      }, TypeError);
    });
  });

  describe('#parseFqTypeName()', function () {
    it('should parse single type names', function () {
      const encoder = new Encoder(2, {});
      let type = encoder.parseFqTypeName('org.apache.cassandra.db.marshal.Int32Type');
      assert.strictEqual(dataTypes.int, type.code);
      type = encoder.parseFqTypeName('org.apache.cassandra.db.marshal.UUIDType');
      assert.strictEqual(dataTypes.uuid, type.code);
      type = encoder.parseFqTypeName('org.apache.cassandra.db.marshal.UTF8Type');
      assert.strictEqual(dataTypes.varchar, type.code);
      type = encoder.parseFqTypeName('org.apache.cassandra.db.marshal.BytesType');
      assert.strictEqual(dataTypes.blob, type.code);
      type = encoder.parseFqTypeName('org.apache.cassandra.db.marshal.FloatType');
      assert.strictEqual(dataTypes.float, type.code);
      type = encoder.parseFqTypeName('org.apache.cassandra.db.marshal.DoubleType');
      assert.strictEqual(dataTypes.double, type.code);
      type = encoder.parseFqTypeName('org.apache.cassandra.db.marshal.BooleanType');
      assert.strictEqual(dataTypes.boolean, type.code);
      type = encoder.parseFqTypeName('org.apache.cassandra.db.marshal.InetAddressType');
      assert.strictEqual(dataTypes.inet, type.code);
      type = encoder.parseFqTypeName('org.apache.cassandra.db.marshal.DateType');
      assert.strictEqual(dataTypes.timestamp, type.code);
      type = encoder.parseFqTypeName('org.apache.cassandra.db.marshal.TimestampType');
      assert.strictEqual(dataTypes.timestamp, type.code);
      type = encoder.parseFqTypeName('org.apache.cassandra.db.marshal.LongType');
      assert.strictEqual(dataTypes.bigint, type.code);
      type = encoder.parseFqTypeName('org.apache.cassandra.db.marshal.DecimalType');
      assert.strictEqual(dataTypes.decimal, type.code);
      type = encoder.parseFqTypeName('org.apache.cassandra.db.marshal.IntegerType');
      assert.strictEqual(dataTypes.varint, type.code);
      type = encoder.parseFqTypeName('org.apache.cassandra.db.marshal.CounterColumnType');
      assert.strictEqual(dataTypes.counter, type.code);
      type = encoder.parseFqTypeName('org.apache.cassandra.db.marshal.TimeUUIDType');
      assert.strictEqual(dataTypes.timeuuid, type.code);
      type = encoder.parseFqTypeName('org.apache.cassandra.db.marshal.AsciiType');
      assert.strictEqual(dataTypes.ascii, type.code);
    });

    it('should parse complex type names', function () {
      const encoder = new Encoder(2, {});
      let type = encoder.parseFqTypeName('org.apache.cassandra.db.marshal.ListType(org.apache.cassandra.db.marshal.Int32Type)');
      assert.strictEqual(dataTypes.list, type.code);
      assert.ok(type.info);
      assert.strictEqual(dataTypes.int, type.info.code);

      type = encoder.parseFqTypeName('org.apache.cassandra.db.marshal.SetType(org.apache.cassandra.db.marshal.UUIDType)');
      assert.strictEqual(dataTypes.set, type.code);
      assert.ok(type.info);
      assert.strictEqual(dataTypes.uuid, type.info.code);

      type = encoder.parseFqTypeName('org.apache.cassandra.db.marshal.SetType(org.apache.cassandra.db.marshal.TimeUUIDType)');
      assert.strictEqual(dataTypes.set, type.code);
      assert.ok(type.info);
      assert.strictEqual(dataTypes.timeuuid, type.info.code);

      type = encoder.parseFqTypeName('org.apache.cassandra.db.marshal.MapType(org.apache.cassandra.db.marshal.UTF8Type,org.apache.cassandra.db.marshal.LongType)');
      assert.strictEqual(dataTypes.map, type.code);
      assert.ok(Array.isArray(type.info));
      assert.strictEqual(dataTypes.varchar, type.info[0].code);
      assert.strictEqual(dataTypes.bigint, type.info[1].code);

      type = encoder.parseFqTypeName('org.apache.cassandra.db.marshal.TupleType(org.apache.cassandra.db.marshal.UTF8Type,org.apache.cassandra.db.marshal.Int32Type)');
      assert.strictEqual(dataTypes.tuple, type.code);
      assert.ok(Array.isArray(type.info));
      assert.strictEqual(dataTypes.varchar, type.info[0].code);
      assert.strictEqual(dataTypes.int, type.info[1].code);

      type = encoder.parseFqTypeName('org.apache.cassandra.db.marshal.VectorType(org.apache.cassandra.db.marshal.FloatType,10)');
      assert.strictEqual(dataTypes.custom, type.code);
      assert.ok(typeof type.info === 'object');
      assert.strictEqual(dataTypes.float, type.info[0].code);
      assert.strictEqual(10, type.info[1]);
    });

    it('should parse frozen types', function () {
      const encoder = new Encoder(2, {});
      let type = encoder.parseFqTypeName('org.apache.cassandra.db.marshal.FrozenType(org.apache.cassandra.db.marshal.ListType(org.apache.cassandra.db.marshal.TimeUUIDType))');
      assert.strictEqual(dataTypes.list, type.code);
      assert.ok(type.info);
      assert.strictEqual(dataTypes.timeuuid, type.info.code);

      type = encoder.parseFqTypeName('org.apache.cassandra.db.marshal.MapType(org.apache.cassandra.db.marshal.UTF8Type,org.apache.cassandra.db.marshal.FrozenType(org.apache.cassandra.db.marshal.ListType(org.apache.cassandra.db.marshal.Int32Type)))');
      assert.strictEqual(dataTypes.map, type.code);
      assert.ok(Array.isArray(type.info));
      assert.strictEqual(dataTypes.varchar, type.info[0].code);
      assert.strictEqual(dataTypes.list, type.info[1].code);
      const subType = type.info[1].info;
      assert.ok(subType);
      assert.strictEqual(dataTypes.int, subType.code);
    });

    it('should parse udt types', function () {
      const encoder = new Encoder(2, {});
      const typeText =
        'org.apache.cassandra.db.marshal.UserType(' +
        'tester,70686f6e65,616c696173:org.apache.cassandra.db.marshal.UTF8Type,6e756d626572:org.apache.cassandra.db.marshal.UTF8Type' +
        ')';
      const dataType = encoder.parseFqTypeName(typeText);
      assert.strictEqual(dataTypes.udt, dataType.code);
      //Udt name
      assert.ok(dataType.info);
      assert.strictEqual('phone', dataType.info.name);
      assert.strictEqual(2, dataType.info.fields.length);
      assert.strictEqual('alias', dataType.info.fields[0].name);
      assert.strictEqual(dataTypes.varchar, dataType.info.fields[0].type.code);
      assert.strictEqual('number', dataType.info.fields[1].name);
      assert.strictEqual(dataTypes.varchar, dataType.info.fields[1].type.code);
    });

    it('should parse nested udt types', function () {
      const encoder = new Encoder(2, {});
      const typeText =
        'org.apache.cassandra.db.marshal.UserType(' +
        'tester,' +
        '61646472657373,' +
        '737472656574:org.apache.cassandra.db.marshal.UTF8Type,' +
        '5a4950:org.apache.cassandra.db.marshal.Int32Type,' +
        '70686f6e6573:org.apache.cassandra.db.marshal.SetType(' +
        'org.apache.cassandra.db.marshal.UserType(' +
        'tester,' +
        '70686f6e65,' +
        '616c696173:org.apache.cassandra.db.marshal.UTF8Type,' +
        '6e756d626572:org.apache.cassandra.db.marshal.UTF8Type))' +
        ')';
      const dataType = encoder.parseFqTypeName(typeText);
      assert.strictEqual(dataTypes.udt, dataType.code);
      assert.strictEqual('address', dataType.info.name);
      assert.strictEqual('tester', dataType.info.keyspace);
      const subTypes = dataType.info.fields;
      assert.strictEqual(3, subTypes.length);
      assert.strictEqual('street,ZIP,phones', subTypes.map(function (f) {return f.name;}).join(','));
      assert.strictEqual(dataTypes.varchar, subTypes[0].type.code);
      assert.strictEqual(dataTypes.set, subTypes[2].type.code);
      //field name
      assert.strictEqual('phones', subTypes[2].name);

      const phonesSubType = subTypes[2].type.info;
      assert.strictEqual(dataTypes.udt, phonesSubType.code);
      assert.strictEqual('phone', phonesSubType.info.name);
      assert.strictEqual('tester', phonesSubType.info.keyspace);
      assert.strictEqual(2, phonesSubType.info.fields.length);
      assert.strictEqual('alias', phonesSubType.info.fields[0].name);
      assert.strictEqual('number', phonesSubType.info.fields[1].name);
    });
  });

  describe('#parseTypeName()', function () {
    it('should parse single type names', async () => {
      const encoder = new Encoder(4, {});
      /* eslint-disable no-multi-spaces */
      const items = [
        ['int',        dataTypes.int],
        ['uuid',       dataTypes.uuid],
        ['text',       dataTypes.text],
        ['varchar',    dataTypes.varchar],
        ['blob',       dataTypes.blob],
        ['float',      dataTypes.float],
        ['double',     dataTypes.double],
        ['boolean',    dataTypes.boolean],
        ['inet',       dataTypes.inet],
        ['timestamp',  dataTypes.timestamp],
        ['bigint',     dataTypes.bigint],
        ['decimal',    dataTypes.decimal],
        ['varint',     dataTypes.varint],
        ['counter',    dataTypes.counter],
        ['timeuuid',   dataTypes.timeuuid],
        ['ascii',      dataTypes.ascii]
      ];
      /* eslint-enable no-multi-spaces */

      for (const item of items) {
        const dataType = await encoder.parseTypeName('ks1', item[0], 0, null, helper.failop);
        assert.ok(dataType);
        assert.strictEqual(dataType.code, item[1]);
      }
    });

    it('should parse complex type names', async () => {
      const encoder = new Encoder(4, {});
      const items = [
        ['list<int>', dataTypes.list, dataTypes.int],
        ['set<uuid>', dataTypes.set, dataTypes.uuid],
        ['set<timeuuid>', dataTypes.set, dataTypes.timeuuid],
        ['map<varchar,bigint>', dataTypes.map, [dataTypes.varchar, dataTypes.bigint]],
        ['tuple<varchar,int>', dataTypes.tuple, [dataTypes.varchar, dataTypes.int]],
        ['frozen<list<timeuuid>>', dataTypes.list, dataTypes.timeuuid],
        ['map<text,frozen<list<int>>>', dataTypes.map, [dataTypes.text, dataTypes.list]],
        ['vector<float,20>', dataTypes.custom, {code: dataTypes.float, dimension: 20}]
      ];

      for (const item of items) {
        const dataType = await encoder.parseTypeName('ks1', item[0], 0, null, helper.failop);
        assert.ok(dataType, `Type not parsed for ${item[0]}`);
        assert.strictEqual(dataType.code, item[1]);
        assert.notEqual(dataType.info, null);
        if (Array.isArray(item[2])) {
          assert.strictEqual(dataType.info.length, item[2].length);
          dataType.info.forEach(function (childType, i) {
            // If it's an object use the code property, otherwise just use the value itself
            assert.strictEqual((childType.code || childType), item[2][i]);
          });
        }
        else if (typeof item[2] === 'object') {
          assert.strictEqual(dataType.info[0].code, item[2].code);
          assert.strictEqual(dataType.info[1], item[2].dimension);
        }
        else {
          assert.strictEqual(dataType.info.code, item[2]);
        }
      }
    });

    it('should parse nested subtypes', async () => {
      const encoder = new Encoder(4, {});
      const name = 'map<text,frozen<list<frozen<map<text,frozen<list<int>>>>>>>';
      const type = await encoder.parseTypeName('ks1', name, 0, null, helper.failop);
      assert.ok(type);
      assert.strictEqual(type.code, dataTypes.map);
      assert.strictEqual(type.info[0].code, dataTypes.text);
      assert.strictEqual(type.info[1].code, dataTypes.list);
      const subType1 = type.info[1];
      assert.strictEqual(subType1.info.code, dataTypes.map);
      const subType2 = subType1.info.info[1];
      assert.strictEqual(subType2.code, dataTypes.list);
      assert.strictEqual(subType2.info.code, dataTypes.int);
    });

    [
      true, false
    ].forEach(frozen => {
      it(`should parse udts with frozen ${frozen}`, async () => {
        const encoder = new Encoder(4, {});
        const udtResolver = sinon.spy(() => Promise.resolve('udt info dummy'));
        const name = frozen ? 'frozen<address>' : 'address';
        const type = await encoder.parseTypeName('ks_sample', name, 0, null, udtResolver);
        assert.ok(type);
        assert.strictEqual(type.code, dataTypes.udt);
        assert.strictEqual(type.options.frozen, frozen);
        assert.strictEqual(udtResolver.callCount, 1);
        assert.deepStrictEqual(udtResolver.firstCall.args, ['ks_sample', 'address']);
      });
    });

    [
      { name: 'PHONE', frozen: true },
      { name: 'PhoNe', frozen: false }
    ].forEach(({ name, frozen }) => {
      it(`should parse quoted udts with frozen ${frozen}`, async () => {
        const encoder = new Encoder(4, {});
        const udtResolver = sinon.spy(() => Promise.resolve('udt info dummy'));
        const nameParameter = frozen ? `frozen<"${name}">` : `"${name}"`;
        const type = await encoder.parseTypeName('ks_sample2', nameParameter, 0, null, udtResolver);
        assert.strictEqual(type.code, dataTypes.udt);
        assert.strictEqual(type.options.frozen, frozen);
        assert.strictEqual(udtResolver.callCount, 1);
        assert.deepStrictEqual(udtResolver.firstCall.args, ['ks_sample2', name]);
      });
    });

    it('should callback with TypeError when not found', async () => {
      const encoder = new Encoder(4, {});
      const udtResolver = sinon.spy(() => Promise.resolve(null));
      await helper.assertThrowsAsync(encoder.parseTypeName('ks2', 'WHATEVER', 0, null, udtResolver), TypeError);
      assert.ok(udtResolver.calledOnce);
    });

    it('should callback with the same error if udtResolver fails', async () => {
      const encoder = new Encoder(4, {});
      const testError = new Error('Test error');
      const udtResolver = sinon.spy(() => Promise.reject(testError));

      const err = await helper.assertThrowsAsync(encoder.parseTypeName('ks2', 'WHATEVER', 0, null, udtResolver));
      assert.strictEqual(err, testError);
    });
  });

  describe('#parseKeyTypes', function () {
    const encoder = new Encoder(1, {});
    it('should parse single type', function () {
      let value = 'org.apache.cassandra.db.marshal.UTF8Type';
      let result = encoder.parseKeyTypes(value);
      assert.strictEqual(result.types.length, 1);
      assert.strictEqual(result.types[0].code, types.dataTypes.varchar);
      value = 'org.apache.cassandra.db.marshal.TimeUUIDType';
      result = encoder.parseKeyTypes(value);
      assert.strictEqual(result.types.length, 1);
      assert.strictEqual(result.types[0].code, types.dataTypes.timeuuid);
      assert.strictEqual(result.isComposite, false);
      assert.strictEqual(result.hasCollections, false);
    });

    it('should parse composites', function () {
      const value = 'org.apache.cassandra.db.marshal.CompositeType(org.apache.cassandra.db.marshal.Int32Type,org.apache.cassandra.db.marshal.UTF8Type)';
      const result = encoder.parseKeyTypes(value);
      assert.strictEqual(result.types.length, 2);
      assert.strictEqual(result.types[0].code, types.dataTypes.int);
      assert.strictEqual(result.types[1].code, types.dataTypes.varchar);
      assert.strictEqual(result.isComposite, true);
      assert.strictEqual(result.hasCollections, false);
    });

    it('should parse composites with collection types', function () {
      const value = 'org.apache.cassandra.db.marshal.CompositeType(org.apache.cassandra.db.marshal.UTF8Type,org.apache.cassandra.db.marshal.Int32Type,org.apache.cassandra.db.marshal.ColumnToCollectionType(6c6973745f73616d706c65:org.apache.cassandra.db.marshal.ListType(org.apache.cassandra.db.marshal.Int32Type),6d61705f73616d706c65:org.apache.cassandra.db.marshal.MapType(org.apache.cassandra.db.marshal.Int32Type,org.apache.cassandra.db.marshal.Int32Type)))';
      const result = encoder.parseKeyTypes(value);
      assert.strictEqual(result.types.length, 4);
      assert.strictEqual(result.types[0].code, types.dataTypes.varchar);
      assert.strictEqual(result.types[1].code, types.dataTypes.int);
      assert.strictEqual(result.types[2].code, types.dataTypes.list);
      assert.strictEqual(result.types[3].code, types.dataTypes.map);
    });
  });

  describe('constructor', function () {
    it('should define instance members', function () {
      const encoder = new Encoder(4, {});
      assert.strictEqual(typeof encoder.encodeBlob, 'function');
      assert.strictEqual(typeof encoder.decodeBlob, 'function');
      assert.strictEqual(typeof encoder.protocolVersion, 'number');
    });
  });

  describe('prototype', function () {
    it('should only expose encode() and decode() functions', function () {
      const keys = Object.keys(Encoder.prototype);
      assert.deepStrictEqual(keys, ['decode', 'encode']);
      keys.forEach(function (k) {
        assert.strictEqual(typeof Encoder.prototype[k], 'function');
      });
    });
  });
});

function getExecOptions(options) {
  const result = ExecutionOptions.empty();
  let routingKey = options.routingKey;
  result.setRoutingKey = v => routingKey = v;
  result.getRoutingKey = () => routingKey;
  result.getHints = () => options.hints;
  result.getRoutingIndexes = () => options.routingIndexes;
  return result;
}

function newInstance(protocolVersion, clientOptions) {
  return new Encoder(protocolVersion || types.protocolVersion.maxSupported, clientOptions || {});
}

function getType(typeCode) {
  return { code: typeCode };
}

function assertBuffer(buffer, length) {
  helper.assertInstanceOf(buffer, Buffer);
  assert.strictEqual(buffer.length, length);
}