import assert from 'assert';
import { hex, unhex } from './testutil.js';
import { decode, encode, sizeOf } from '../src/types.js';

describe('types.js', () => {
  it('can handle BYTE', () => {
    assert.equal(hex(encode.BYTE(0xFE)), 'FE');
    assert.equal(sizeOf.BYTE(0xFE), 1);
  });

  it('can handle CHAR', () => {
    assert.equal(hex(encode.CHAR('@')), '40');
    assert.equal(sizeOf.CHAR('@'), 1);
  });

  it('can handle CHARARRAY', () => {
    assert.equal(hex(encode.CHARARRAY('A/B')), '41 2F 42');
    assert.equal(sizeOf.CHARARRAY('A/B'), 3);
  });

  it('can handle undefined as CHARARRAY', () => {
    assert.equal(hex(encode.CHARARRAY(undefined)), '');
    assert.equal(sizeOf.CHARARRAY(undefined), 0);
  });

  it('can handle USHORT', () => {
    assert.equal(hex(encode.USHORT(0xCAFE)), 'CA FE');
    assert.equal(sizeOf.USHORT(0xCAFE), 2);
  });

  it('can handle SHORT', () => {
    assert.equal(hex(encode.USHORT(-345)), 'FE A7');
    assert.equal(sizeOf.SHORT(-345), 2);
  });

  it('can handle UINT24', () => {
    assert.equal(hex(encode.UINT24(0xABCDEF)), 'AB CD EF');
    assert.equal(sizeOf.UINT24(0xABCDEF), 3);
  });

  it('can handle ULONG', () => {
    assert.equal(hex(encode.ULONG(0xDEADBEEF)), 'DE AD BE EF');
    assert.equal(sizeOf.ULONG(0xDEADBEEF), 4);
  });

  it('can handle LONG', () => {
    assert.equal(hex(encode.LONG(-123456789)), 'F8 A4 32 EB');
    assert.equal(sizeOf.LONG(-123456789), 4);
  });

  it('can handle FIXED', () => {
    assert.equal(hex(encode.FIXED(0xBEEFCAFE)), 'BE EF CA FE');
    assert.equal(sizeOf.FIXED(0xBEEFCAFE), 4);
  });

  it('can handle FWORD', () => {
    assert.equal(hex(encode.FWORD(-8193)), 'DF FF');
    assert.equal(sizeOf.FWORD(-8193), 2);
  });

  it('can handle UFWORD', () => {
    assert.equal(hex(encode.UFWORD(0xDEED)), 'DE ED');
    assert.equal(sizeOf.UFWORD(0xDEED), 2);
  });

  // FIXME: Test LONGDATETIME when it gets implemented.

  it('can handle TAG', () => {
    assert.equal(hex(encode.TAG('Font')), '46 6F 6E 74');
    assert.equal(sizeOf.TAG('Font'), 4);
  });

  it('can handle Card8', () => {
    assert.equal(hex(encode.Card8(0xFE)), 'FE');
    assert.equal(sizeOf.Card8(0xFE), 1);
  });

  it('can handle Card16', () => {
    assert.equal(hex(encode.Card16(0xCAFE)), 'CA FE');
    assert.equal(sizeOf.Card16(0xCAFE), 2);
  });

  it('can handle OffSize', () => {
    assert.equal(hex(encode.OffSize(0xFE)), 'FE');
    assert.equal(sizeOf.OffSize(0xFE), 1);
  });

  it('can handle SID', () => {
    assert.equal(hex(encode.SID(0xCAFE)), 'CA FE');
    assert.equal(sizeOf.SID(0xCAFE), 2);
  });

  it('can handle NUMBER', () => {
    assert.equal(hex(encode.NUMBER(-32769)), '1D FF FF 7F FF');
    assert.equal(hex(encode.NUMBER(-32768)), '1C 80 00');
    assert.equal(hex(encode.NUMBER(-32767)), '1C 80 01');
    assert.equal(hex(encode.NUMBER(-1133)), '1C FB 93');
    assert.equal(hex(encode.NUMBER(-1132)), '1C FB 94');
    assert.equal(hex(encode.NUMBER(-1131)), 'FE FF');
    assert.equal(hex(encode.NUMBER(-109)), 'FB 01');
    assert.equal(hex(encode.NUMBER(-108)), 'FB 00');
    assert.equal(hex(encode.NUMBER(-107)), '20');
    assert.equal(hex(encode.NUMBER(-106)), '21');
    assert.equal(hex(encode.NUMBER(0)), '8B');
    assert.equal(hex(encode.NUMBER(107)), 'F6');
    assert.equal(hex(encode.NUMBER(108)), 'F7 00');
    assert.equal(hex(encode.NUMBER(109)), 'F7 01');
    assert.equal(hex(encode.NUMBER(1131)), 'FA FF');
    assert.equal(hex(encode.NUMBER(1132)), '1C 04 6C');
    assert.equal(hex(encode.NUMBER(1133)), '1C 04 6D');
    assert.equal(hex(encode.NUMBER(32767)), '1C 7F FF');
    assert.equal(hex(encode.NUMBER(32768)), '1D 00 00 80 00');
    assert.equal(hex(encode.NUMBER(32769)), '1D 00 00 80 01');

    assert.equal(sizeOf.NUMBER(-32769), 5);
    assert.equal(sizeOf.NUMBER(-32768), 3);
    assert.equal(sizeOf.NUMBER(-32767), 3);
    assert.equal(sizeOf.NUMBER(-1133), 3);
    assert.equal(sizeOf.NUMBER(-1132), 3);
    assert.equal(sizeOf.NUMBER(-1131), 2);
    assert.equal(sizeOf.NUMBER(-109), 2);
    assert.equal(sizeOf.NUMBER(-108), 2);
    assert.equal(sizeOf.NUMBER(-107), 1);
    assert.equal(sizeOf.NUMBER(-106), 1);
    assert.equal(sizeOf.NUMBER(0), 1);
    assert.equal(sizeOf.NUMBER(107), 1);
    assert.equal(sizeOf.NUMBER(108), 2);
    assert.equal(sizeOf.NUMBER(109), 2);
    assert.equal(sizeOf.NUMBER(1131), 2);
    assert.equal(sizeOf.NUMBER(1132), 3);
    assert.equal(sizeOf.NUMBER(1133), 3);
    assert.equal(sizeOf.NUMBER(32767), 3);
    assert.equal(sizeOf.NUMBER(32768), 5);
    assert.equal(sizeOf.NUMBER(32769), 5);
  });

  it('can handle NUMBER16', () => {
    assert.equal(hex(encode.NUMBER16(-32768)), '1C 80 00');
    assert.equal(hex(encode.NUMBER16(-1133)), '1C FB 93');
    assert.equal(hex(encode.NUMBER16(-108)), '1C FF 94');
    assert.equal(hex(encode.NUMBER16(0)), '1C 00 00');
    assert.equal(hex(encode.NUMBER16(108)), '1C 00 6C');
    assert.equal(hex(encode.NUMBER16(1133)), '1C 04 6D');
    assert.equal(hex(encode.NUMBER16(32767)), '1C 7F FF');

    assert.equal(sizeOf.NUMBER16(-32768), 3);
    assert.equal(sizeOf.NUMBER16(-1133), 3);
    assert.equal(sizeOf.NUMBER16(-108), 3);
    assert.equal(sizeOf.NUMBER16(0), 3);
    assert.equal(sizeOf.NUMBER16(108), 3);
    assert.equal(sizeOf.NUMBER16(1133), 3);
    assert.equal(sizeOf.NUMBER16(32767), 3);
  });

  it('can handle NUMBER32', () => {
    assert.equal(hex(encode.NUMBER32(-1)), '1D FF FF FF FF');
    assert.equal(hex(encode.NUMBER32(0)), '1D 00 00 00 00');
    assert.equal(hex(encode.NUMBER32(0xDEADBEEF)), '1D DE AD BE EF');

    assert.equal(sizeOf.NUMBER32(-1), 5);
    assert.equal(sizeOf.NUMBER32(0), 5);
    assert.equal(sizeOf.NUMBER32(0xDEADBEEF), 5);
  });

  it('can handle REAL', () => {
    // FIXME: It would be good if somebody who actually understands
    // how REAL.encode() works could write tests for edge cases.
    assert.equal(hex(encode.REAL(0.0)), '1E 0F');
    assert.equal(sizeOf.REAL(0.0), 2);

    assert.equal(hex(encode.REAL(0.1)), '1E 0A 1F');
    assert.equal(sizeOf.REAL(0.1), 3);

    assert.equal(hex(encode.REAL(99.999)), '1E 99 A9 99 FF');
    assert.equal(sizeOf.REAL(99.999), 5);

    assert.equal(hex(encode.REAL(-123456.78)), '1E E1 23 45 6A 78 FF');
    assert.equal(sizeOf.REAL(-123456.78), 7);
  });

  it('can handle NAME', () => {
    assert.equal(hex(encode.NAME('hello')), '68 65 6C 6C 6F');
    assert.equal(sizeOf.NAME('hello'), 5);
  });

  it('can handle STRING', () => {
    assert.equal(hex(encode.STRING('hello')), '68 65 6C 6C 6F');
    assert.equal(sizeOf.STRING('hello'), 5);
  });

  it('can handle UTF16', () => {
    assert.equal(decode.UTF16(unhex('DE AD 5B 57 4F 53'), 2, 4), '字体');
    assert.equal(hex(encode.UTF16('字体')), '5B 57 4F 53');
    assert.equal(sizeOf.UTF16('字体'), 4);

    // In JavaScript, characters outside the Basic Multilingual Plane
    // are represented with surrogate pairs. For example, U+1F404 COW
    // is stored as the surrogate pair U+D83D U+DC04. This is also
    // exactly what we need for representing U+1F404 in UTF-16.
    assert.equal(decode.UTF16(unhex('DE AD D8 3D DC 04'), 2, 4), '\uD83D\uDC04');
    assert.equal(hex(encode.UTF16('\uD83D\uDC04')), 'D8 3D DC 04');
    assert.equal(sizeOf.UTF16('\uD83D\uDC04'), 4);
  });

  it('can handle MACSTRING in Central European encoding', () => {
    const encoding = 'x-mac-ce';
    const data = '42 65 74 F5 74 92 70 75 73';

    assert.equal(
      decode.MACSTRING(unhex(`DE AD BE EF ${data}`), 4, 9, encoding),
      'Betűtípus');

    assert.equal(hex(encode.MACSTRING('Betűtípus', encoding)), data); // not in cache
    assert.equal(hex(encode.MACSTRING('Betűtípus', encoding)), data); // likely cached
    assert.equal(encode.MACSTRING('Betűtípus 字体', encoding), undefined); // not encodable

    assert.equal(sizeOf.MACSTRING('Betűtípus', encoding), 9);
    assert.equal(sizeOf.MACSTRING('Betűtípus 字体', encoding), 0);
  });

  it('can handle MACSTRING in Croatian encoding', () => {
    const encoding = 'x-mac-croatian';
    const data = 'A9 74 61 6D 70 61 E8';

    assert.equal(
      decode.MACSTRING(unhex(`DE AD BE EF ${data}`), 4, 7, encoding),
      'Štampač');

    assert.equal(hex(encode.MACSTRING('Štampač', encoding)), data); // not in cache
    assert.equal(hex(encode.MACSTRING('Štampač', encoding)), data); // likely cached
    assert.equal(encode.MACSTRING('Štampač 字体', encoding), undefined); // not encodable

    assert.equal(sizeOf.MACSTRING('Štampač', encoding), 7);
    assert.equal(sizeOf.MACSTRING('Štampač 字体', encoding), 0);
  });

  it('can handle MACSTRING in Cyrillic encoding', () => {
    const encoding = 'x-mac-cyrillic';
    const data = '98 F0 E8 F4 F2 20 46 6F 6F';

    assert.equal(
      decode.MACSTRING(unhex(`DE AD BE EF ${data}`), 4, 9, encoding),
      'Шрифт Foo');

    assert.equal(hex(encode.MACSTRING('Шрифт Foo', encoding)), data); // not in cache
    assert.equal(hex(encode.MACSTRING('Шрифт Foo', encoding)), data); // likely cached
    assert.equal(encode.MACSTRING('Шрифт 字体', encoding), undefined); // not encodable

    assert.equal(sizeOf.MACSTRING('Шрифт Foo', encoding), 9);
    assert.equal(sizeOf.MACSTRING('Шрифт 字体', encoding), 0);
  });

  it('can handle MACSTRING in Gaelic encoding', () => {
    const encoding = 'x-mac-gaelic';
    const data = '44 9C 69 E0 B6 92';

    assert.equal(
      decode.MACSTRING(unhex(`DE AD BE EF ${data}`), 4, 6, encoding),
      'Dúiṫċí');

    assert.equal(hex(encode.MACSTRING('Dúiṫċí', encoding)), data); // not in cache
    assert.equal(hex(encode.MACSTRING('Dúiṫċí', encoding)), data); // likely cached
    assert.equal(encode.MACSTRING('Dúiṫċí 字体', encoding), undefined);

    assert.equal(sizeOf.MACSTRING('Dúiṫċí Foo', encoding), 10);
    assert.equal(sizeOf.MACSTRING('Dúiṫċí 字体', encoding), 0);
  });

  it('can handle MACSTRING in Greek encoding', () => {
    const encoding = 'x-mac-greek';
    const data = 'A1 F2 E1 ED ED E1 F4 EF F3 E5 E9 F2 C0 20 2E 85 2E';

    assert.equal(
      decode.MACSTRING(unhex(`DE AD BE EF ${data}`), 4, 17, encoding),
      'Γραμματοσειρά .Ö.');

    assert.equal(hex(encode.MACSTRING('Γραμματοσειρά .Ö.', encoding)), data); // not in cache
    assert.equal(hex(encode.MACSTRING('Γραμματοσειρά .Ö.', encoding)), data); // likely cached
    assert.equal(encode.MACSTRING('Γραμματοσειρά 字体', encoding), undefined); // not encodable

    assert.equal(sizeOf.MACSTRING('Γραμματοσειρά .Ö.', encoding), 17);
    assert.equal(sizeOf.MACSTRING('Γραμματοσειρά 字体', encoding), 0);
  });

  it('can handle MACSTRING in Icelandic encoding', () => {
    const encoding = 'x-mac-icelandic';
    const data = 'DE 97 72 69 73 64 97 74 74 69 72 20 DF 97 20 61 DD 8E 67';

    assert.equal(
      decode.MACSTRING(unhex(`DE AD BE EF ${data}`), 4, 19, encoding),
      'Þórisdóttir þó aðég');

    assert.equal(hex(encode.MACSTRING('Þórisdóttir þó aðég', encoding)), data); // not in cache
    assert.equal(hex(encode.MACSTRING('Þórisdóttir þó aðég', encoding)), data); // likely cached
    assert.equal(encode.MACSTRING('Þórisdóttir 字体', encoding), undefined); // not encodable

    assert.equal(sizeOf.MACSTRING('Þórisdóttir þó aðég', encoding), 19);
    assert.equal(sizeOf.MACSTRING('Þórisdóttir 字体', encoding), 0);
  });

  it('can handle MACSTRING in Inuit encoding', () => {
    const encoding = 'x-mac-inuit';
    const data = '8A 80 8C 8B AB DA CC C6 93';

    assert.equal(
      decode.MACSTRING(unhex(`DE AD BE EF ${data}`), 4, 9, encoding),
      'ᐸᐃᑉᐹᒨᕆᔾᔪᑦ');

    assert.equal(hex(encode.MACSTRING('ᐸᐃᑉᐹᒨᕆᔾᔪᑦ', encoding)), data); // not in cache
    assert.equal(hex(encode.MACSTRING('ᐸᐃᑉᐹᒨᕆᔾᔪᑦ', encoding)), data); // likely cached
    assert.equal(encode.MACSTRING('ᐸᐃᑉᐹᒨᕆᔾᔪᑦ 字体', encoding), undefined);

    assert.equal(sizeOf.MACSTRING('ᐸᐃᑉᐹᒨᕆᔾᔪᑦ Foo', encoding), 13);
    assert.equal(sizeOf.MACSTRING('ᐸᐃᑉᐹᒨᕆᔾᔪᑦ 字体', encoding), 0);
  });

  it('can handle MACSTRING in Roman encoding', () => {
    const encoding = 'macintosh';
    const data = '86 65 74 6C 69 62 8A 72 67';

    assert.equal(
      decode.MACSTRING(unhex(`DE AD BE EF ${data}`), 4, 9, encoding),
      'Üetlibärg');

    assert.equal(hex(encode.MACSTRING('Üetlibärg', encoding)), data); // not in cache
    assert.equal(hex(encode.MACSTRING('Üetlibärg', encoding)), data); // likely cached
    assert.equal(encode.MACSTRING('Üetlibärg 字体', encoding), undefined); // not encodable

    assert.equal(sizeOf.MACSTRING('Üetlibärg', encoding), 9);
    assert.equal(sizeOf.MACSTRING('Üetlibärg 字体', encoding), 0);
  });

  it('can handle MACSTRING in Romanian encoding', () => {
    const encoding = 'x-mac-romanian';
    const data = '54 69 70 BE 72 69 72 65';

    assert.equal(
      decode.MACSTRING(unhex(`DE AD BE EF ${data}`), 4, 8, encoding),
      'Tipărire');

    assert.equal(hex(encode.MACSTRING('Tipărire', encoding)), data); // not in cache
    assert.equal(hex(encode.MACSTRING('Tipărire', encoding)), data); // likely cached
    assert.equal(encode.MACSTRING('Tipărire 字体', encoding), undefined); // not encodable

    assert.equal(sizeOf.MACSTRING('Tipărire', encoding), 8);
    assert.equal(sizeOf.MACSTRING('Tipărire 字体', encoding), 0);
  });

  it('can handle MACSTRING in Turkish encoding', () => {
    const encoding = 'x-mac-turkish';
    const data = '42 61 73 DD 6C 6D DD DF';

    assert.equal(
      decode.MACSTRING(unhex(`DE AD BE EF ${data}`), 4, 8, encoding),
      'Basılmış');

    assert.equal(hex(encode.MACSTRING('Basılmış', encoding)), data); // not in cache
    assert.equal(hex(encode.MACSTRING('Basılmış', encoding)), data); // likely cached
    assert.equal(encode.MACSTRING('Basılmış 字体', encoding), undefined); // not encodable

    assert.equal(sizeOf.MACSTRING('Basılmış', encoding), 8);
    assert.equal(sizeOf.MACSTRING('Basılmış 字体', encoding), 0);
  });

  it('rejects MACSTRING in unsupported encodings', () => {
    const encoding = 'KOI8-R';
    assert.equal(decode.MACSTRING(unhex('41 42'), 0, 1, encoding), undefined);
    assert.equal(encode.MACSTRING('AB', encoding), undefined);
    assert.equal(sizeOf.MACSTRING('AB', encoding), 0);
  });

  it('can handle INDEX', () => {
    assert.equal(hex(encode.INDEX([])), '00 00');
    assert.equal(sizeOf.INDEX([]), 2);

    const foo = { name: 'foo', type: 'STRING', value: 'hello' };
    const bar = { name: 'bar', type: 'NUMBER', value: 23 };
    assert.equal(hex(encode.INDEX([foo, bar])),
      '00 02 01 01 06 07 68 65 6C 6C 6F A2');
    assert.equal(sizeOf.INDEX([foo, bar]), 12);
  });

  it('can handle DICT', () => {
    assert.equal(hex(encode.DICT({})), '');
    assert.equal(sizeOf.DICT({}), 0);

    const foo = { name: 'foo', type: 'number', value: -1131 };
    const bar = { name: 'bar', type: 'number', value: 1131 };
    assert.equal(hex(encode.DICT({ 7: foo, 42: bar })), 'FE FF 07 FA FF 2A');
    assert.equal(sizeOf.DICT({ 7: foo, 42: bar }), 6);
  });

  it('can handle OPERATOR', () => {
    // FIXME: Somebody who knows CFF should double-check this.
    // Are there edge cases we should test here?
    assert.deepEqual(encode.OPERATOR(1199), [1199]);
    assert.deepEqual(encode.OPERATOR(1200), [12, 0]);
    assert.deepEqual(encode.OPERATOR(2399), [12, 1199]);
  });

  it('can handle OPERAND', () => {
    // FIXME: Somebody who knows CFF should double-check this.
    // Is it really the case that a SID operand becomes a single byte,
    // even though encode.SID would return '00 A2' here?
    assert.equal(hex(encode.OPERAND(23, 'SID')), 'A2');
    assert.equal(hex(encode.OPERAND(23, 'offset')), '1D 00 00 00 17');
    assert.equal(hex(encode.OPERAND(23, 'number')), 'A2');
    assert.equal(hex(encode.OPERAND(23.0, 'real')), '1E 23 FF');
  });

  it('can handle OP', () => {
    assert.equal(hex(encode.OP(0x42)), '42');
    assert.equal(sizeOf.OP(0x42), 1);
  });

  it('can handle CHARSTRING', () => {
    assert.equal(hex(encode.CHARSTRING([])), '');
    assert.equal(sizeOf.CHARSTRING([]), 0);

    // FIXME: Somebody who knows CFF should double-check this;
    // the result seems a little short.
    const ops = [
      { name: 'width', type: 'NUMBER', value: 42 },
      { name: 'dx', type: 'NUMBER', value: 17 },
      { name: 'dy', type: 'NUMBER', value: -23 },
      { name: 'rlineto', type: 'OP', value: 5 },
    ];

    // Because encode.CHARSTRING uses a cache, we call it twice
    // for testing both the uncached and the (likely) cached case.
    assert.equal(hex(encode.CHARSTRING(ops)), 'B5 9C 74 05');
    assert.equal(hex(encode.CHARSTRING(ops)), 'B5 9C 74 05');
    assert.equal(sizeOf.CHARSTRING(ops), 4);
  });

  it('can handle OBJECT', () => {
    const obj = { type: 'TAG', value: 'Font' };
    assert.equal(hex(encode.OBJECT(obj)), '46 6F 6E 74');
    assert.equal(sizeOf.OBJECT(obj), 4);
  });

  it('can handle TABLE', () => {
    const table = {
      fields: [
        { name: 'version', type: 'FIXED', value: 0x01234567 },
        { name: 'flags', type: 'USHORT', value: 0xBEEF },
      ],
    };
    assert.equal(hex(encode.TABLE(table)), '01 23 45 67 BE EF');
    assert.equal(sizeOf.TABLE(table), 6);
  });

  it('can handle subTABLEs', () => {
    const table = {
      fields: [
        { name: 'version', type: 'FIXED', value: 0x01234567 },
        {
          name: 'subtable',
          type: 'TABLE',
          value: {
            fields: [
              { name: 'flags', type: 'USHORT', value: 0xBEEF },
            ],
          },
        },
      ],
    };
    assert.equal(hex(encode.TABLE(table)), '01 23 45 67 00 06 BE EF');
    assert.equal(sizeOf.TABLE(table), 8);
  });

  it('can handle deeply nested TABLEs', () => {
    // First 58 bytes of Roboto-Black.ttf GSUB table.
    const expected = '00 01 00 00 00 0A 00 20 00 3A ' // header
            + '00 01 44 46 4C 54 00 08 00 04 00 00 00 00 FF FF 00 02 00 00 00 01 ' // script list
            + '00 02 6C 69 67 61 00 0E 73 6D 63 70 00 14 00 00 00 01 00 01 00 00 00 01 00 00'; // feature list

    const table = {
      fields: [
        { name: 'version', type: 'FIXED', value: 0x00010000 },
        { name: 'scriptList', type: 'TABLE' },
        { name: 'featureList', type: 'TABLE' },
        { name: 'lookupList', type: 'TABLE' },
      ],
      scriptList: {
        fields: [
          { name: 'scriptCount', type: 'USHORT', value: 1 },
          { name: 'scriptTag_0', type: 'TAG', value: 'DFLT' },
          {
            name: 'script_0',
            type: 'TABLE',
            value: {
              fields: [
                {
                  name: 'defaultLangSys',
                  type: 'TABLE',
                  value: {
                    fields: [
                      { name: 'lookupOrder', type: 'USHORT', value: 0 },
                      { name: 'reqFeatureIndex', type: 'USHORT', value: 0xffff },
                      { name: 'featureCount', type: 'USHORT', value: 2 },
                      { name: 'featureIndex_0', type: 'USHORT', value: 0 },
                      { name: 'featureIndex_1', type: 'USHORT', value: 1 },
                    ],
                  },
                },
                { name: 'langSysCount', type: 'USHORT', value: 0 },
              ],
            },
          },
        ],
      },
      featureList: {
        fields: [
          { name: 'featureCount', type: 'USHORT', value: 2 },
          { name: 'featureTag_0', type: 'TAG', value: 'liga' },
          {
            name: 'feature_0',
            type: 'TABLE',
            value: {
              fields: [
                { name: 'featureParams', type: 'USHORT', value: 0 },
                { name: 'lookupCount', type: 'USHORT', value: 1 },
                { name: 'lookupListIndex', type: 'USHORT', value: 1 },
              ],
            },
          },
          { name: 'featureTag_1', type: 'TAG', value: 'smcp' },
          {
            name: 'feature_1',
            type: 'TABLE',
            value: {
              fields: [
                { name: 'featureParams', type: 'USHORT', value: 0 },
                { name: 'lookupCount', type: 'USHORT', value: 1 },
                { name: 'lookupListIndex', type: 'USHORT', value: 0 },
              ],
            },
          },
        ],
      },
      lookupList: { fields: [] },
    };

    assert.equal(hex(encode.TABLE(table)), expected);
    assert.equal(sizeOf.TABLE(table), 58);
  });

  it('can handle RECORD', () => {
    const table = {
      fields: [
        { name: 'version', type: 'FIXED', value: 0x01234567 },
        { name: 'record', type: 'RECORD' },
      ],
    };

    table.record = {
      fields: [
        { name: 'flags_0', type: 'USHORT', value: 0xDEAF },
        { name: 'flags_1', type: 'USHORT', value: 0xCAFE },
      ],
    };

    assert.equal(hex(encode.TABLE(table)), '01 23 45 67 DE AF CA FE');
    assert.equal(sizeOf.TABLE(table), 8);
  });

  it('can handle LITERAL', () => {
    assert.equal(hex(encode.LITERAL([])), '');
    assert.equal(sizeOf.LITERAL([]), 0);

    assert.equal(hex(encode.LITERAL([0xff, 0x23, 0xA7])), 'FF 23 A7');
    assert.equal(sizeOf.LITERAL([0xff, 0x23, 0xA7]), 3);
  });

  it('can encode VARDELTAS', () => {
    const e = function (deltas) {
      return hex(encode.VARDELTAS(deltas));
    };
    assert.equal(e([]), '');

    // zeroes
    assert.equal(e([0]), '80');
    assert.equal(e(new Array(64).fill(0)), 'BF');
    assert.equal(e(new Array(65).fill(0)), 'BF 80');
    assert.equal(e(new Array(100).fill(0)), 'BF A3');
    assert.equal(e(new Array(256).fill(0)), 'BF BF BF BF');

    // bytes
    assert.equal(e([1]), '00 01');
    assert.equal(e([1, 2, 3, 127, -128, -1, -2]), '06 01 02 03 7F 80 FF FE');
    assert.equal(e(new Array(64).fill(127)),
      `3F ${(new Array(64).fill('7F')).join(' ')}`);
    assert.equal(e(new Array(65).fill(127)),
      `3F ${(new Array(64).fill('7F')).join(' ')} 00 7F`);

    // words
    assert.equal(e([0x6666]), '40 66 66');
    assert.equal(e([0x6666, 32767, -1, -32768]), '43 66 66 7F FF FF FF 80 00');
    assert.equal(e(new Array(64).fill(0x1122)),
      `7F ${(new Array(64).fill('11 22')).join(' ')}`);
    assert.equal(e(new Array(65).fill(0x1122)),
      `7F ${(new Array(64).fill('11 22')).join(' ')} 40 11 22`);

    // bytes, zeroes
    assert.equal(e([1, 0]), '01 01 00');
    assert.equal(e([1, 0, 0]), '00 01 81');

    // bytes, zeroes, bytes:
    // a single zero is more compact when encoded within the bytes run
    assert.equal(e([127, 127, 0, 127, 127]), '04 7F 7F 00 7F 7F');
    // multiple zeroes are more compact when encoded into their own run
    assert.equal(e([127, 127, 0, 0, 127, 127]), '01 7F 7F 81 01 7F 7F');
    assert.equal(e([127, 127, 0, 0, 0, 127, 127]), '01 7F 7F 82 01 7F 7F');
    assert.equal(e([127, 127, 0, 0, 0, 0, 127, 127]), '01 7F 7F 83 01 7F 7F');

    // words, zeroes
    assert.equal(e([0x6789, 0]), '40 67 89 80');
    assert.equal(e([0x6666, 0, 0]), '40 66 66 81');

    // words, zeroes, bytes
    assert.equal(e([0x6666, 0, 1, 2, 3]), '40 66 66 80 02 01 02 03');
    assert.equal(e([0x6666, 0, 0, 1, 2, 3]), '40 66 66 81 02 01 02 03');
    assert.equal(e([0x6666, 0, 0, 0, 1, 2, 3]), '40 66 66 82 02 01 02 03');

    // words, zeroes, words
    assert.equal(e([0x6666, 0, 0x7777]), '40 66 66 80 40 77 77');
    assert.equal(e([0x6666, 0, 0, 0x7777]), '40 66 66 81 40 77 77');
    assert.equal(e([0x6666, 0, 0, 0, 0x7777]), '40 66 66 82 40 77 77');

    // words, bytes, words:
    // a single byte-encodable word is more compact when encoded within the words run
    assert.equal(e([0x6666, 2, 0x7777]), '42 66 66 00 02 77 77');
    // multiple byte-encodable words are more compacted when forming their own run
    assert.equal(e([0x6666, 2, 2, 0x7777]), '40 66 66 01 02 02 40 77 77');
  });
});
