/* jshint node: true, mocha: true */

/**
 *  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
 *
 *  https://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';

var utils = require('../lib/utils'),
    schemas = require('../lib/schemas'),
    assert = require('assert'),
    util = require('util');

var Tap = utils.Tap;
var createType = schemas.createType;
var types = schemas.types;


describe('types', function () {

  describe('BooleanType', function () {

    var data = [
      {
        valid: [true, false],
        invalid: [null, 'hi', undefined, 1.5, 1e28, 123124123123213]
      }
    ];

    testType(types.BooleanType, data);

    it('to JSON', function () {
      var t = new types.BooleanType();
      assert.equal(t.toJSON(), 'boolean');
    });

    it('compare buffers', function () {
      var t = new types.BooleanType();
      var bt = t.toBuffer(true);
      var bf = t.toBuffer(false);
      assert.equal(t.compareBuffers(bt, bf), 1);
      assert.equal(t.compareBuffers(bf, bt), -1);
      assert.equal(t.compareBuffers(bt, bt), 0);
    });

    it('get name', function () {
      var t = new types.BooleanType();
      assert.strictEqual(t.getName(), undefined);
      assert.equal(t.getName(true), 'boolean');
    });

  });

  describe('IntType', function () {

    var data = [
      {
        valid: [1, -3, 12314, 0, 1e9],
        invalid: [null, 'hi', undefined, 1.5, 1e28, 123124123123213]
      }
    ];

    testType(types.IntType, data);

    it('toBuffer int', function () {

      var type = createType('int');
      assert.equal(type.fromBuffer(Buffer.from([0x80, 0x01])), 64);
      assert(Buffer.from([0]).equals(type.toBuffer(0)));

    });

    it('resolve int > long', function () {
      var intType = createType('int');
      var longType = createType('long');
      var buf = intType.toBuffer(123);
      assert.equal(
        longType.fromBuffer(buf, longType.createResolver(intType)),
        123
      );
    });

    it('resolve int > [null, int]', function () {
      var wt = createType('int');
      var rt = createType(['null', 'int']);
      var buf = wt.toBuffer(123);
      assert.deepEqual(
        rt.fromBuffer(buf, rt.createResolver(wt)),
        {'int': 123}
      );
    });

    it('resolve int > float', function () {
      var wt = createType('int');
      var rt = createType('float');
      var buf = wt.toBuffer(123);
      assert.deepEqual(rt.fromBuffer(buf, rt.createResolver(wt)), 123);
    });

    it('resolve int > double', function () {
      var wt = createType('int');
      var rt = createType('double');
      var n = Math.pow(2, 30) + 1;
      var buf = wt.toBuffer(n);
      assert.deepEqual(rt.fromBuffer(buf, rt.createResolver(wt)), n);
    });

    it('toString', function () {
      assert.equal(createType('int').toString(), '"int"');
    });

    it('clone', function () {
      var t = createType('int');
      assert.equal(t.clone(123), 123);
      assert.throws(function () { t.clone(''); });
    });

    it('resolve invalid', function () {
      assert.throws(function () { getResolver('int', 'long'); });
    });

  });

  describe('LongType', function () {

    var data = [
      {
        valid: [1, -3, 12314, 9007199254740990, 900719925474090],
        invalid: [null, 'hi', undefined, 9007199254740991, 1.3, 1e67]
      }
    ];

    testType(types.LongType, data);

    it('resolve invalid', function () {
      assert.throws(function () { getResolver('long', 'double'); });
    });

    it('resolve long > float', function () {
      var t1 = createType('long');
      var t2 = createType('float');
      var n = 9007199254740990; // Number.MAX_SAFE_INTEGER - 1
      var buf = t1.toBuffer(n);
      var f = t2.fromBuffer(buf, t2.createResolver(t1));
      assert(Math.abs(f - n) / n < 1e-7);
      assert(t2.isValid(f));
    });

    it('precision loss', function () {
      var type = createType('long');
      var buf = Buffer.from([0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x20]);
      assert.throws(function () { type.fromBuffer(buf); });
    });

    it('using missing methods', function () {
      assert.throws(function () { types.LongType.using(); });
    });

  });

  describe('StringType', function () {

    var data = [
      {
        valid: ['', 'hi'],
        invalid: [null, undefined, 1, 0]
      }
    ];

    testType(types.StringType, data);

    it('fromBuffer string', function () {
      var type = createType('string');
      var buf = Buffer.from([0x06, 0x68, 0x69, 0x21]);
      var s = 'hi!';
      assert.equal(type.fromBuffer(buf), s);
      assert(buf.equals(type.toBuffer(s)));
    });

    it('toBuffer string', function () {
      var type = createType('string');
      var buf = Buffer.from([0x06, 0x68, 0x69, 0x21]);
      assert(buf.equals(type.toBuffer('hi!', 1)));
    });

    it('resolve string > bytes', function () {
      var stringT = createType('string');
      var bytesT = createType('bytes');
      var buf = stringT.toBuffer('\x00\x01');
      assert.deepEqual(
        bytesT.fromBuffer(buf, bytesT.createResolver(stringT)),
        Buffer.from([0, 1])
      );
    });

    it('encode resize', function () {
      var t = createType('string');
      var s = 'hello';
      var b, pos;
      b = Buffer.alloc(2);
      pos = t.encode(s, b);
      assert(pos < 0);
      b = Buffer.alloc(2 - pos);
      pos = t.encode(s, b);
      assert(pos >= 0);
      assert.equal(s, t.fromBuffer(b)); // Also checks exact length match.
    });

  });

  describe('NullType', function () {

    var data = [
      {
        schema: 'null',
        valid: [null],
        invalid: [0, 1, 'hi', undefined]
      }
    ];

    testType(types.NullType, data);

  });

  describe('FloatType', function () {

    var data = [
      {
        valid: [1, -3, 123e7],
        invalid: [null, 'hi', undefined],
        check: function (a, b) { assert(floatEquals(a, b)); }
      }
    ];

    testType(types.FloatType, data);

    it('compare buffer', function () {
      var t = createType('float');
      var b1 = t.toBuffer(0.5);
      assert.equal(t.compareBuffers(b1, b1), 0);
      var b2 = t.toBuffer(-0.75);
      assert.equal(t.compareBuffers(b1, b2), 1);
      var b3 = t.toBuffer(175);
      assert.equal(t.compareBuffers(b1, b3), -1);
    });

    it('resolver double > float', function () {
      assert.throws(function () { getResolver('float', 'double'); });
    });

    it('fromString', function () {
      var t = createType('float');
      var f = t.fromString('3.1');
      assert(t.isValid(f));
    });

    it('clone from double', function () {
      var t = createType('float');
      var d = 3.1;
      var f;
      f = t.clone(d);
      assert(t.isValid(f));
    });

  });

  describe('DoubleType', function () {

    var data = [
      {
        valid: [1, -3.4, 12314e31, 5e37],
        invalid: [null, 'hi', undefined],
        check: function (a, b) { assert(floatEquals(a, b), '' + [a, b]); }
      }
    ];

    testType(types.DoubleType, data);

    it('resolver string > double', function () {
      assert.throws(function () { getResolver('double', 'string'); });
    });

    it('compare buffer', function () {
      var t = createType('double');
      var b1 = t.toBuffer(0.5);
      assert.equal(t.compareBuffers(b1, b1), 0);
      var b2 = t.toBuffer(-0.75);
      assert.equal(t.compareBuffers(b1, b2), 1);
      var b3 = t.toBuffer(175);
      assert.equal(t.compareBuffers(b1, b3), -1);
    });

  });

  describe('BytesType', function () {

    var data = [
      {
        valid: [Buffer.alloc(1), Buffer.from('abc')],
        invalid: [null, 'hi', undefined, 1, 0, -3.5]
      }
    ];

    testType(types.BytesType, data);

    it('clone', function () {
      var t = createType('bytes');
      var s = '\x01\x02';
      var buf = Buffer.from(s);
      var clone;
      clone = t.clone(buf);
      assert.deepEqual(clone, buf);
      clone[0] = 0;
      assert.equal(buf[0], 1);
      assert.throws(function () { t.clone(s); });
      clone = t.clone(buf.toJSON(), {coerceBuffers: true});
      assert.deepEqual(clone, buf);
      assert.throws(function () { t.clone(1, {coerceBuffers: true}); });
    });

    it('fromString', function () {
      var t = createType('bytes');
      var s = '\x01\x02';
      var buf = Buffer.from(s);
      var clone = t.fromString(JSON.stringify(s));
      assert.deepEqual(clone, buf);
    });

    it('compare', function () {
      var t = createType('bytes');
      var b1 = t.toBuffer(Buffer.from([0, 2]));
      assert.equal(t.compareBuffers(b1, b1), 0);
      var b2 = t.toBuffer(Buffer.from([0, 2, 3]));
      assert.equal(t.compareBuffers(b1, b2), -1);
      var b3 = t.toBuffer(Buffer.from([1]));
      assert.equal(t.compareBuffers(b3, b1), 1);
    });

  });

  describe('UnionType', function () {

    var data = [
      {
        name: 'null & string',
        schema: ['null', 'string'],
        valid: [null, {string: 'hi'}],
        invalid: ['null', undefined, {string: 1}],
        check: assert.deepEqual
      },
      {
        name: 'qualified name',
        schema: ['null', {type: 'fixed', name: 'a.B', size: 2}],
        valid: [null, {'a.B': Buffer.alloc(2)}],
        invalid: [Buffer.alloc(2)],
        check: assert.deepEqual
      },
      {
        name: 'array int',
        schema: ['int', {type: 'array', items: 'int'}],
        valid: [{'int': 1}, {array: [1,3]}],
        invalid: [null, 2, {array: ['a']}, [4], 2],
        check: assert.deepEqual
      },
      {
        name: 'null',
        schema: ['null'],
        valid: [null],
        invalid: [{array: ['a']}, [4], 'null'],
        check: assert.deepEqual
      }
    ];

    var schemas = [
      {},
      [],
      ['null', 'null'],
      ['null', {type: 'map', values: 'int'}, {type: 'map', values: 'long'}],
      ['null', ['int', 'string']]
    ];

    testType(types.UnionType, data, schemas);

    it('getTypes', function () {
      var t = createType(['null', 'int']);
      assert.deepEqual(t.getTypes(), [createType('null'), createType('int')]);
    });

    it('instanceof Union', function () {
      var type = new types.UnionType(['null', 'int']);
      assert(type instanceof types.UnionType);
    });

    it('missing name write', function () {
      var type = new types.UnionType(['null', 'int']);
      assert.throws(function () {
        type.toBuffer({b: 'a'});
      });
    });

    it('read invalid index', function () {
      var type = new types.UnionType(['null', 'int']);
      var buf = Buffer.from([1, 0]);
      assert.throws(function () { type.fromBuffer(buf); });
    });

    it('non wrapped write', function () {
      var type = new types.UnionType(['null', 'int']);
      assert.throws(function () {
        type.toBuffer(1, true);
      }, Error);
    });

    it('to JSON', function () {
      var type = new types.UnionType(['null', 'int']);
      assert.equal(JSON.stringify(type), '["null","int"]');
    });

    it('resolve int to [long, int]', function () {
      var t1 = createType('int');
      var t2 = createType(['long', 'int']);
      var a = t2.createResolver(t1);
      var buf = t1.toBuffer(23);
      assert.deepEqual(t2.fromBuffer(buf, a), {'long': 23});
    });

    it('resolve null to [null, int]', function () {
      var t1 = createType('null');
      var t2 = createType(['null', 'int']);
      var a = t2.createResolver(t1);
      assert.deepEqual(t2.fromBuffer(Buffer.alloc(0), a), null);
    });

    it('resolve [string, int] to [long, string]', function () {
      var t1 = createType(['string', 'int']);
      var t2 = createType(['int', 'bytes']);
      var a = t2.createResolver(t1);
      var buf;
      buf = t1.toBuffer({string: 'hi'});
      assert.deepEqual(t2.fromBuffer(buf, a), {'bytes': Buffer.from('hi')});
      buf = t1.toBuffer({'int': 1});
      assert.deepEqual(t2.fromBuffer(buf, a), {'int': 1});
    });

    it('clone', function () {
      var t = new types.UnionType(['null', 'int']);
      var o = {'int': 1};
      assert.strictEqual(t.clone(null), null);
      var c = t.clone(o);
      assert.deepEqual(c, o);
      c.int = 2;
      assert.equal(o.int, 1);
      assert.throws(function () { t.clone([]); });
      assert.throws(function () { t.clone(undefined); });
    });

    it('clone and wrap', function () {
      var t = createType(['string', 'int']);
      var o;
      o = t.clone('hi', {wrapUnions: true});
      assert.deepEqual(o, {'string': 'hi'});
      o = t.clone(3, {wrapUnions: true});
      assert.deepEqual(o, {'int': 3});
      assert.throws(function () { t.clone(null, {wrapUnions: 2}); });
    });

    it('invalid multiple keys', function () {
      var t = createType(['null', 'int']);
      var o = {'int': 2};
      assert(t.isValid(o));
      o.foo = 3;
      assert(!t.isValid(o));
    });

    it('clone multiple keys', function () {
      var t = createType(['null', 'int']);
      var o = {'int': 2, foo: 3};
      assert.throws(function () { t.clone(o); });
    });

    it('clone unqualified names', function () {
      var t = createType({
        name: 'Person',
        type: 'record',
        fields: [
          {name: 'id1', type: {name: 'an.Id', type: 'fixed', size: 1}},
          {name: 'id2', type: ['null', 'an.Id']}
        ]
      });
      var b = Buffer.from([0]);
      var o = {id1: b, id2: {Id: b}};
      assert.deepEqual(t.clone(o), {id1: b, id2: {'an.Id': b}});
    });

    it('clone unqualified names', function () {
      var t = createType({
        name: 'Person',
        type: 'record',
        fields: [
          {name: 'id1', type: {name: 'Id', type: 'fixed', size: 1}},
          {name: 'id2', type: ['null', 'Id']}
        ]
      });
      var b = Buffer.from([0]);
      var o = {id1: b, id2: {'an.Id': b}};
      assert.throws(function () { t.clone(o); });
    });

    it('compare buffers', function () {
      var t = createType(['null', 'double']);
      var b1 = t.toBuffer(null);
      assert.equal(t.compareBuffers(b1, b1), 0);
      var b2 = t.toBuffer({'double': 4});
      assert.equal(t.compareBuffers(b2, b1), 1);
      assert.equal(t.compareBuffers(b1, b2), -1);
      var b3 = t.toBuffer({'double': 6});
      assert.equal(t.compareBuffers(b3, b2), 1);
    });

    it('compare', function () {
      var t;
      t = createType(['null', 'int']);
      assert.equal(t.compare(null, {'int': 3}), -1);
      assert.equal(t.compare(null, null), 0);
      t = createType(['int', 'float']);
      assert.equal(t.compare({'int': 2}, {'float': 0.5}), -1);
      assert.equal(t.compare({'int': 20}, {'int': 5}), 1);
    });

  });

  describe('EnumType', function () {

    var data = [
      {
        name: 'single symbol',
        schema: {name: 'Foo', symbols: ['HI']},
        valid: ['HI'],
        invalid: ['HEY', null, undefined, 0]
      },
      {
        name: 'number-ish as symbol',
        schema: {name: 'Foo', symbols: ['HI', 'A0']},
        valid: ['HI', 'A0'],
        invalid: ['HEY', null, undefined, 0, 'a0']
      }
    ];

    var schemas = [
      {name: 'Foo', symbols: []},
      {name: 'Foo'},
      {symbols: ['hi']},
      {name: 'G', symbols: ['0']}
    ];

    testType(types.EnumType, data, schemas);

    it('get full name', function () {
      var t = createType({
        type: 'enum',
        symbols: ['A', 'B'],
        name: 'Letter',
        namespace: 'latin'
      });
      assert.equal(t.getName(), 'latin.Letter');
    });

    it('get aliases', function () {
      var t = createType({
        type: 'enum',
        symbols: ['A', 'B'],
        name: 'Letter',
        namespace: 'latin',
        aliases: ['Character', 'alphabet.Letter']
      });
      var aliases = t.getAliases();
      assert.deepEqual(aliases, ['latin.Character', 'alphabet.Letter']);
      aliases.push('Char');
      assert.equal(t.getAliases().length, 3);
    });

    it('get symbols', function () {
      var t = createType({type: 'enum', symbols: ['A', 'B'], name: 'Letter'});
      var symbols = t.getSymbols();
      assert.deepEqual(symbols, ['A', 'B']);
      symbols.push('Char');
      assert.equal(t.getSymbols().length, 2);
    });

    it('duplicate symbol', function () {
      assert.throws(function () {
        createType({type: 'enum', symbols: ['A', 'B', 'A'], name: 'B'});
      });
    });

    it('write invalid', function () {
      var type = createType({type: 'enum', symbols: ['A'], name: 'a'});
      assert.throws(function () {
        type.toBuffer('B');
      });
    });

    it('read invalid index', function () {
      var type = new types.EnumType({type: 'enum', symbols: ['A'], name: 'a'});
      var buf = Buffer.from([2]);
      assert.throws(function () { type.fromBuffer(buf); });
    });

    it('resolve', function () {
      var t1, t2, buf, resolver;
      t1 = newEnum('Foo', ['bar', 'baz']);
      t2 = newEnum('Foo', ['bar', 'baz']);
      resolver = t1.createResolver(t2);
      buf = t2.toBuffer('bar');
      assert.equal(t1.fromBuffer(buf, resolver), 'bar');
      t2 = newEnum('Foo', ['baz', 'bar']);
      buf = t2.toBuffer('bar');
      resolver = t1.createResolver(t2);
      assert.notEqual(t1.fromBuffer(buf), 'bar');
      assert.equal(t1.fromBuffer(buf, resolver), 'bar');
      t1 = newEnum('Foo2', ['foo', 'baz', 'bar'], ['Foo']);
      resolver = t1.createResolver(t2);
      assert.equal(t1.fromBuffer(buf, resolver), 'bar');
      t2 = newEnum('Foo', ['bar', 'bax']);
      assert.throws(function () { t1.createResolver(t2); });
      assert.throws(function () {
        t1.createResolver(createType('int'));
      });
      function newEnum(name, symbols, aliases, namespace) {
        var obj = {type: 'enum', name: name, symbols: symbols};
        if (aliases !== undefined) {
          obj.aliases = aliases;
        }
        if (namespace !== undefined) {
          obj.namespace = namespace;
        }
        return new types.EnumType(obj);
      }
    });

    it('clone', function () {
      var t = createType({type: 'enum', name: 'Foo', symbols: ['bar', 'baz']});
      assert.equal(t.clone('bar'), 'bar');
      assert.throws(function () { t.clone('BAR'); });
      assert.throws(function () { t.clone(null); });
    });

    it('compare buffers', function () {
      var t = createType({type: 'enum', name: 'Foo', symbols: ['bar', 'baz']});
      var b1 = t.toBuffer('bar');
      var b2 = t.toBuffer('baz');
      assert.equal(t.compareBuffers(b1, b1), 0);
      assert.equal(t.compareBuffers(b2, b1), 1);
    });

    it('compare', function () {
      var t = createType({type: 'enum', name: 'Foo', symbols: ['b', 'a']});
      assert.equal(t.compare('b', 'a'), -1);
      assert.equal(t.compare('a', 'a'), 0);
    });

  });

  describe('FixedType', function () {

    var data = [
      {
        name: 'size 1',
        schema: {name: 'Foo', size: 2},
        valid: [Buffer.from([1, 2]), Buffer.from([2, 3])],
        invalid: ['HEY', null, undefined, 0, Buffer.alloc(1), Buffer.alloc(3)],
        check: function (a, b) { assert(a.equals(b)); }
      }
    ];

    var schemas = [
      {name: 'Foo', size: 0},
      {name: 'Foo', size: -2},
      {size: 2},
      {name: 'Foo'},
      {}
    ];

    testType(types.FixedType, data, schemas);

    it('get full name', function () {
      var t = createType({
        type: 'fixed',
        size: 2,
        name: 'Id',
        namespace: 'id'
      });
      assert.equal(t.getName(), 'id.Id');
    });

    it('get aliases', function () {
      var t = createType({
        type: 'fixed',
        size: 3,
        name: 'Id'
      });
      var aliases = t.getAliases();
      assert.deepEqual(aliases, []);
      aliases.push('ID');
      assert.equal(t.getAliases().length, 1);
    });

    it('get size', function () {
      var t = createType({type: 'fixed', size: 5, name: 'Id'});
      assert.equal(t.getSize(), 5);
    });

    it('resolve', function () {
      var t1 = new types.FixedType({name: 'Id', size: 4});
      var t2 = new types.FixedType({name: 'Id', size: 4});
      assert.doesNotThrow(function () { t2.createResolver(t1); });
      t2 = new types.FixedType({name: 'Id2', size: 4});
      assert.throws(function () { t2.createResolver(t1); });
      t2 = new types.FixedType({name: 'Id2', size: 4, aliases: ['Id']});
      assert.doesNotThrow(function () { t2.createResolver(t1); });
      t2 = new types.FixedType({name: 'Id2', size: 5, aliases: ['Id']});
      assert.throws(function () { t2.createResolver(t1); });
    });

    it('clone', function () {
      var t = new types.FixedType({name: 'Id', size: 2});
      var s = '\x01\x02';
      var buf = Buffer.from(s);
      var clone;
      clone = t.clone(buf);
      assert.deepEqual(clone, buf);
      clone[0] = 0;
      assert.equal(buf[0], 1);
      assert.throws(function () { t.clone(s); });
      clone = t.clone(buf.toJSON(), {coerceBuffers: true});
      assert.deepEqual(clone, buf);
      assert.throws(function () { t.clone(1, {coerceBuffers: true}); });
      assert.throws(function () { t.clone(Buffer.from([2])); });
    });

    it('getSchema with extra fields', function () {
      var t = createType({type: 'fixed', name: 'Id', size: 2, three: 3});
      t.one = 1;
      assert.equal(t.getSchema(), '{"name":"Id","type":"fixed","size":2}');
      assert.equal(t.getSchema(true), '"Id"');
    });

    it('fromString', function () {
      var t = new types.FixedType({name: 'Id', size: 2});
      var s = '\x01\x02';
      var buf = Buffer.from(s);
      var clone = t.fromString(JSON.stringify(s));
      assert.deepEqual(clone, buf);
    });

    it('compare buffers', function () {
      var t = createType({type: 'fixed', name: 'Id', size: 2});
      var b1 = Buffer.from([1, 2]);
      assert.equal(t.compareBuffers(b1, b1), 0);
      var b2 = Buffer.from([2, 2]);
      assert.equal(t.compareBuffers(b1, b2), -1);
    });

  });

  describe('MapType', function () {

    var data = [
      {
        name: 'int',
        schema: {values: 'int'},
        valid: [{one: 1}, {two: 2, o: 0}],
        invalid: [1, {o: null}, [], undefined, {o: 'hi'}, {1: '', 2: 3}, ''],
        check: assert.deepEqual
      },
      {
        name: 'enum',
        schema: {values: {type: 'enum', name: 'a', symbols: ['A', 'B']}},
        valid: [{a: 'A'}, {a: 'A', b: 'B'}, {}],
        invalid: [{o: 'a'}, {1: 'A', 2: 'b'}, {a: 3}],
        check: assert.deepEqual
      },
      {
        name: 'array of string',
        schema: {values: {type: 'array', items: 'string'}},
        valid: [{a: []}, {a: ['A'], b: ['B', '']}, {}],
        invalid: [{o: 'a', b: []}, {a: [1, 2]}, {a: {b: ''}}],
        check: assert.deepEqual
      }
    ];

    var schemas = [
      {},
      {values: ''},
      {values: {type: 'array'}}
    ];

    testType(types.MapType, data, schemas);

    it('get values type', function () {
      var t = new types.MapType({type: 'map', values: 'int'});
      assert.deepEqual(t.getValuesType(), createType('int'));
    });

    it('write int', function () {
      var t = new types.MapType({type: 'map', values: 'int'});
      var buf = t.toBuffer({'\x01': 3, '\x02': 4});
      assert.deepEqual(buf, Buffer.from([4, 2, 1, 6, 2, 2, 8, 0]));
    });

    it('read long', function () {
      var t = new types.MapType({type: 'map', values: 'long'});
      var buf = Buffer.from([4, 2, 1, 6, 2, 2, 8, 0]);
      assert.deepEqual(t.fromBuffer(buf), {'\x01': 3, '\x02': 4});
    });

    it('read with sizes', function () {
      var t = new types.MapType({type: 'map', values: 'int'});
      var buf = Buffer.from([1,6,2,97,2,0]);
      assert.deepEqual(t.fromBuffer(buf), {a: 1});
    });

    it('skip', function () {
      var v1 = createType({
        name: 'Foo',
        type: 'record',
        fields: [
          {name: 'map', type: {type: 'map', values: 'int'}},
          {name: 'val', type: 'int'}
        ]
      });
      var v2 = createType({
        name: 'Foo',
        type: 'record',
        fields: [{name: 'val', type: 'int'}]
      });
      var b1 = Buffer.from([2,2,97,2,0,6]); // Without sizes.
      var b2 = Buffer.from([1,6,2,97,2,0,6]); // With sizes.
      var resolver = v2.createResolver(v1);
      assert.deepEqual(v2.fromBuffer(b1, resolver), {val: 3});
      assert.deepEqual(v2.fromBuffer(b2, resolver), {val: 3});
    });

    it('resolve int > long', function () {
      var t1 = new types.MapType({type: 'map', values: 'int'});
      var t2 = new types.MapType({type: 'map', values: 'long'});
      var resolver = t2.createResolver(t1);
      var obj = {one: 1, two: 2};
      var buf = t1.toBuffer(obj);
      assert.deepEqual(t2.fromBuffer(buf, resolver), obj);
    });

    it('resolve double > double', function () {
      var t = new types.MapType({type: 'map', values: 'double'});
      var resolver = t.createResolver(t);
      var obj = {one: 1, two: 2};
      var buf = t.toBuffer(obj);
      assert.deepEqual(t.fromBuffer(buf, resolver), obj);
    });

    it('resolve invalid', function () {
      var t1 = new types.MapType({type: 'map', values: 'int'});
      var t2 = new types.MapType({type: 'map', values: 'string'});
      assert.throws(function () { t2.createResolver(t1); });
      t2 = new types.ArrayType({type: 'array', items: 'string'});
      assert.throws(function () { t2.createResolver(t1); });
    });

    it('resolve fixed', function () {
      var t1 = createType({
        type: 'map', values: {name: 'Id', type: 'fixed', size: 2}
      });
      var t2 = createType({
        type: 'map', values: {
          name: 'Id2', aliases: ['Id'], type: 'fixed', size: 2
        }
      });
      var resolver = t2.createResolver(t1);
      var obj = {one: Buffer.from([1, 2])};
      var buf = t1.toBuffer(obj);
      assert.deepEqual(t2.fromBuffer(buf, resolver), obj);
    });

    it('clone', function () {
      var t = new types.MapType({type: 'map', values: 'int'});
      var o = {one: 1, two: 2};
      var c = t.clone(o);
      assert.deepEqual(c, o);
      c.one = 3;
      assert.equal(o.one, 1);
      assert.throws(function () { t.clone(undefined); });
    });

    it('clone coerce buffers', function () {
      var t = new types.MapType({type: 'map', values: 'bytes'});
      var o = {one: {type: 'Buffer', data: [1]}};
      assert.throws(function () { t.clone(o); });
      var c = t.clone(o, {coerceBuffers: true});
      assert.deepEqual(c, {one: Buffer.from([1])});
    });

    it('compare buffers', function () {
      var t = new types.MapType({type: 'map', values: 'bytes'});
      var b1 = t.toBuffer({});
      assert.throws(function () { t.compareBuffers(b1, b1); });
    });

    it('isValid hook', function () {
      var t = new types.MapType({type: 'map', values: 'int'});
      var o = {one: 1, two: 'deux', three: null, four: 4};
      var errs = {};
      assert(!t.isValid(o, {errorHook: hook}));
      assert.deepEqual(errs, {two: 'deux', three: null});

      function hook(path, obj, type) {
        assert.strictEqual(type, t.getValuesType());
        assert.equal(path.length, 1);
        errs[path[0]] = obj;
      }
    });

    it('getName', function () {
      var t = new types.MapType({type: 'map', values: 'int'});
      assert.strictEqual(t.getName(), undefined);
    });

  });

  describe('ArrayType', function () {

    var data = [
      {
        name: 'int',
        schema: {items: 'int'},
        valid: [[1,3,4], []],
        invalid: [1, {o: null}, undefined, ['a'], [true]],
        check: assert.deepEqual
      }
    ];

    var schemas = [
      {},
      {items: ''},
    ];

    testType(types.ArrayType, data, schemas);

    it('get items type', function () {
      var t = new types.ArrayType({type: 'array', items: 'int'});
      assert.deepEqual(t.getItemsType(), createType('int'));
    });

    it('read with sizes', function () {
      var t = new types.ArrayType({type: 'array', items: 'int'});
      var buf = Buffer.from([1,2,2,0]);
      assert.deepEqual(t.fromBuffer(buf), [1]);
    });

    it('skip', function () {
      var v1 = createType({
        name: 'Foo',
        type: 'record',
        fields: [
          {name: 'array', type: {type: 'array', items: 'int'}},
          {name: 'val', type: 'int'}
        ]
      });
      var v2 = createType({
        name: 'Foo',
        type: 'record',
        fields: [{name: 'val', type: 'int'}]
      });
      var b1 = Buffer.from([2,2,0,6]); // Without sizes.
      var b2 = Buffer.from([1,2,2,0,6]); // With sizes.
      var resolver = v2.createResolver(v1);
      assert.deepEqual(v2.fromBuffer(b1, resolver), {val: 3});
      assert.deepEqual(v2.fromBuffer(b2, resolver), {val: 3});
    });

    it('resolve string items to bytes items', function () {
      var t1 = new types.ArrayType({type: 'array', items: 'string'});
      var t2 = new types.ArrayType({type: 'array', items: 'bytes'});
      var resolver = t2.createResolver(t1);
      var obj = ['\x01\x02'];
      var buf = t1.toBuffer(obj);
      assert.deepEqual(t2.fromBuffer(buf, resolver), [Buffer.from([1, 2])]);
    });

    it('resolve invalid', function () {
      var t1 = new types.ArrayType({type: 'array', items: 'string'});
      var t2 = new types.ArrayType({type: 'array', items: 'long'});
      assert.throws(function () { t2.createResolver(t1); });
      t2 = new types.MapType({type: 'map', values: 'string'});
      assert.throws(function () { t2.createResolver(t1); });
    });

    it('clone', function () {
      var t = new types.ArrayType({type: 'array', items: 'int'});
      var o = [1, 2];
      var c = t.clone(o);
      assert.deepEqual(c, o);
      c.one = 3;
      assert.equal(o[0], 1);
      assert.throws(function () { t.clone({}); });
    });

    it('clone coerce buffers', function () {
      var t = createType({
        type: 'array',
        items: {type: 'fixed', name: 'Id', size: 2}
      });
      var o = [{type: 'Buffer', data: [1, 2]}];
      assert.throws(function () { t.clone(o); });
      var c = t.clone(o, {coerceBuffers: true});
      assert.deepEqual(c, [Buffer.from([1, 2])]);
    });

    it('compare buffers', function () {
      var t = createType({type: 'array', items: 'int'});
      assert.equal(t.compareBuffers(t.toBuffer([]), t.toBuffer([])), 0);
      assert.equal(t.compareBuffers(t.toBuffer([1, 2]), t.toBuffer([])), 1);
      assert.equal(t.compareBuffers(t.toBuffer([1]), t.toBuffer([1, -1])), -1);
      assert.equal(t.compareBuffers(t.toBuffer([1]), t.toBuffer([2])), -1);
      assert.equal(t.compareBuffers(t.toBuffer([1, 2]), t.toBuffer([1])), 1);
    });

    it('compare', function () {
      var t = createType({type: 'array', items: 'int'});
      assert.equal(t.compare([], []), 0);
      assert.equal(t.compare([], [-1]), -1);
      assert.equal(t.compare([1], [1]), 0);
      assert.equal(t.compare([2], [1, 2]), 1);
    });

    it('isValid hook invalid array', function () {
      var t = createType({type: 'array', items: 'int'});
      var hookCalled = false;
      assert(!t.isValid({}, {errorHook: hook}));
      assert(hookCalled);

      function hook(path, obj, type) {
        assert.strictEqual(type, t);
        assert.deepEqual(path, []);
        hookCalled = true;
      }
    });

    it('isValid hook invalid elems', function () {
      var t = createType({type: 'array', items: 'int'});
      var paths = [];
      assert(!t.isValid([0, 3, 'hi', 5, 'hey'], {errorHook: hook}));
      assert.deepEqual(paths, [['2'], ['4']]);

      function hook(path, obj, type) {
        assert.strictEqual(type, t.getItemsType());
        assert.equal(typeof obj, 'string');
        paths.push(path);
      }
    });

  });

  describe('RecordType', function () {

    var data = [
      {
        name: 'union field null and string with default',
        schema: {
          type: 'record',
          name: 'a',
          fields: [{name: 'b', type: ['null', 'string'], 'default': null}]
        },
        valid: [],
        invalid: [],
        check: assert.deepEqual
      }
    ];

    var schemas = [
      {type: 'record', name: 'a', fields: ['null', 'string']},
      {type: 'record', name: 'a', fields: [{type: ['null', 'string']}]},
      {
        type: 'record',
        name: 'a',
        fields: [{name: 'b', type: ['null', 'string'], 'default': 'a'}]
      },
      {type: 'record', name: 'a', fields: {type: 'int', name: 'age'}}
    ];

    testType(types.RecordType, data, schemas);

    it('duplicate field names', function () {
      assert.throws(function () {
        createType({
          type: 'record',
          name: 'Person',
          fields: [{name: 'age', type: 'int'}, {name: 'age', type: 'float'}]
        });
      });
    });

    it('default constructor', function () {
      var type = createType({
        type: 'record',
        name: 'Person',
        fields: [{name: 'age', type: 'int', 'default': 25}]
      });
      var Person = type.getRecordConstructor();
      var p = new Person();
      assert.equal(p.age, 25);
      assert.strictEqual(p.constructor, Person);
    });

    it('default check & write', function () {
      var type = createType({
        type: 'record',
        name: 'Person',
        fields: [
          {name: 'age', type: 'int', 'default': 25},
          {name: 'name', type: 'string', 'default': '\x01'}
        ]
      });
      assert.deepEqual(type.toBuffer({}), Buffer.from([50, 2, 1]));
    });

    it('fixed string default', function () {
      var s = '\x01\x04';
      var b = Buffer.from(s);
      var type = createType({
        type: 'record',
        name: 'Object',
        fields: [
          {
            name: 'id',
            type: {type: 'fixed', size: 2, name: 'Id'},
            'default': s
          }
        ]
      });
      var obj = new (type.getRecordConstructor())();
      assert.deepEqual(obj.id, Buffer.from([1, 4]));
      assert.deepEqual(type.toBuffer({}), b);
    });

    it('fixed buffer invalid default', function () {
      assert.throws(function () {
        createType({
          type: 'record',
          name: 'Object',
          fields: [
            {
              name: 'id',
              type: {type: 'fixed', size: 2, name: 'Id'},
              'default': Buffer.from([0])
            }
          ]
        });
      });
    });

    it('union invalid default', function () {
      assert.throws(function () {
        createType({
          type: 'record',
          name: 'Person',
          fields: [{name: 'name', type: ['null', 'string'], 'default': ''}]
        });
      });
    });

    it('record default', function () {
      var d = {street: null, zip: 123};
      var Person = createType({
        name: 'Person',
        type: 'record',
        fields: [
          {
            name: 'address',
            type: {
              name: 'Address',
              type: 'record',
              fields: [
                {name: 'street', type: ['null', 'string']},
                {name: 'zip', type: ['int', 'string']}
              ]
            },
            'default': d
          }
        ]
      }).getRecordConstructor();
      var p = new Person();
      assert.deepEqual(p.address, {street: null, zip: {'int': 123}});
    });

    it('record keyword field name', function () {
      var type = createType({
        type: 'record',
        name: 'Person',
        fields: [{name: 'null', type: 'int'}]
      });
      var Person = type.getRecordConstructor();
      assert.deepEqual(new Person(2), {'null': 2});
    });

    it('record isValid', function () {
      var type = createType({
        type: 'record',
        name: 'Person',
        fields: [{name: 'age', type: 'int'}]
      });
      var Person = type.getRecordConstructor();
      assert((new Person(20)).$isValid());
      assert(!(new Person()).$isValid());
      assert(!(new Person('a')).$isValid());
    });

    it('record toBuffer', function () {
      var type = createType({
        type: 'record',
        name: 'Person',
        fields: [{name: 'age', type: 'int'}]
      });
      var Person = type.getRecordConstructor();
      assert.deepEqual((new Person(48)).$toBuffer(), Buffer.from([96]));
      assert.throws(function () { (new Person()).$toBuffer(); });
    });

    it('record compare', function () {
      var P = createType({
        type: 'record',
        name: 'Person',
        fields: [
          {name: 'data', type: {type: 'map', values: 'int'}, order:'ignore'},
          {name: 'age', type: 'int'}
        ]
      }).getRecordConstructor();
      var p1 = new P({}, 1);
      var p2 = new P({}, 2);
      assert.equal(p1.$compare(p2), -1);
      assert.equal(p2.$compare(p2), 0);
      assert.equal(p2.$compare(p1), 1);
    });

    it('Record type', function () {
      var type = createType({
        type: 'record',
        name: 'Person',
        fields: [{name: 'age', type: 'int'}]
      });
      var Person = type.getRecordConstructor();
      assert.strictEqual(Person.getType(), type);
    });

    it('mutable defaults', function () {
      var Person = createType({
        type: 'record',
        name: 'Person',
        fields: [
          {
            name: 'friends',
            type: {type: 'array', items: 'string'},
            'default': []
          }
        ]
      }).getRecordConstructor();
      var p1 = new Person(undefined);
      assert.deepEqual(p1.friends, []);
      p1.friends.push('ann');
      var p2 = new Person(undefined);
      assert.deepEqual(p2.friends, []);
    });

    it('resolve alias', function () {
      var v1 = createType({
        type: 'record',
        name: 'Person',
        fields: [{name: 'name', type: 'string'}]
      });
      var p = v1.random();
      var buf = v1.toBuffer(p);
      var v2 = createType({
        type: 'record',
        name: 'Human',
        aliases: ['Person'],
        fields: [{name: 'name', type: 'string'}]
      });
      var resolver = v2.createResolver(v1);
      assert.deepEqual(v2.fromBuffer(buf, resolver), p);
      var v3 = createType({
        type: 'record',
        name: 'Human',
        fields: [{name: 'name', type: 'string'}]
      });
      assert.throws(function () { v3.createResolver(v1); });
    });

    it('resolve alias with namespace', function () {
      var v1 = createType({
        type: 'record',
        name: 'Person',
        namespace: 'earth',
        fields: [{name: 'name', type: 'string'}]
      });
      var v2 = createType({
        type: 'record',
        name: 'Human',
        aliases: ['Person'],
        fields: [{name: 'name', type: 'string'}]
      });
      assert.throws(function () { v2.createResolver(v1); });
      var v3 = createType({
        type: 'record',
        name: 'Human',
        aliases: ['earth.Person'],
        fields: [{name: 'name', type: 'string'}]
      });
      assert.doesNotThrow(function () { v3.createResolver(v1); });
    });

    it('resolve skip field', function () {
      var v1 = createType({
        type: 'record',
        name: 'Person',
        fields: [
          {name: 'age', type: 'int'},
          {name: 'name', type: 'string'}
        ]
      });
      var p = {age: 25, name: 'Ann'};
      var buf = v1.toBuffer(p);
      var v2 = createType({
        type: 'record',
        name: 'Person',
        fields: [{name: 'name', type: 'string'}]
      });
      var resolver = v2.createResolver(v1);
      assert.deepEqual(v2.fromBuffer(buf, resolver), {name: 'Ann'});
    });

    it('resolve new field', function () {
      var v1 = createType({
        type: 'record',
        name: 'Person',
        fields: [{name: 'name', type: 'string'}]
      });
      var p = {name: 'Ann'};
      var buf = v1.toBuffer(p);
      var v2 = createType({
        type: 'record',
        name: 'Person',
        fields: [
          {name: 'age', type: 'int', 'default': 25},
          {name: 'name', type: 'string'}
        ]
      });
      var resolver = v2.createResolver(v1);
      assert.deepEqual(v2.fromBuffer(buf, resolver), {name: 'Ann', age: 25});
    });

    it('resolve new field no default', function () {
      var v1 = createType({
        type: 'record',
        name: 'Person',
        fields: [{name: 'name', type: 'string'}]
      });
      var v2 = createType({
        type: 'record',
        name: 'Person',
        fields: [
          {name: 'age', type: 'int'},
          {name: 'name', type: 'string'}
        ]
      });
      assert.throws(function () { v2.createResolver(v1); });
    });

    it('resolve from recursive schema', function () {
      var v1 = createType({
        type: 'record',
        name: 'Person',
        fields: [{name: 'friends', type: {type: 'array', items: 'Person'}}]
      });
      var v2 = createType({
        type: 'record',
        name: 'Person',
        fields: [{name: 'age', type: 'int', 'default': -1}]
      });
      var resolver = v2.createResolver(v1);
      var p1 = {friends: [{friends: []}]};
      var p2 = v2.fromBuffer(v1.toBuffer(p1), resolver);
      assert.deepEqual(p2, {age: -1});
    });

    it('resolve to recursive schema', function () {
      var v1 = createType({
        type: 'record',
        name: 'Person',
        fields: [{name: 'age', type: 'int', 'default': -1}]
      });
      var v2 = createType({
        type: 'record',
        name: 'Person',
        fields: [
          {
            name: 'friends',
            type: {type: 'array', items: 'Person'},
            'default': []
          }
        ]
      });
      var resolver = v2.createResolver(v1);
      var p1 = {age: 25};
      var p2 = v2.fromBuffer(v1.toBuffer(p1), resolver);
      assert.deepEqual(p2, {friends: []});
    });

    it('resolve from both recursive schema', function () {
      var v1 = createType({
        type: 'record',
        name: 'Person',
        fields: [
          {name: 'friends', type: {type: 'array', items: 'Person'}},
          {name: 'age', type: 'int'}
        ]
      });
      var v2 = createType({
        type: 'record',
        name: 'Person',
        fields: [{name: 'friends', type: {type: 'array', items: 'Person'}}]
      });
      var resolver = v2.createResolver(v1);
      var p1 = {friends: [{age: 1, friends: []}], age: 10};
      var p2 = v2.fromBuffer(v1.toBuffer(p1), resolver);
      assert.deepEqual(p2, {friends: [{friends: []}]});
    });

    it('resolve multiple matching aliases', function () {
      var v1 = createType({
        type: 'record',
        name: 'Person',
        fields: [
          {name: 'phone', type: 'string'},
          {name: 'number', type: 'string'}
        ]
      });
      var v2 = createType({
        type: 'record',
        name: 'Person',
        fields: [{name: 'number', type: 'string', aliases: ['phone']}]
      });
      assert.throws(function () { v2.createResolver(v1); });
    });

    it('getName', function () {
      var t = createType({
        type: 'record',
        name: 'Person',
        doc: 'Hi!',
        namespace: 'earth',
        aliases: ['Human'],
        fields: [
          {name: 'friends', type: {type: 'array', items: 'string'}},
          {name: 'age', aliases: ['years'], type: {type: 'int'}}
        ]
      });
      assert.strictEqual(t.getName(), 'earth.Person');
      assert.equal(t.getName(true), 'record');
    });

    it('getSchema', function () {
      var t = createType({
        type: 'record',
        name: 'Person',
        doc: 'Hi!',
        namespace: 'earth',
        aliases: ['Human'],
        fields: [
          {name: 'friends', type: {type: 'array', items: 'string'}},
          {name: 'age', aliases: ['years'], type: {type: 'int'}}
        ]
      });
      assert.equal(
        t.getSchema(),
        '{"name":"earth.Person","type":"record","fields":[{"name":"friends","type":{"type":"array","items":"string"}},{"name":"age","type":"int"}]}'
      );
      assert.equal(t.getSchema(true), '"earth.Person"');
    });

    it('getSchema recursive schema', function () {
      var t = createType({
        type: 'record',
        name: 'Person',
        namespace: 'earth',
        fields: [
          {name: 'friends', type: {type: 'array', items: 'Person'}},
        ]
      });
      assert.equal(
        t.getSchema(),
        '{"name":"earth.Person","type":"record","fields":[{"name":"friends","type":{"type":"array","items":"earth.Person"}}]}'
      );
      assert.equal(t.getSchema(true), '"earth.Person"');
    });

    it('toString record', function () {
      var T = createType({
        type: 'record',
        name: 'Person',
        fields: [{name: 'pwd', type: 'bytes'}]
      }).getRecordConstructor();
      var r = new T(Buffer.from([1, 2]));
      assert.equal(r.$toString(), T.getType().toString(r));
    });

    it('clone', function () {
      var t = createType({
        type: 'record',
        name: 'Person',
        fields: [{name: 'age', type: 'int'}, {name: 'name', type: 'string'}]
      });
      var Person = t.getRecordConstructor();
      var o = {age: 25, name: 'Ann'};
      var c = t.clone(o);
      assert.deepEqual(c, o);
      assert(c instanceof Person);
      c.age = 26;
      assert.equal(o.age, 25);
      assert.strictEqual(c.$getType(), t);
      assert.deepEqual(c.$clone(), c);
    });

    it('clone field hook', function () {
      var t = createType({
        type: 'record',
        name: 'Person',
        fields: [{name: 'age', type: 'int'}, {name: 'name', type: 'string'}]
      });
      var o = {name: 'Ann', age: 25};
      var c = t.clone(o, {fieldHook: function (f, o, r) {
        assert.strictEqual(r, t);
        return f._type instanceof types.StringType ? o.toUpperCase() : o;
      }});
      assert.deepEqual(c, {name: 'ANN', age: 25});
    });

    it('get full name & aliases', function () {
      var t = createType({
        type: 'record',
        name: 'Person',
        namespace: 'a',
        fields: [{name: 'age', type: 'int'}, {name: 'name', type: 'string'}]
      });
      assert.equal(t.getName(), 'a.Person');
      assert.deepEqual(t.getAliases(), []);
    });

    it('field getters', function () {
      var t = createType({
        type: 'record',
        name: 'Person',
        namespace: 'a',
        fields: [
          {name: 'age', type: 'int'},
          {name: 'name', type: 'string', aliases: ['word'], namespace: 'b'}
        ]
      });
      var fields = t.getFields();
      assert.deepEqual(fields[0].getAliases(), []);
      assert.deepEqual(fields[1].getAliases(), ['word']);
      assert.equal(fields[1].getName(), 'name'); // Namespaces are ignored.
      assert.deepEqual(fields[1].getType(), createType('string'));
      fields.push('null');
      assert.equal(t.getFields().length, 2); // No change.
    });

    it('field order', function () {
      var t = createType({
        type: 'record',
        name: 'Person',
        fields: [{name: 'age', type: 'int'}]
      });
      var field = t.getFields()[0];
      assert.equal(field.getOrder(), 'ascending'); // Default.
    });

    it('compare buffers default order', function () {
      var t = createType({
        type: 'record',
        name: 'Person',
        fields: [
          {name: 'age', type: 'long'},
          {name: 'name', type: 'string'},
          {name: 'weight', type: 'float'},
        ]
      });
      var b1 = t.toBuffer({age: 20, name: 'Ann', weight: 0.5});
      assert.equal(t.compareBuffers(b1, b1), 0);
      var b2 = t.toBuffer({age: 20, name: 'Bob', weight: 0});
      assert.equal(t.compareBuffers(b1, b2), -1);
      var b3 = t.toBuffer({age: 19, name: 'Carrie', weight: 0});
      assert.equal(t.compareBuffers(b1, b3), 1);
    });

    it('compare buffers custom order', function () {
      var t = createType({
        type: 'record',
        name: 'Person',
        fields: [
          {name: 'meta', type: {type: 'map', values: 'int'}, order: 'ignore'},
          {name: 'name', type: 'string', order: 'descending'}
        ]
      });
      var b1 = t.toBuffer({meta: {}, name: 'Ann'});
      assert.equal(t.compareBuffers(b1, b1), 0);
      var b2 = t.toBuffer({meta: {foo: 1}, name: 'Bob'});
      assert.equal(t.compareBuffers(b1, b2), 1);
      var b3 = t.toBuffer({meta: {foo: 0}, name: 'Alex'});
      assert.equal(t.compareBuffers(b1, b3), -1);
    });

    it('compare buffers invalid order', function () {
      assert.throws(function () { createType({
        type: 'record',
        name: 'Person',
        fields: [{name: 'age', type: 'int', order: 'up'}]
      }); });
    });

    it('error type', function () {
      var t = createType({
        type: 'error',
        name: 'Ouch',
        fields: [{name: 'name', type: 'string'}]
      });
      var E = t.getRecordConstructor();
      var err = new E('MyError');
      assert(err instanceof Error);
    });

    it('isValid hook', function () {
      var t = createType({
        type: 'record',
        name: 'Person',
        fields: [
          {name: 'age', type: 'int'},
          {name: 'names', type: {type: 'array', items: 'string'}}
        ]
      });
      var hasErr = false;
      try {
        assert(!t.isValid({age: 23, names: ['ann', null]}, {errorHook: hook}));
      } catch (err) {
        hasErr = true;
      }
      assert(hasErr);
      hasErr = false;
      try {
        // Again to make sure `PATH` was correctly reset.
        assert(!t.isValid({age: 23, names: ['ann', null]}, {errorHook: hook}));
      } catch (err) {
        hasErr = true;
      }
      assert(hasErr);

      function hook(path, obj, type) {
        assert.strictEqual(type, t.getFields()[1].getType().getItemsType());
        assert.deepEqual(path, ['names', '1']);
        throw new Error();
      }
    });

    it('isValid empty record', function () {
      var t = createType({type: 'record', name: 'Person', fields: []});
      assert(t.isValid({}));
    });

  });

  describe('AbstractLongType', function () {

    var fastLongType = new types.LongType();

    describe('unpacked', function () {

      var slowLongType = types.LongType.using({
        fromBuffer: function (buf) {
          var neg = buf[7] >> 7;
          if (neg) { // Negative number.
            invert(buf);
          }
          var n = buf.readInt32LE() + Math.pow(2, 32) * buf.readInt32LE(4);
          if (neg) {
            invert(buf);
            n = -n - 1;
          }
          return n;
        },
        toBuffer: function (n) {
          var buf = Buffer.alloc(8);
          var neg = n < 0;
          if (neg) {
            invert(buf);
            n = -n - 1;
          }
          buf.writeInt32LE(n | 0);
          var h = n / Math.pow(2, 32) | 0;
          buf.writeInt32LE(h ? h : (n >= 0 ? 0 : -1), 4);
          if (neg) {
            invert(buf);
          }
          return buf;
        },
        isValid: function (n) {
          return typeof n == 'number' && n % 1 === 0;
        },
        fromJSON: function (n) { return n; },
        toJSON: function (n) { return n; },
        compare: function (n1, n2) {
          return n1 === n2 ? 0 : (n1 < n2 ? -1 : 1);
        }
      });

      it('encode', function () {
        [123, -1, 321414, 900719925474090].forEach(function (n) {
          assert.deepEqual(slowLongType.toBuffer(n), fastLongType.toBuffer(n));
        });
      });

      it('decode', function () {
        [123, -1, 321414, 900719925474090].forEach(function (n) {
          var buf = fastLongType.toBuffer(n);
          assert.deepEqual(slowLongType.fromBuffer(buf), n);
        });
      });

      it('clone', function () {
        assert.equal(slowLongType.clone(123), 123);
        assert.equal(slowLongType.fromString('-1'), -1);
        assert.equal(slowLongType.toString(-1), '-1');
      });

      it('random', function () {
        assert(slowLongType.isValid(slowLongType.random()));
      });

      it('isValid hook', function () {
        var s = 'hi';
        var errs = [];
        assert(!slowLongType.isValid(s, {errorHook: hook}));
        assert.deepEqual(errs, [s]);
        assert.throws(function () { slowLongType.toBuffer(s); });

        function hook(path, obj, type) {
          assert.strictEqual(type, slowLongType);
          assert.equal(path.length, 0);
          errs.push(obj);
        }
      });

    });

    describe('packed', function () {

      var slowLongType = types.LongType.using({
        fromBuffer: function (buf) {
          var tap = new Tap(buf);
          return tap.readLong();
        },
        toBuffer: function (n) {
          var buf = Buffer.alloc(10);
          var tap = new Tap(buf);
          tap.writeLong(n);
          return buf.slice(0, tap.pos);
        },
        fromJSON: function (n) { return n; },
        toJSON: function (n) { return n; },
        isValid: function (n) { return typeof n == 'number' && n % 1 === 0; },
        compare: function (n1, n2) {
          return n1 === n2 ? 0 : (n1 < n2 ? -1 : 1);
        }
      }, true);

      it('encode', function () {
        [123, -1, 321414, 900719925474090].forEach(function (n) {
          assert.deepEqual(slowLongType.toBuffer(n), fastLongType.toBuffer(n));
        });
      });

      it('decode', function () {
        [123, -1, 321414, 900719925474090].forEach(function (n) {
          var buf = fastLongType.toBuffer(n);
          assert.deepEqual(slowLongType.fromBuffer(buf), n);
        });
      });

      it('clone', function () {
        assert.equal(slowLongType.clone(123), 123);
        assert.equal(slowLongType.fromString('-1'), -1);
        assert.equal(slowLongType.toString(-1), '-1');
      });

      it('random', function () {
        assert(slowLongType.isValid(slowLongType.random()));
      });

    });

    it('incomplete buffer', function () {
      // Check that `fromBuffer` doesn't get called.
      var slowLongType = new types.LongType.using({
        fromBuffer: function () { throw new Error('no'); },
        toBuffer: null,
        fromJSON: null,
        toJSON: null,
        isValid: null,
        compare: null
      });
      var buf = fastLongType.toBuffer(12314);
      assert.deepEqual(
        slowLongType.decode(buf.slice(0, 1)),
        {value: undefined, offset: -1}
      );
    });

  });

  describe('LogicalType', function () {

    function DateType(attrs, opts) {
      types.LogicalType.call(this, attrs, opts, [types.LongType]);
    }
    util.inherits(DateType, types.LogicalType);

    DateType.prototype._fromValue = function (val) { return new Date(val); };

    DateType.prototype._toValue = function (date) { return +date; };

    DateType.prototype._resolve = function (type) {
      if (type instanceof types.StringType) {
        return function (str) { return new Date(Date.parse(str)); };
      } else if (type instanceof types.LongType) {
        return this.fromValue;
      }
    };

    function AgeType(attrs, opts) {
      types.LogicalType.call(this, attrs, opts, [types.IntType]);
    }
    util.inherits(AgeType, types.LogicalType);

    AgeType.prototype._fromValue = function (val) {
      if (val < 0) { throw new Error('invalid age'); }
      return val;
    };

    AgeType.prototype._toValue = AgeType.prototype._fromValue;

    var logicalTypes = {age: AgeType, date: DateType};

    it('valid type', function () {
      var t = createType({
        type: 'long',
        logicalType: 'date'
      }, {logicalTypes: logicalTypes});
      assert(t instanceof DateType);
      assert(t.getUnderlyingType() instanceof types.LongType);
      assert(t.isValid(t.random()));
      var d = new Date(123);
      assert.equal(t.toString(d), '123');
      assert.deepEqual(t.fromString('123'), d);
      assert.deepEqual(t.clone(d), d);
      assert.equal(t.compare(d, d), 0);
      assert.equal(t.getSchema(), '"long"');
    });

    it('invalid type', function () {
      var attrs = {
        type: 'int',
        logicalType: 'date'
      };
      var t;
      t = createType(attrs); // Missing.
      assert(t instanceof types.IntType);
      t = createType(attrs, {logicalTypes: logicalTypes}); // Invalid.
      assert(t instanceof types.IntType);
      assert.throws(function () {
        createType(attrs, {
          logicalTypes: logicalTypes,
          assertLogicalTypes: true
        });
      });
    });

    it('nested types', function () {
      var attrs = {
        name: 'Person',
        type: 'record',
        fields: [
          {name: 'age', type: {type: 'int', logicalType: 'age'}},
          {name: 'time', type: {type: 'long', logicalType: 'date'}}
        ]
      };
      var base = createType(attrs);
      var derived = createType(attrs, {logicalTypes: logicalTypes});
      var fields = derived.getFields();
      assert(fields[0].getType() instanceof AgeType);
      assert(fields[1].getType() instanceof DateType);
      var date = new Date(Date.now());
      var buf = base.toBuffer({age: 12, time: +date});
      var person = derived.fromBuffer(buf);
      assert.deepEqual(person.age, 12);
      assert.deepEqual(person.time, date);
      assert.throws(function () { derived.toBuffer({age: -1, date: date}); });

      var invalid = {age: -1, time: date};
      assert.throws(function () { derived.toBuffer(invalid); });
      var hasError = false;
      derived.isValid(invalid, {errorHook: function (path, any, type) {
        hasError = true;
        assert.deepEqual(path, ['age']);
        assert.equal(any, -1);
        assert(type instanceof AgeType);
      }});
      assert(hasError);
    });

    it('recursive', function () {

      function Person(friends) { this.friends = friends || []; }

      function PersonType(attrs, opts) {
        types.LogicalType.call(this, attrs, opts);
      }
      util.inherits(PersonType, types.LogicalType);

      PersonType.prototype._fromValue = function (val) {
        return new Person(val.friends);
      };

      PersonType.prototype._toValue = function (val) { return val; };

      var t = createType({
        type: 'record',
        name: 'Person',
        namespace: 'earth',
        logicalType: 'person',
        fields: [
          {name: 'friends', type: {type: 'array', items: 'Person'}},
        ]
      }, {logicalTypes: {'person': PersonType}});

      var p1 = new Person([new Person()]);
      var buf = t.toBuffer(p1);
      var p2 = t.fromBuffer(buf);
      assert(p2 instanceof Person);
      assert(p2.friends[0] instanceof Person);
      assert.deepEqual(p2, p1);
    });

    it('resolve underlying > logical', function () {
      var t1 = createType({type: 'string'});
      var t2 = createType({
        type: 'long',
        logicalType: 'date'
      }, {logicalTypes: logicalTypes});

      var d1 = new Date(Date.now());
      var buf = t1.toBuffer('' + d1);
      var res = t2.createResolver(t1);
      assert.throws(function () { t2.createResolver(createType('float')); });
      var d2 = t2.fromBuffer(buf, res);
      assert.deepEqual('' + d2, '' + d1); // Rounding error on date objects.
    });

    it('resolve logical > underlying', function () {
      var t1 = createType({
        type: 'long',
        logicalType: 'date'
      }, {logicalTypes: logicalTypes});
      var t2 = createType({type: 'double'}); // Note long > double too.

      var d = new Date(Date.now());
      var buf = t1.toBuffer(d);
      var res = t2.createResolver(t1);
      assert.throws(function () { createType('int').createResolver(t1); });
      assert.equal(t2.fromBuffer(buf, res), +d);
    });

    describe('union logical types', function () {
      // Unions are slightly tricky to override with logical types since their
      // schemas aren't represented as objects.

      var schema = [
        'null',
        {
          name: 'Person',
          type: 'record',
          fields: [
            {name: 'name', type: 'string'},
            {name: 'age', type: ['null', 'int'], 'default': null}
          ]
        }
      ];

      function createUnionTypeHook(Type) {
        var visited = [];
        return function(attrs, opts) {
          if (attrs instanceof Array && !~visited.indexOf(attrs)) {
            visited.push(attrs);
            return new Type(attrs, opts);
          }
        };
      }

      it('unwrapped', function () {

        /**
        * A generic union type which exposes its values directly.
        *
        * This implementation is pretty minimal, we could optimize it by caching
        * the underlying union's type names for example.
        *
        */
        function UnwrappedUnionType(attrs, opts) {
          types.LogicalType.call(this, attrs, opts);
        }
        util.inherits(UnwrappedUnionType, types.LogicalType);

        UnwrappedUnionType.prototype._fromValue = function (val) {
          return val === null ? null : val[Object.keys(val)[0]];
        };

        UnwrappedUnionType.prototype._toValue = function (any) {
          return this.getUnderlyingType().clone(any, {wrapUnions: true});
        };

        var t1 = createType(
          schema,
          {typeHook: createUnionTypeHook(UnwrappedUnionType)}
        );
        var obj = {name: 'Ann', age: 23};
        assert(t1.isValid(obj));
        var buf = t1.toBuffer(obj);
        var t2 = createType(schema);
        assert.deepEqual(
          t2.fromBuffer(buf),
          {Person: {name: 'Ann', age: {'int': 23}}}
        );

      });

      it('optional', function () {

        /**
        * A basic optional type.
        *
        * It assumes an underlying union of the form `["null", ???]`.
        *
        * Enhancements include:
        *
        * + Performing a check in the constructor on the underlying type (i.e.
        *   union with the correct form).
        * + Code-generating the conversion methods (especially a constructor
        *   for `_toValue`).
        *
        */
        function OptionalType(attrs, opts) {
          types.LogicalType.call(this, attrs, opts);
          var type = this.getUnderlyingType().getTypes()[1];
          this._name = type.getName() || type.getName(true);
        }
        util.inherits(OptionalType, types.LogicalType);

        OptionalType.prototype._fromValue = function (val) {
          return val === null ? null : val[this._name];
        };

        OptionalType.prototype._toValue = function (any) {
          if (any === null) {
            return null;
          }
          var obj = {};
          obj[this._name] = any;
          return obj;
        };

        var t1 = createType(
          schema,
          {typeHook: createUnionTypeHook(OptionalType)}
        );
        var obj = {name: 'Ann', age: 23};
        assert(t1.isValid(obj));
        var buf = t1.toBuffer(obj);
        var t2 = createType(schema);
        assert.deepEqual(
          t2.fromBuffer(buf),
          {Person: {name: 'Ann', age: {'int': 23}}}
        );

      });

    });

    it('even integer', function () {
      function EvenIntType(attrs, opts) {
        types.LogicalType.call(this, attrs, opts, [types.IntType]);
      }
      util.inherits(EvenIntType, types.LogicalType);
      EvenIntType.prototype._fromValue = function (val) {
        this._assertValid(val);
        return val;
      };
      EvenIntType.prototype._toValue = EvenIntType.prototype._fromValue;
      EvenIntType.prototype._assertValid = function (any) {
        if (any !== (any | 0) || any % 2) {
          throw new Error('invalid');
        }
      };

      var opts = {logicalTypes: {'even-integer': EvenIntType}};
      var t = createType({type: 'int', logicalType: 'even-integer'}, opts);
      assert(t.isValid(2));
      assert(!t.isValid(3));
      assert(!t.isValid('abc'));
      assert.equal(t.fromBuffer(Buffer.from([4])), 2);
      assert.equal(t.clone(4), 4);
      assert.equal(t.fromString('6'), 6);
      assert.throws(function () { t.clone(3); });
      assert.throws(function () { t.fromString('5'); });
      assert.throws(function () { t.toBuffer(3); });
      assert.throws(function () { t.fromBuffer(Buffer.from([2])); });
    });

  });

  describe('createType', function  () {

    it('null type', function () {
      assert.throws(function () { createType(null); });
    });

    it('unknown types', function () {
      assert.throws(function () { createType('a'); });
      assert.throws(function () { createType({type: 'b'}); });
    });

    it('namespaced type', function () {
      var type = createType({
        type: 'record',
        name: 'Human',
        namespace: 'earth',
        fields: [
          {
            name: 'id',
            type: {type: 'fixed', name: 'Id', size: 2, namespace: 'all'}
          },
          {
            name: 'alien',
            type: {
              type: 'record',
              name: 'Alien',
              namespace: 'all',
              fields: [
                {name: 'friend', type: 'earth.Human'},
                {name: 'id', type: 'Id'},
              ]
            }
          }
        ]
      });
      assert.equal(type._name, 'earth.Human');
      assert.equal(type._fields[0]._type._name, 'all.Id');
      assert.equal(type._fields[1]._type._name, 'all.Alien');
    });

    it('wrapped primitive', function () {
      var type = createType({
        type: 'record',
        name: 'Person',
        fields: [{name: 'nothing', type: {type: 'null'}}]
      });
      assert.strictEqual(type._fields[0]._type.constructor, types.NullType);
    });

    it('fromBuffer truncated', function () {
      var type = createType('int');
      assert.throws(function () {
        type.fromBuffer(Buffer.from([128]));
      });
    });

    it('fromBuffer bad resolver', function () {
      var type = createType('int');
      assert.throws(function () {
        type.fromBuffer(Buffer.from([0]), 123, {});
      });
    });

    it('fromBuffer trailing', function () {
      var type = createType('int');
      assert.throws(function () {
        type.fromBuffer(Buffer.from([0, 2]));
      });
    });

    it('fromBuffer trailing with resolver', function () {
      var type = createType('int');
      var resolver = type.createResolver(createType(['int']));
      assert.equal(type.fromBuffer(Buffer.from([0, 2]), resolver), 1);
    });

    it('toBuffer', function () {
      var type = createType('int');
      assert.throws(function () { type.toBuffer('abc'); });
      assert.doesNotThrow(function () { type.toBuffer(123); });
    });

    it('toBuffer and resize', function () {
      var type = createType('string');
      assert.deepEqual(type.toBuffer('\x01', 1), Buffer.from([2, 1]));
    });

    it('type hook', function () {
      var refs = [];
      var ts = [];
      var o = {
        type: 'record',
        name: 'Human',
        fields: [
          {name: 'age', type: 'int'},
          {name: 'name', type: {type: 'string'}}
        ]
      };
      createType(o, {typeHook: hook});
      assert.equal(ts.length, 1);
      assert.equal(ts[0].getName(), 'Human');

      function hook(schema, opts) {
        if (~refs.indexOf(schema)) {
          // Already seen this schema.
          return;
        }
        refs.push(schema);

        var type = createType(schema, opts);
        if (type instanceof types.RecordType) {
          ts.push(type);
        }
        return type;
      }
    });

    it('type hook invalid return value', function () {
      assert.throws(function () {
        createType({type: 'int'}, {typeHook: hook});
      });

      function hook() { return 'int'; }
    });

    it('fingerprint', function () {
      var t = createType('int');
      var buf = Buffer.from('ef524ea1b91e73173d938ade36c1db32', 'hex');
      assert.deepEqual(t.getFingerprint('md5'), buf);
      assert.deepEqual(t.getFingerprint(), buf);
    });

    it('getSchema default', function () {
      var type = createType({
        type: 'record',
        name: 'Human',
        fields: [
          {name: 'id1', type: ['string', 'null'], 'default': ''},
          {name: 'id2', type: ['null', 'string'], 'default': null}
        ]
      });
      assert.deepEqual(
        JSON.parse(type.getSchema()),
        {
          type: 'record',
          name: 'Human',
          fields: [
            {name: 'id1', type: ['string', 'null']}, // Stripped defaults.
            {name: 'id2', type: ['null', 'string']}
          ]
        }
      );
    });

  });

  describe('fromString', function () {

    it('int', function () {
      var t = createType('int');
      assert.equal(t.fromString('2'), 2);
      assert.throws(function () { t.fromString('"a"'); });
    });

    it('string', function () {
      var t = createType('string');
      assert.equal(t.fromString('"2"'), '2');
      assert.throws(function () { t.fromString('a'); });
    });

    it('coerce buffers', function () {
      var t = createType({
        name: 'Ids',
        type: 'record',
        fields: [{name: 'id1', type: {name: 'Id1', type: 'fixed', size: 2}}]
      });
      var o = {id1: Buffer.from([0, 1])};
      var s = '{"id1": "\\u0000\\u0001"}';
      var c = t.fromString(s);
      assert.deepEqual(c, o);
      assert(c instanceof t.getRecordConstructor());
    });

  });

  describe('toString', function () {

    it('int', function () {
      var t = createType('int');
      assert.equal(t.toString(2), '2');
      assert.throws(function () { t.toString('a'); });
    });

  });

  describe('resolve', function () {

    it('non type', function () {
      var t = createType({type: 'map', values: 'int'});
      var obj = {type: 'map', values: 'int'};
      assert.throws(function () { t.createResolver(obj); });
    });

    it('union to valid union', function () {
      var t1 = createType(['int', 'string']);
      var t2 = createType(['null', 'string', 'long']);
      var resolver = t2.createResolver(t1);
      var buf = t1.toBuffer({'int': 12});
      assert.deepEqual(t2.fromBuffer(buf, resolver), {'long': 12});
    });

    it('union to invalid union', function () {
      var t1 = createType(['int', 'string']);
      var t2 = createType(['null', 'long']);
      assert.throws(function () { t2.createResolver(t1); });
    });

    it('union to non union', function () {
      var t1 = createType(['int', 'long']);
      var t2 = createType('long');
      var resolver = t2.createResolver(t1);
      var buf = t1.toBuffer({'int': 12});
      assert.equal(t2.fromBuffer(buf, resolver), 12);
      buf = Buffer.from([4, 0]);
      assert.throws(function () { t2.fromBuffer(buf, resolver); });
    });

    it('union to invalid non union', function () {
      var t1 = createType(['int', 'long']);
      var t2 = createType('int');
      assert.throws(function() { t2.createResolver(t1); });
    });

  });

  describe('type references', function () {

    it('existing', function () {
      var type = createType({
        type: 'record',
        name: 'Person',
        fields: [{name: 'so', type: 'Person'}]
      });
      assert.strictEqual(type, type._fields[0]._type);
    });

    it('namespaced', function () {
      var type = createType({
        type: 'record',
        name: 'Person',
        fields: [
          {
            name: 'so',
            type: {
              type: 'record',
              name: 'Person',
              fields: [{name: 'age', type: 'int'}],
              namespace: 'a'
            }
          }
        ]
      });
      assert.equal(type._name, 'Person');
      assert.equal(type._fields[0]._type._name, 'a.Person');
    });

    it('redefining', function () {
      assert.throws(function () {
        createType({
          type: 'record',
          name: 'Person',
          fields: [
            {
              name: 'so',
              type: {
                type: 'record',
                name: 'Person',
                fields: [{name: 'age', type: 'int'}]
              }
            }
          ]
        });
      });
    });

    it('missing', function () {
      assert.throws(function () {
        createType({
          type: 'record',
          name: 'Person',
          fields: [{name: 'so', type: 'Friend'}]
        });
      });
    });

    it('redefining primitive', function () {
      assert.throws( // Unqualified.
        function () { createType({type: 'fixed', name: 'int', size: 2}); }
      );
      assert.throws( // Qualified.
        function () {
          createType({type: 'fixed', name: 'int', size: 2, namespace: 'a'});
        }
      );
    });

    it('aliases', function () {
      var type = createType({
        type: 'record',
        name: 'Person',
        namespace: 'a',
        aliases: ['Human', 'b.Being'],
        fields: [{name: 'age', type: 'int'}]
      });
      assert.deepEqual(type._aliases, ['a.Human', 'b.Being']);
    });

    it('invalid', function () {
      // Name.
      assert.throws(function () {
        createType({type: 'fixed', name: 'ID$', size: 3});
      });
      // Namespace.
      assert.throws(function () {
        createType({type: 'fixed', name: 'ID', size: 3, namespace: '1a'});
      });
      // Qualified.
      assert.throws(function () {
        createType({type: 'fixed', name: 'a.2.ID', size: 3});
      });
    });

  });

  describe('decode', function () {

    it('long valid', function () {
      var t = createType('long');
      var buf = Buffer.from([0, 128, 2, 0]);
      var res = t.decode(buf, 1);
      assert.deepEqual(res, {value: 128, offset: 3});
    });

    it('bytes invalid', function () {
      var t = createType('bytes');
      var buf = Buffer.from([4, 1]);
      var res = t.decode(buf, 0);
      assert.deepEqual(res, {value: undefined, offset: -1});
    });

  });

  describe('encode', function () {

    it('int valid', function () {
      var t = createType('int');
      var buf = Buffer.alloc(2);
      buf.fill(0);
      var n = t.encode(5, buf, 1);
      assert.equal(n, 2);
      assert.deepEqual(buf, Buffer.from([0, 10]));
    });

    it('string invalid', function () {
      var t = createType('string');
      var buf = Buffer.alloc(1);
      var n = t.encode('\x01\x02', buf, 0);
      assert.equal(n, -2);
    });

    it('invalid', function () {
      var t = createType('float');
      var buf = Buffer.alloc(2);
      assert.throws(function () { t.encode('hi', buf, 0); });
    });

  });

  describe('inspect', function () {

    it('type', function () {
      assert.equal(createType('int').inspect(), '<IntType>');
      assert.equal(
        createType({type: 'map', values: 'string'}).inspect(),
        '<MapType {"values":"string"}>'
      );
      assert.equal(
        createType({type: 'fixed', name: 'Id', size: 2}).inspect(),
        '<FixedType "Id">'
      );
    });

    it('field', function () {
      var type = createType({
        type: 'record',
        name: 'Person',
        fields: [{name: 'age', type: 'int'}]
      });
      var field = type.getFields()[0];
      assert.equal(field.inspect(), '<Field "age">');
    });

    it('resolver', function () {
      var t1 = createType('int');
      var t2 = createType('double');
      var resolver = t2.createResolver(t1);
      assert.equal(resolver.inspect(), '<Resolver>');
    });

  });

  it('reset', function () {
    types.Type.__reset(0);
    var t = createType('string');
    var buf = t.toBuffer('\x01');
    assert.deepEqual(buf, Buffer.from([2, 1]));
  });

});

