/*
 * 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 events = require('events');

const Client = require('../../lib/client');
const clientOptions = require('../../lib/client-options');
const auth = require('../../lib/auth');
const types = require('../../lib/types');
const { dataTypes } = types;
const loadBalancing = require('../../lib/policies/load-balancing');
const retry = require('../../lib/policies/retry');
const speculativeExecution = require('../../lib/policies/speculative-execution');
const timestampGeneration = require('../../lib/policies/timestamp-generation');
const Encoder = require('../../lib/encoder');
const utils = require('../../lib/utils');
const writers = require('../../lib/writers');
const OperationState = require('../../lib/operation-state');
const helper = require('../test-helper');

const contactPoints = ['a'];

describe('types', function () {
  describe('Long', function () {
    const Long = types.Long;
    it('should convert from and to Buffer', function () {
      /* eslint-disable no-multi-spaces */
      [
        //int64 decimal value    //hex value
        ['-123456789012345678', 'fe4964b459cf0cb2'],
        ['-800000000000000000', 'f4e5d43d13b00000'],
        ['-888888888888888888', 'f3aa0843dcfc71c8'],
        ['-555555555555555555', 'f84a452a6a1dc71d'],
        ['-789456',             'fffffffffff3f430'],
        ['-911111111111111144', 'f35b15458f4f8e18'],
        ['-9007199254740993',   'ffdfffffffffffff'],
        ['-1125899906842624',   'fffc000000000000'],
        ['555555555555555555',  '07b5bad595e238e3'],
        ['789456'            ,  '00000000000c0bd0'],
        ['888888888888888888',  '0c55f7bc23038e38']
      ].forEach(function (item) {
        const buffer = utils.allocBufferFromString(item[1], 'hex');
        const value = Long.fromBuffer(buffer);
        assert.strictEqual(value.toString(), item[0]);
        assert.strictEqual(Long.toBuffer(value).toString('hex'), buffer.toString('hex'),
          'Hexadecimal values should match for ' + item[1]);
      });
      /* eslint-enable no-multi-spaces */
    });

    it('should return a valid number for int greater than 2^53 and less than -2^53', function () {
      [
        new Long(0, 0x7FFFFFFF),
        new Long(0xFFFFFFFF, 0x7FFFFFFF),
        new Long(0xFFFFFFFF, 0x7FFFFF01)
      ].forEach(function (item) {
        assert.ok(item.toNumber() > Math.pow(2, 53), util.format('Value should be greater than 2^53 for %s', item));
      });
      [
        new Long(0, 0xF0000000),
        new Long(0, 0xF0000001)
      ].forEach(function (item) {
        assert.ok(item.toNumber() < Math.pow(2, 53), util.format('Value should be less than -2^53 for %s', item));
      });
    });
  });
  describe('Integer', function () {
    const Integer = types.Integer;
    /* eslint-disable no-multi-spaces */
    const values = [
      //hex value                      |      string varint
      ['02000001',                            '33554433'],
      ['02000000',                            '33554432'],
      ['1111111111111111',                    '1229782938247303441'],
      ['01',                                  '1'],
      ['0400',                                '1024'],
      ['7fffffff',                            '2147483647'],
      ['02000000000001',                      '562949953421313'],
      ['ff',                                  '-1'],
      ['ff01',                                '-255'],
      ['faa8c4',                              '-350012'],
      ['eb233d9f',                            '-350012001'],
      ['f7d9c411c4',                          '-35001200188'],
      ['f0bdc0',                              '-1000000'],
      ['ff172b5aeff4',                        '-1000000000012'],
      ['9c',                                  '-100'],
      ['c31e',                                '-15586'],
      ['00c31e',                              '49950'],
      ['0500e3c2cef9eaaab3',                  '92297829382473034419'],
      ['033171cbe0fac2d665b78d4e',            '988229782938247303441911118'],
      ['fcce8e341f053d299a4872b2',            '-988229782938247303441911118'],
      ['00b70cefb9c19c9c5112972fd01a4e676d',  '243315893003967298069149506221212854125'],
      ['00ba0cef',                            '12193007'],
      ['00ffffffff',                          '4294967295']
    ];
    /* eslint-enable no-multi-spaces */
    it('should create from buffer', function () {
      values.forEach(function (item) {
        const buffer = utils.allocBufferFromString(item[0], 'hex');
        const value = Integer.fromBuffer(buffer);
        assert.strictEqual(value.toString(), item[1]);
      });
    });
    it('should convert to buffer', function () {
      values.forEach(function (item) {
        const buffer = Integer.toBuffer(Integer.fromString(item[1]));
        assert.strictEqual(buffer.toString('hex'), item[0]);
      });
    });
  });
  describe('Tuple', function () {
    const Tuple = types.Tuple;
    describe('#get()', function () {
      it('should return the element at position', function () {
        const t = new Tuple('first', 'second');
        assert.strictEqual(t.get(0), 'first');
        assert.strictEqual(t.get(1), 'second');
        assert.strictEqual(t.get(2), undefined);
        assert.strictEqual(t.length, 2);
      });
    });
    describe('#toString()', function () {
      it('should return the string of the elements surrounded by parenthesis', function () {
        const id = types.Uuid.random();
        const decimal = types.BigDecimal.fromString('1.2');
        const t = new Tuple(id, decimal, 0);
        assert.strictEqual(t.toString(), '(' + id.toString() + ',' + decimal.toString() + ',0)');
      });
    });
    describe('#toJSON()', function () {
      it('should return the string of the elements surrounded by square brackets', function () {
        const id = types.TimeUuid.now();
        const decimal = types.BigDecimal.fromString('-1');
        const t = new Tuple(id, decimal, 1, {z: 1});
        assert.strictEqual(JSON.stringify(t), '["' + id.toString() + '","' + decimal.toString() + '",1,{"z":1}]');
      });
    });
    describe('#values()', function () {
      it('should return the Array representation of the Tuple', function () {
        const t = new Tuple('first2', 'second2', 'third2');
        assert.strictEqual(t.length, 3);
        const values = t.values();
        assert.ok(Array.isArray(values));
        assert.strictEqual(values.length, 3);
        assert.strictEqual(values[0], 'first2');
        assert.strictEqual(values[1], 'second2');
        assert.strictEqual(values[2], 'third2');
      });
      it('when modifying the returned Array the Tuple should not change its values', function () {
        const t = new Tuple('first3', 'second3', 'third3');
        const values = t.values();
        assert.strictEqual(values.length, 3);
        values[0] = 'whatever';
        values.shift();
        assert.strictEqual(t.get(0), 'first3');
        assert.strictEqual(t.get(1), 'second3');
        assert.strictEqual(t.get(2), 'third3');
      });
    });

    describe('fromArray()', () => {
      it('should return a Tuple instance using the Array items as elements', () => {
        [
          [ 'a' ],
          [ 'a', 'b' ],
          [ 'a', 'b', 'c' ]
        ].forEach(items => {
          const tuple = Tuple.fromArray(items);
          // The Array instance should not be the same
          assert.notStrictEqual(tuple.elements, items);
          // The elements should be the same
          assert.deepStrictEqual(tuple.elements, items);
        });
      });
    });

  });
  describe('LocalDate', function () {
    const LocalDate = types.LocalDate;
    describe('new LocalDate', function (){
      it('should refuse to create LocalDate from invalid values.', function () {
        assert.throws(() => new types.LocalDate(), Error);
        assert.throws(() => new types.LocalDate(undefined), Error);
        // Outside of ES5 Date range.
        assert.throws(() => new types.LocalDate(-271821, 4, 19), Error);
        assert.throws(() => new types.LocalDate(275760, 9, 14), Error);
        // Outside of LocalDate range.
        assert.throws(() => new types.LocalDate(-2147483649), Error);
        assert.throws(() => new types.LocalDate(2147483648), Error);

      });
    });
    describe('#toString()', function () {
      it('should return the string in the form of yyyy-mm-dd', function () {
        assert.strictEqual(new LocalDate(2015, 2, 1).toString(), '2015-02-01');
        assert.strictEqual(new LocalDate(2015, 12, 13).toString(), '2015-12-13');
        assert.strictEqual(new LocalDate(101, 12, 14).toString(), '0101-12-14');
        assert.strictEqual(new LocalDate(-100, 11, 6).toString(), '-0100-11-06');
      });
    });
    describe('#fromBuffer() and #toBuffer()', function () {
      it('should encode and decode a LocalDate', function () {
        const value = new LocalDate(2010, 8, 5);
        const encoded = value.toBuffer();
        const decoded = LocalDate.fromBuffer(encoded);
        assert.strictEqual(decoded.toString(), value.toString());
        assert.ok(decoded.equals(value));
        assert.ok(value.equals(decoded));
      });
    });
    describe('#fromString()', function () {
      it('should parse the string representation as yyyy-mm-dd', function () {
        [
          ['1200-12-30', 1200, 12, 30],
          ['1-1-1', 1, 1, 1],
          ['21-2-1', 21, 2, 1],
          ['-21-2-1', -21, 2, 1],
          ['2010-4-29', 2010, 4, 29],
          ['-199-06-30', -199, 6, 30],
          ['1201-04-03', 1201, 4, 3],
          ['-1201-04-03', -1201, 4, 3],
          ['0-1-1', 0, 1, 1]
        ].forEach(function (item) {
          const value = LocalDate.fromString(item[0]);
          assert.strictEqual(value.year, item[1]);
          assert.strictEqual(value.month, item[2]);
          assert.strictEqual(value.day, item[3]);
        });
      });
      it('should parse the string representation as since epoch days', function () {
        [
          ['0', '1970-01-01'],
          ['1', '1970-01-02'],
          ['2147483647', '2147483647'],
          ['-2147483648', '-2147483648'],
          ['-719162', '0001-01-01']
        ].forEach(function (item) {
          const value = LocalDate.fromString(item[0]);
          assert.strictEqual(value.toString(), item[1]);
        });
      });
      it('should throw when string representation is invalid', function () {
        [
          '',
          '1880-1',
          '1880-1-z',
          undefined,
          null,
          '  '
        ].forEach(function (value) {
          assert.throws(function () {
            LocalDate.fromString(value);
          });
        });
      });
    });
  });
  describe('LocalTime', function () {
    const LocalTime = types.LocalTime;
    const Long = types.Long;
    /* eslint-disable no-multi-spaces */
    const values = [
      //Long value         |     string representation  |   hour/min/sec/nanos
      ['1000000001',             '00:00:01.000000001',      [0, 0, 1, 1]],
      ['0',                      '00:00:00',                [0, 0, 0, 0]],
      ['3600000006001',          '01:00:00.000006001',      [1, 0, 0, 6001]],
      ['61000000000',            '00:01:01',                [0, 1, 1, 0]],
      ['610000030000',           '00:10:10.00003',          [0, 10, 10, 30000]],
      ['52171800000000',         '14:29:31.8',              [14, 29, 31, 800000000]],
      ['52171800600000',         '14:29:31.8006',           [14, 29, 31, 800600000]]
    ];
    /* eslint-enable no-multi-spaces */
    describe('new LocalTime', function () {
      it('should refuse to create LocalTime from invalid values.', function () {
        // Not a long.
        assert.throws(() => new types.LocalTime(23.0), Error);
        // < 0
        assert.throws(() => new types.LocalTime(types.Long(-1)), Error);
        // > maxNanos
        assert.throws(() => new types.LocalTime(Long.fromString('86400000000000')), Error);
      });
    });
    describe('#toString()', function () {
      it('should return the string representation', function () {
        values.forEach(function (item) {
          const val = new LocalTime(Long.fromString(item[0]));
          assert.strictEqual(val.toString(), item[1]);
        });
      });
    });
    describe('#toJSON()', function () {
      it('should return the string representation', function () {
        values.forEach(function (item) {
          const val = new LocalTime(Long.fromString(item[0]));
          assert.strictEqual(val.toString(), item[1]);
        });
      });
    });
    describe('#fromString()', function () {
      it('should parse the string representation', function () {
        values.forEach(function (item) {
          const val = LocalTime.fromString(item[1]);
          assert.ok(new LocalTime(Long.fromString(item[0])).equals(val));
          assert.ok(new LocalTime(Long.fromString(item[0]))
            .getTotalNanoseconds()
            .equals(val.getTotalNanoseconds()));
        });
      });
    });
    describe('#toBuffer() and fromBuffer()', function () {
      values.forEach(function (item) {
        const val = new LocalTime(Long.fromString(item[0]));
        const encoded = val.toBuffer();
        const decoded = LocalTime.fromBuffer(encoded);
        assert.ok(decoded.equals(val));
        assert.strictEqual(val.toString(), decoded.toString());
      });
    });
    describe('#hour #minute #second #nanosecond', function () {
      it('should get the correct parts', function () {
        values.forEach(function (item) {
          const val = new LocalTime(Long.fromString(item[0]));
          const parts = item[2];
          assert.strictEqual(val.hour, parts[0]);
          assert.strictEqual(val.minute, parts[1]);
          assert.strictEqual(val.second, parts[2]);
          assert.strictEqual(val.nanosecond, parts[3]);
        });
      });
    });
    describe('fromDate()', function () {
      it('should use the local time', function () {
        const date = new Date();
        const time = LocalTime.fromDate(date, 1);
        assert.strictEqual(time.hour, date.getHours());
        assert.strictEqual(time.minute, date.getMinutes());
        assert.strictEqual(time.second, date.getSeconds());
        assert.strictEqual(time.nanosecond, date.getMilliseconds() * 1000000 + 1);
      });
    });
    describe('fromMilliseconds', function () {
      it('should default nanoseconds to 0 when not provided', function () {
        const time = LocalTime.fromMilliseconds(1);
        assert.ok(time.equals(LocalTime.fromMilliseconds(1, 0)));
      });
    });
  });
  describe('ResultStream', function () {
    it('should be readable as soon as it has data', function (done) {
      const buf = [];
      const stream = new types.ResultStream();
      
      stream.on('end', function streamEnd() {
        assert.strictEqual(Buffer.concat(buf).toString(), 'Jimmy McNulty');
        done();
      });
      stream.on('readable', function streamReadable() {
        let item;
        while ((item = stream.read())) {
          buf.push(item);
        }
      });
      stream.add(utils.allocBufferFromString('Jimmy'));
      stream.add(utils.allocBufferFromString(' '));
      stream.add(utils.allocBufferFromString('McNulty'));
      stream.add(null);
    });

    it('should buffer until is read', function (done) {
      const buf = [];
      const stream = new types.ResultStream();
      stream.add(utils.allocBufferFromString('Stringer'));
      stream.add(utils.allocBufferFromString(' '));
      stream.add(utils.allocBufferFromString('Bell'));
      stream.add(null);

      stream.on('end', function streamEnd() {
        assert.equal(Buffer.concat(buf).toString(), 'Stringer Bell');
        done();
      });
      stream.on('readable', function streamReadable() {
        let item;
        while ((item = stream.read())) {
          buf.push(item);
        }
      });
    });

    it('should be readable until the end', function (done) {
      const buf = [];
      const stream = new types.ResultStream();
      stream.add(utils.allocBufferFromString('Omar'));
      stream.add(utils.allocBufferFromString(' '));

      stream.on('end', function streamEnd() {
        assert.equal(Buffer.concat(buf).toString(), 'Omar Little');
        done();
      });
      stream.on('readable', function streamReadable() {
        let item;
        while ((item = stream.read())) {
          buf.push(item);
        }
      });

      stream.add(utils.allocBufferFromString('Little'));
      stream.add(null);
    });

    it('should be readable on objectMode', function (done) {
      const buf = [];
      const stream = new types.ResultStream({objectMode: true});
      //passing objects
      stream.add({toString: function (){return 'One';}});
      stream.add({toString: function (){return 'Two';}});
      stream.add(null);
      stream.on('end', function streamEnd() {
        assert.equal(buf.join(' '), 'One Two');
        done();
      });
      stream.on('readable', function streamReadable() {
        let item;
        while ((item = stream.read())) {
          buf.push(item);
        }
      });
    });
  });
  describe('Row', function () {
    it('should get the value by column name or index', function () {
      const columns = [{name: 'first', type: { code: dataTypes.varchar}}, {name: 'second', type: { code: dataTypes.varchar}}];
      const row = new types.Row(columns);
      row['first'] = 'hello';
      row['second'] = 'world';
      assert.ok(row.get, 'It should contain a get method');
      assert.strictEqual(row['first'], 'hello');
      assert.strictEqual(row.get('first'), row['first']);
      assert.strictEqual(row.get(0), row['first']);
      assert.strictEqual(row.get('second'), row['second']);
      assert.strictEqual(row.get(1), row['second']);
    });
    it('should enumerate only columns defined', function () {
      const columns = [{name: 'col1', type: { code: dataTypes.varchar}}, {name: 'col2', type: { code: dataTypes.varchar}}];
      const row = new types.Row(columns);
      row['col1'] = 'val1';
      row['col2'] = 'val2';
      assert.strictEqual(JSON.stringify(row), JSON.stringify({col1: 'val1', col2: 'val2'}));
    });
    it('should be serializable to json', function () {
      let i;
      let columns = [{name: 'col1', type: { code: dataTypes.varchar}}, {name: 'col2', type: { code: dataTypes.varchar}}];
      let row = new types.Row(columns, [utils.allocBufferFromString('val1'), utils.allocBufferFromString('val2')]);
      row['col1'] = 'val1';
      row['col2'] = 'val2';
      assert.strictEqual(JSON.stringify(row), JSON.stringify({col1: 'val1', col2: 'val2'}));

      columns = [
        {name: 'cid', type: { code: dataTypes.uuid}},
        {name: 'ctid', type: { code: dataTypes.timeuuid}},
        {name: 'clong', type: { code: dataTypes.bigint}},
        {name: 'cvarint', type: { code: dataTypes.varint}}
      ];
      let rowValues = [
        types.Uuid.random(),
        types.TimeUuid.now(),
        types.Long.fromNumber(1000),
        types.Integer.fromNumber(22)
      ];
      row = new types.Row(columns);
      for (i = 0; i < columns.length; i++) {
        row[columns[i].name] = rowValues[i];
      }
      let expected = util.format('{"cid":"%s","ctid":"%s","clong":"1000","cvarint":"22"}',
        rowValues[0].toString(), rowValues[1].toString());
      assert.strictEqual(JSON.stringify(row), expected);
      rowValues = [
        types.BigDecimal.fromString("1.762"),
        new types.InetAddress(utils.allocBufferFromArray([192, 168, 0, 1])),
        null];
      columns = [
        {name: 'cdecimal', type: { code: dataTypes.decimal}},
        {name: 'inet1', type: { code: dataTypes.inet}},
        {name: 'inet2', type: { code: dataTypes.inet}}
      ];
      row = new types.Row(columns);
      for (i = 0; i < columns.length; i++) {
        row[columns[i].name] = rowValues[i];
      }
      expected = '{"cdecimal":"1.762","inet1":"192.168.0.1","inet2":null}';
      assert.strictEqual(JSON.stringify(row), expected);
    });
    it('should have values that can be inspected', function () {
      const columns = [{name: 'col10', type: { code: dataTypes.varchar}}, {name: 'col2', type: { code: dataTypes.int}}];
      const row = new types.Row(columns);
      row['col10'] = 'val1';
      row['col2'] = 2;
      helper.assertContains(util.inspect(row), util.inspect({col10: 'val1', col2: 2}));
    });
  });
  describe('uuid() backward-compatibility', function () {
    it('should generate a random string uuid', function () {
      const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
      const val = types.uuid();
      assert.strictEqual(typeof val, 'string');
      assert.strictEqual(val.length, 36);
      assert.ok(uuidRegex.test(val));
      assert.notEqual(val, types.uuid());
    });
    it('should fill in the values in a buffer', function () {
      const buf = utils.allocBufferUnsafe(16);
      const val = types.uuid(null, buf);
      assert.strictEqual(val, buf);
    });
  });
  describe('timeuuid() backward-compatibility', function () {
    it('should generate a string uuid', function () {
      const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
      const val = types.timeuuid();
      assert.strictEqual(typeof val, 'string');
      assert.strictEqual(val.length, 36);
      assert.ok(uuidRegex.test(val));
      assert.notEqual(val, types.timeuuid());
    });
    it('should fill in the values in a buffer', function () {
      const buf = utils.allocBufferUnsafe(16);
      const val = types.timeuuid(null, buf);
      assert.strictEqual(val, buf);
    });
  });
  describe('generateTimestamp()', function () {
    it('should generate using date and microseconds parts', function () {
      let date = new Date();
      let value = types.generateTimestamp(date, 123);
      assert.instanceOf(value, types.Long);
      assert.strictEqual(value.toString(), types.Long
        .fromNumber(date.getTime())
        .multiply(types.Long.fromInt(1000))
        .add(types.Long.fromInt(123))
        .toString());

      date = new Date('2010-04-29');
      value = types.generateTimestamp(date, 898);
      assert.instanceOf(value, types.Long);
      assert.strictEqual(value.toString(), types.Long
        .fromNumber(date.getTime())
        .multiply(types.Long.fromInt(1000))
        .add(types.Long.fromInt(898))
        .toString());
    });
  });
});
describe('utils', function () {
  describe('#extend()', function () {
    it('should allow null sources', function () {
      const originalObject = {};
      const extended = utils.extend(originalObject, null);
      assert.strictEqual(originalObject, extended);
    });
  });
  describe('#funcCompare()', function () {
    it('should return a compare function valid for Array#sort', function () {
      const values = [
        {id: 1, getValue : () => 100},
        {id: 2, getValue : () => 3},
        {id: 3, getValue : () => 1}
      ];
      values.sort(utils.funcCompare('getValue'));
      assert.strictEqual(values[0].id, 3);
      assert.strictEqual(values[1].id, 2);
      assert.strictEqual(values[2].id, 1);
    });
  });
  describe('#binarySearch()', function () {
    it('should return the key index if found, or the bitwise compliment of the first larger value', function () {
      const compareFunc = function (a, b) {
        if (a > b) {
          return 1;
        }
        if (a < b) {
          return -1;
        }
        return 0;
      };
      let val;
      val = utils.binarySearch([0, 1, 2, 3, 4], 2, compareFunc);
      assert.strictEqual(val, 2);
      val = utils.binarySearch(['A', 'B', 'C', 'D', 'E'], 'D', compareFunc);
      assert.strictEqual(val, 3);
      val = utils.binarySearch(['A', 'B', 'C', 'D', 'Z'], 'M', compareFunc);
      assert.strictEqual(val, ~4);
      val = utils.binarySearch([0, 1, 2, 3, 4], 2.5, compareFunc);
      assert.strictEqual(val, ~3);
    });
  });
  describe('#deepExtend', function () {
    it('should override only the most inner props', function () {
      let value;
      //single values
      value = utils.deepExtend({}, {a: '1'});
      assert.strictEqual(value.a, '1');
      value = utils.deepExtend({a: '2'}, {a: '1'});
      assert.strictEqual(value.a, '1');
      value = utils.deepExtend({a: new Date()}, {a: new Date(100)});
      assert.strictEqual(value.a.toString(), new Date(100).toString());
      value = utils.deepExtend({a: 2}, {a: 1});
      assert.strictEqual(value.a, 1);
      //composed 1 level
      value = utils.deepExtend({a: { a1: 1, a2: 2}, b: 1000}, {a: {a2: 15}});
      assert.strictEqual(value.a.a2, 15);
      assert.strictEqual(value.a.a1, 1);
      assert.strictEqual(value.b, 1000);
      //composed 2 level
      value = utils.deepExtend({a: { a1: 1, a2: { a21: 10, a22: 20}}}, {a: {a2: {a21: 11}}, b: { b1: 100, b2: 200}});
      assert.strictEqual(value.a.a2.a21, 11);
      assert.strictEqual(value.a.a2.a22, 20);
      assert.strictEqual(value.a.a1, 1);
      assert.strictEqual(value.b.b1, 100);
      assert.strictEqual(value.b.b2, 200);
      //multiple sources
      value = utils.deepExtend({z: 9}, {a: { a1: 1, a2: { a21: 10, a22: 20}}}, {a: {a2: {a21: 11}}, b: { b1: 100, b2: 200}});
      assert.strictEqual(value.a.a2.a21, 11);
      assert.strictEqual(value.a.a2.a22, 20);
      assert.strictEqual(value.a.a1, 1);
      assert.strictEqual(value.b.b1, 100);
      assert.strictEqual(value.b.b2, 200);
      assert.strictEqual(value.z, 9);
      //!source
      value = utils.deepExtend({z: 3}, null);
      assert.strictEqual(value.z, 3);
      //undefined
      const o = undefined;
      value = utils.deepExtend({z: 4}, o);
      assert.strictEqual(value.z, 4);
    });
  });
});
describe('clientOptions', function () {
  describe('#extend()', function () {
    it('should require contactPoints', function () {
      assert.doesNotThrow(function () {
        clientOptions.extend({contactPoints: ['host1', 'host2']});
      });
      assert.throws(function () {
        clientOptions.extend({contactPoints: {}});
      });
      assert.throws(function () {
        clientOptions.extend({});
      });
      assert.throws(function () {
        clientOptions.extend(null);
      });
      assert.throws(function () {
        clientOptions.extend(undefined);
      });
    });
    it('should create a new instance', function () {
      const a = {contactPoints: ['host1']};
      let options = clientOptions.extend(a);
      assert.notStrictEqual(a, options);
      assert.notStrictEqual(options, clientOptions.defaultOptions());
      //it should use baseOptions as source
      const b = {};
      options = clientOptions.extend(b, a);
      //B is the instance source
      assert.strictEqual(b, options);
      //A is the target
      assert.notStrictEqual(a, options);
      assert.notStrictEqual(options, clientOptions.defaultOptions());
    });
    it('should validate the policies', function () {
      const policy1 = new loadBalancing.RoundRobinPolicy();
      const policy2 = new retry.RetryPolicy();
      const options = clientOptions.extend({
        contactPoints: ['host1'],
        policies: {
          loadBalancing: policy1,
          retry: policy2
        }
      });
      assert.strictEqual(options.policies.loadBalancing, policy1);
      assert.strictEqual(options.policies.retry, policy2);

      assert.throws(function () {
        clientOptions.extend({
          contactPoints: ['host1'],
          policies: {
            loadBalancing: {}
          }
        });
      });
      assert.throws(function () {
        clientOptions.extend({
          contactPoints: ['host1'],
          policies: {
            //Use whatever object
            loadBalancing: new (function C1() {})()
          }
        });
      });
      assert.throws(function () {
        clientOptions.extend({
          contactPoints: ['host1'],
          policies: {
            //Use whatever object
            retry: new (function C2() {})()
          }
        });
      });
    });
    it('should validate the encoding options', function () {
      function DummyConstructor() {}
      assert.doesNotThrow(function () {
        clientOptions.extend({
          contactPoints: ['host1'],
          encoding: {}
        });
      });
      assert.doesNotThrow(function () {
        clientOptions.extend({
          contactPoints: ['host1'],
          encoding: { map: helper.Map}
        });
      });
      assert.throws(function () {
        clientOptions.extend({
          contactPoints: ['host1'],
          encoding: { map: 1}
        });
      });
      assert.throws(function () {
        clientOptions.extend({
          contactPoints: ['host1'],
          encoding: { map: DummyConstructor}
        });
      });
    });
    it('should validate protocolOptions.maxVersion', function () {
      assert.throws(function () {
        clientOptions.extend({
          contactPoints: ['host1'],
          protocolOptions: { maxVersion: '1' }
        });
      }, TypeError);
      assert.throws(function () {
        clientOptions.extend({
          contactPoints: ['host1'],
          protocolOptions: { maxVersion: 16 }
        });
      }, TypeError);
    });
    it('should validate credentials', () => {
      const message = /credentials username and password must be a string/;

      assert.throws(() => clientOptions.extend({ contactPoints, credentials: {} }), message);

      assert.throws(() => clientOptions.extend({ contactPoints, credentials: { username: 'a' } }), message);

      assert.throws(() => clientOptions.extend({ contactPoints, credentials: { password: 'b' } }), message);

      assert.doesNotThrow(() => clientOptions.extend({
        contactPoints, credentials: { username: 'a', password: 'b' }
      }));
    });
    it('should validate authProvider', () => {
      const message = /options\.authProvider must be an instance of AuthProvider/;

      assert.throws(() => clientOptions.extend({ contactPoints, authProvider: {} }), message);

      assert.throws(() => clientOptions.extend({ contactPoints, authProvider: true }), message);

      assert.throws(() => clientOptions.extend({ contactPoints, authProvider: 'abc' }), message);

      assert.doesNotThrow(() => clientOptions.extend({
        contactPoints, authProvider: new auth.PlainTextAuthProvider('a', 'b')
      }));

      assert.doesNotThrow(() => clientOptions.extend({ contactPoints, authProvider: null }));

      assert.doesNotThrow(() => clientOptions.extend({ contactPoints, authProvider: undefined }));
    });
    it('should use the authProvider when both credentials and authProvider are defined', () => {
      const authProvider = new auth.PlainTextAuthProvider('a', 'b');

      const options = clientOptions.extend({
        contactPoints, authProvider, credentials: { username: 'c', password: 'd' }
      });

      assert.strictEqual(options.authProvider, authProvider);
    });
  });
  describe('#defaultOptions()', function () {
    const options = clientOptions.defaultOptions();
    it('should set not set the default consistency level', function () {
      assert.strictEqual(options.queryOptions.consistency, undefined);
    });
    it('should set True to warmup option', function () {
      assert.strictEqual(options.pooling.warmup, true);
    });
    it('should set 12secs as default read timeout', function () {
      assert.strictEqual(12000, options.socketOptions.readTimeout);
    });
    it('should set useUndefinedAsUnset as true', function () {
      assert.strictEqual(true, options.encoding.useUndefinedAsUnset);
    });
  });
});

describe('writers', function () {
  describe('WriteQueue', function () {
    it('should buffer until threshold is passed', async () => {
      const coalescingThreshold = 50;
      const buffers = [];
      const socketMock = {
        write: function (buf, cb) {
          buffers.push(buf);
          setImmediate(cb);
          return true;
        },
        on: utils.noop
      };

      const options = utils.extend({}, clientOptions.defaultOptions());
      options.socketOptions.coalescingThreshold = coalescingThreshold;
      const encoder = new Encoder(3, options);
      const queue = new writers.WriteQueue(socketMock, encoder, options);
      const request = {
        write: () => utils.allocBufferUnsafe(10)
      };

      const itemCallback = sinon.spy(() => {});

      for (let i = 0; i < 10; i++) {
        queue.push(new OperationState(request, null, utils.noop), itemCallback);
      }

      await helper.wait.until(() => itemCallback.callCount === 10);

      // 10 frames written
      assert.strictEqual(itemCallback.callCount, 10);
      // 10 frames coalesced into 2 buffers of 50b each
      assert.strictEqual(buffers.length, 2);
      buffers.forEach(b => assert.strictEqual(b.length, coalescingThreshold));
    });

    it('should stop writing and wait for the drain event when socket.write() returns false', async () => {
      let writeReturnValue = true;

      class SocketMock extends events.EventEmitter {
        write(buffer, cb) {
          setImmediate(cb);
          return writeReturnValue;
        }
      }

      const socket = sinon.spy(new SocketMock());
      const options = clientOptions.defaultOptions();
      const encoder = new Encoder(types.protocolVersion.v4, options);
      const queue = new writers.WriteQueue(socket, encoder, options);
      const request = {
        write: () => utils.allocBufferUnsafe(10)
      };

      const itemCallback = sinon.spy(() => {});

      queue.push(new OperationState(request, null, utils.noop), itemCallback);
      await helper.wait.until(() => itemCallback.callCount === 1);

      // Set socket.write() return value to false
      writeReturnValue = false;
      queue.push(new OperationState(request, null, utils.noop), itemCallback);
      await helper.wait.until(() => itemCallback.callCount === 2);
      assert.strictEqual(socket.write.callCount, 2);

      // Next time an item is added to the queue, it will not be processed.
      queue.push(new OperationState(request, null, utils.noop), itemCallback);

      // After some time, write should not have been called
      await helper.delayAsync(20);
      assert.strictEqual(socket.write.callCount, 2);
      assert.strictEqual(itemCallback.callCount, 2);

      // It should wait for the drain event to be emitted
      socket.emit('drain');

      // After some ticks, socket.write() should be called again
      await helper.wait.until(() => itemCallback.callCount === 3);
      assert.strictEqual(socket.write.callCount, 3);
    });
  });
});