function testType(Type, data, invalidSchemas) {

  data.forEach(function (elem) {
    it('roundtrip', function () {
      var type = new Type(elem.schema);
      elem.valid.forEach(function (v) {
        assert(type.isValid(v), '' + v);
        var fn = elem.check || assert.deepEqual;
        fn(type.fromBuffer(type.toBuffer(v)), v);
        fn(type.fromString(type.toString(v), {coerceBuffers: true}), v);
      });
      elem.invalid.forEach(function (v) {
        assert(!type.isValid(v), '' + v);
        assert.throws(function () { type.isValid(v, {errorHook: hook}); });
        assert.throws(function () { type.toBuffer(v); });

        function hook() { throw new Error(); }
      });
      var n = 50;
      while (n--) {
        // Run a few times to make sure we cover any branches.
        assert(type.isValid(type.random()));
      }
    });
  });

  it('skip', function () {
    data.forEach(function (elem) {
      var fn = elem.check || assert.deepEqual;
      var items = elem.valid;
      if (items.length > 1) {
        var type = new Type(elem.schema);
        var buf = Buffer.alloc(1024);
        var tap = new Tap(buf);
        type._write(tap, items[0]);
        type._write(tap, items[1]);
        tap.pos = 0;
        type._skip(tap);
        fn(type._read(tap), items[1]);
      }
    });
  });

  if (invalidSchemas) {
    it('invalid', function () {
      invalidSchemas.forEach(function (schema) {
        assert.throws(function () { new Type(schema); });
      });
    });
  }

}

function getResolver(reader, writer) {
  return createType(reader).createResolver(createType(writer));
}

function floatEquals(a, b) {
  return Math.abs((a - b) / Math.min(a, b)) < 1e-7;
}

function invert(buf) {
  var len = buf.length;
  while (len--) {
    buf[len] = ~buf[len];
  }
}