describe('exports', function () {
  it('should contain API', function () {
    //test that the exposed API is the one expected
    //it looks like a dumb test and it is, but it is necessary!
    /* eslint-disable global-require */
    const api = require('../../index.js');
    assert.strictEqual(api.Client, Client);
    assert.ok(api.errors);
    assert.strictEqual(typeof api.errors.DriverError, 'function');
    assert.strictEqual(typeof api.ExecutionProfile, 'function');
    assert.strictEqual(api.ExecutionProfile.name, 'ExecutionProfile');
    assert.strictEqual(typeof api.ExecutionOptions, 'function');
    assert.strictEqual(api.ExecutionOptions.name, 'ExecutionOptions');
    assert.ok(api.types);
    assert.ok(api.policies);
    assert.ok(api.auth);
    assert.ok(typeof api.auth.AuthProvider, 'function');
    //policies modules
    assert.strictEqual(api.policies.loadBalancing, loadBalancing);
    assert.strictEqual(typeof api.policies.loadBalancing.LoadBalancingPolicy, 'function');
    assert.instanceOf(api.policies.defaultLoadBalancingPolicy(), api.policies.loadBalancing.LoadBalancingPolicy);
    assert.strictEqual(api.policies.retry, retry);
    assert.strictEqual(typeof api.policies.retry.RetryPolicy, 'function');
    assert.strictEqual(typeof api.policies.retry.IdempotenceAwareRetryPolicy, 'function');
    assert.instanceOf(api.policies.defaultRetryPolicy(), api.policies.retry.RetryPolicy);
    assert.strictEqual(api.policies.reconnection, require('../../lib/policies/reconnection'));
    assert.strictEqual(typeof api.policies.reconnection.ReconnectionPolicy, 'function');
    assert.instanceOf(api.policies.defaultReconnectionPolicy(), api.policies.reconnection.ReconnectionPolicy);
    assert.strictEqual(api.policies.speculativeExecution, speculativeExecution);
    assert.strictEqual(typeof speculativeExecution.NoSpeculativeExecutionPolicy, 'function');
    assert.strictEqual(typeof speculativeExecution.ConstantSpeculativeExecutionPolicy, 'function');
    assert.strictEqual(typeof speculativeExecution.SpeculativeExecutionPolicy, 'function');
    assert.strictEqual(api.policies.timestampGeneration, timestampGeneration);
    assert.strictEqual(typeof timestampGeneration.TimestampGenerator, 'function');
    assert.strictEqual(typeof timestampGeneration.MonotonicTimestampGenerator, 'function');
    assert.instanceOf(api.policies.defaultTimestampGenerator(), timestampGeneration.MonotonicTimestampGenerator);
    assert.strictEqual(api.auth, require('../../lib/auth'));

    // mapping module
    assert.ok(api.mapping);
    assertConstructorExposed(api.mapping, api.mapping.TableMappings);
    assertConstructorExposed(api.mapping, api.mapping.DefaultTableMappings);
    assertConstructorExposed(api.mapping, api.mapping.UnderscoreCqlToCamelCaseMappings);
    assertConstructorExposed(api.mapping, api.mapping.Mapper);
    assertConstructorExposed(api.mapping, api.mapping.ModelMapper);
    assertConstructorExposed(api.mapping, api.mapping.ModelBatchItem);
    assertConstructorExposed(api.mapping, api.mapping.ModelBatchMapper);
    assertConstructorExposed(api.mapping, api.mapping.Result);
    assert.ok(api.mapping.q);
    assert.strictEqual(typeof api.mapping.q.in_, 'function');

    //metadata module with classes
    assert.ok(api.metadata);
    assert.strictEqual(typeof api.metadata.Metadata, 'function');
    assert.strictEqual(api.metadata.Metadata, require('../../lib/metadata'));
    assert.ok(api.Encoder);
    assert.strictEqual(typeof api.Encoder, 'function');
    assert.strictEqual(api.Encoder, require('../../lib/encoder'));
    assert.ok(api.defaultOptions());
    assert.strictEqual(api.tracker, require('../../lib/tracker'));
    assert.strictEqual(typeof api.tracker.RequestTracker, 'function');
    assert.strictEqual(typeof api.tracker.RequestLogger, 'function');

    assert.ok(api.metrics);
    assert.strictEqual(typeof api.metrics.ClientMetrics, 'function');
    assert.strictEqual(api.metrics.ClientMetrics.name, 'ClientMetrics');
    assert.strictEqual(typeof api.metrics.DefaultMetrics, 'function');
    assert.strictEqual(api.metrics.DefaultMetrics.name, 'DefaultMetrics');

    assert.ok(api.concurrent);
    assert.strictEqual(typeof api.concurrent.executeConcurrent, 'function');
    /* eslint-enable global-require */
  });
});

function assertConstructorExposed(obj, constructorRef) {
  assert.ok(obj);
  assert.strictEqual(typeof constructorRef, 'function');
  // Verify that is exposed with the same name as the class
  assert.strictEqual(obj[constructorRef.name], constructorRef);
}