import * as assert from 'assert';
import { describe, it } from 'mocha';
import * as bufferutils from '../src/bufferutils';
import { BufferReader, BufferWriter } from '../src/bufferutils';

import * as fixtures from './fixtures/bufferutils.json';
const varuint = require('varuint-bitcoin');

describe('bufferutils', () => {
  function concatToBuffer(values: number[][]): Buffer {
    return Buffer.concat(values.map(data => Buffer.from(data)));
  }

  describe('readUInt64LE', () => {
    fixtures.valid.forEach(f => {
      it('decodes ' + f.hex, () => {
        const buffer = Buffer.from(f.hex, 'hex');
        const num = bufferutils.readUInt64LE(buffer, 0);

        assert.strictEqual(num, f.dec);
      });
    });

    fixtures.invalid.readUInt64LE.forEach(f => {
      it('throws on ' + f.description, () => {
        const buffer = Buffer.from(f.hex, 'hex');

        assert.throws(() => {
          bufferutils.readUInt64LE(buffer, 0);
        }, new RegExp(f.exception));
      });
    });
  });

  describe('writeUInt64LE', () => {
    fixtures.valid.forEach(f => {
      it('encodes ' + f.dec, () => {
        const buffer = Buffer.alloc(8, 0);

        bufferutils.writeUInt64LE(buffer, f.dec, 0);
        assert.strictEqual(buffer.toString('hex'), f.hex);
      });
    });

    fixtures.invalid.readUInt64LE.forEach(f => {
      it('throws on ' + f.description, () => {
        const buffer = Buffer.alloc(8, 0);

        assert.throws(() => {
          bufferutils.writeUInt64LE(buffer, f.dec, 0);
        }, new RegExp(f.exception));
      });
    });
  });

  describe('BufferWriter', () => {
    function testBuffer(
      bufferWriter: BufferWriter,
      expectedBuffer: Buffer,
      expectedOffset: number = expectedBuffer.length,
    ): void {
      assert.strictEqual(bufferWriter.offset, expectedOffset);
      assert.deepStrictEqual(
        bufferWriter.buffer.slice(0, expectedOffset),
        expectedBuffer.slice(0, expectedOffset),
      );
    }

    it('writeUint8', () => {
      const values = [0, 1, 254, 255];
      const expectedBuffer = Buffer.from([0, 1, 0xfe, 0xff]);
      const bufferWriter = new BufferWriter(
        Buffer.allocUnsafe(expectedBuffer.length),
      );
      values.forEach((v: number) => {
        const expectedOffset = bufferWriter.offset + 1;
        bufferWriter.writeUInt8(v);
        testBuffer(bufferWriter, expectedBuffer, expectedOffset);
      });
      testBuffer(bufferWriter, expectedBuffer);
    });

    it('writeInt32', () => {
      const values = [
        0,
        1,
        Math.pow(2, 31) - 2,
        Math.pow(2, 31) - 1,
        -1,
        -Math.pow(2, 31),
      ];
      const expectedBuffer = concatToBuffer([
        [0, 0, 0, 0],
        [1, 0, 0, 0],
        [0xfe, 0xff, 0xff, 0x7f],
        [0xff, 0xff, 0xff, 0x7f],
        [0xff, 0xff, 0xff, 0xff],
        [0x00, 0x00, 0x00, 0x80],
      ]);
      const bufferWriter = new BufferWriter(
        Buffer.allocUnsafe(expectedBuffer.length),
      );
      values.forEach((value: number) => {
        const expectedOffset = bufferWriter.offset + 4;
        bufferWriter.writeInt32(value);
        testBuffer(bufferWriter, expectedBuffer, expectedOffset);
      });
      testBuffer(bufferWriter, expectedBuffer);
    });

    it('writeUInt32', () => {
      const maxUInt32 = Math.pow(2, 32) - 1;
      const values = [0, 1, Math.pow(2, 16), maxUInt32];
      const expectedBuffer = concatToBuffer([
        [0, 0, 0, 0],
        [1, 0, 0, 0],
        [0, 0, 1, 0],
        [0xff, 0xff, 0xff, 0xff],
      ]);
      const bufferWriter = new BufferWriter(
        Buffer.allocUnsafe(expectedBuffer.length),
      );
      values.forEach((value: number) => {
        const expectedOffset = bufferWriter.offset + 4;
        bufferWriter.writeUInt32(value);
        testBuffer(bufferWriter, expectedBuffer, expectedOffset);
      });
      testBuffer(bufferWriter, expectedBuffer);
    });

    it('writeUInt64', () => {
      const values = [
        0,
        1,
        Math.pow(2, 32),
        Number.MAX_SAFE_INTEGER /* 2^53 - 1 */,
      ];
      const expectedBuffer = concatToBuffer([
        [0, 0, 0, 0, 0, 0, 0, 0],
        [1, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 1, 0, 0, 0],
        [0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x1f, 0x00],
      ]);
      const bufferWriter = new BufferWriter(
        Buffer.allocUnsafe(expectedBuffer.length),
      );
      values.forEach((value: number) => {
        const expectedOffset = bufferWriter.offset + 8;
        bufferWriter.writeUInt64(value);
        testBuffer(bufferWriter, expectedBuffer, expectedOffset);
      });
      testBuffer(bufferWriter, expectedBuffer);
    });

    it('writeVarInt', () => {
      const values = [
        0,
        1,
        252,
        253,
        254,
        255,
        256,
        Math.pow(2, 16) - 2,
        Math.pow(2, 16) - 1,
        Math.pow(2, 16),
        Math.pow(2, 32) - 2,
        Math.pow(2, 32) - 1,
        Math.pow(2, 32),
        Number.MAX_SAFE_INTEGER,
      ];
      const expectedBuffer = concatToBuffer([
        [0x00],
        [0x01],
        [0xfc],
        [0xfd, 0xfd, 0x00],
        [0xfd, 0xfe, 0x00],
        [0xfd, 0xff, 0x00],
        [0xfd, 0x00, 0x01],
        [0xfd, 0xfe, 0xff],
        [0xfd, 0xff, 0xff],
        [0xfe, 0x00, 0x00, 0x01, 0x00],
        [0xfe, 0xfe, 0xff, 0xff, 0xff],
        [0xfe, 0xff, 0xff, 0xff, 0xff],
        [0xff, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00],
        [0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x1f, 0x00],
      ]);
      const bufferWriter = new BufferWriter(
        Buffer.allocUnsafe(expectedBuffer.length),
      );
      values.forEach((value: number) => {
        const expectedOffset =
          bufferWriter.offset + varuint.encodingLength(value);
        bufferWriter.writeVarInt(value);
        testBuffer(bufferWriter, expectedBuffer, expectedOffset);
      });
      testBuffer(bufferWriter, expectedBuffer);
    });

    it('writeSlice', () => {
      const values = [[], [1], [1, 2, 3, 4], [254, 255]];
      const expectedBuffer = concatToBuffer(values);
      const bufferWriter = new BufferWriter(
        Buffer.allocUnsafe(expectedBuffer.length),
      );
      values.forEach((v: number[]) => {
        const expectedOffset = bufferWriter.offset + v.length;
        bufferWriter.writeSlice(Buffer.from(v));
        testBuffer(bufferWriter, expectedBuffer, expectedOffset);
      });
      testBuffer(bufferWriter, expectedBuffer);
      assert.throws(() => {
        bufferWriter.writeSlice(Buffer.from([0, 0]));
      }, /^Error: Cannot write slice out of bounds$/);
    });

    it('writeVarSlice', () => {
      const values = [
        Buffer.alloc(1, 1),
        Buffer.alloc(252, 2),
        Buffer.alloc(253, 3),
      ];
      const expectedBuffer = Buffer.concat([
        Buffer.from([0x01, 0x01]),
        Buffer.from([0xfc]),
        Buffer.alloc(252, 0x02),
        Buffer.from([0xfd, 0xfd, 0x00]),
        Buffer.alloc(253, 0x03),
      ]);
      const bufferWriter = new BufferWriter(
        Buffer.allocUnsafe(expectedBuffer.length),
      );
      values.forEach((value: Buffer) => {
        const expectedOffset =
          bufferWriter.offset +
          varuint.encodingLength(value.length) +
          value.length;
        bufferWriter.writeVarSlice(value);
        testBuffer(bufferWriter, expectedBuffer, expectedOffset);
      });
      testBuffer(bufferWriter, expectedBuffer);
    });

    it('writeVector', () => {
      const values = [
        [Buffer.alloc(1, 4), Buffer.alloc(253, 5)],
        Array(253).fill(Buffer.alloc(1, 6)),
      ];
      const expectedBuffer = Buffer.concat([
        Buffer.from([0x02]),
        Buffer.from([0x01, 0x04]),
        Buffer.from([0xfd, 0xfd, 0x00]),
        Buffer.alloc(253, 5),

        Buffer.from([0xfd, 0xfd, 0x00]),
        Buffer.concat(
          Array(253)
            .fill(0)
            .map(() => Buffer.from([0x01, 0x06])),
        ),
      ]);

      const bufferWriter = new BufferWriter(
        Buffer.allocUnsafe(expectedBuffer.length),
      );
      values.forEach((value: Buffer[]) => {
        const expectedOffset =
          bufferWriter.offset +
          varuint.encodingLength(value.length) +
          value.reduce(
            (sum: number, v) =>
              sum + varuint.encodingLength(v.length) + v.length,
            0,
          );
        bufferWriter.writeVector(value);
        testBuffer(bufferWriter, expectedBuffer, expectedOffset);
      });
      testBuffer(bufferWriter, expectedBuffer);
    });
  });

  describe('BufferReader', () => {
    function testValue(
      bufferReader: BufferReader,
      value: Buffer | number,
      expectedValue: Buffer | number,
      expectedOffset: number = Buffer.isBuffer(expectedValue)
        ? expectedValue.length
        : 0,
    ): void {
      assert.strictEqual(bufferReader.offset, expectedOffset);
      if (Buffer.isBuffer(expectedValue)) {
        assert.deepStrictEqual(
          (value as Buffer).slice(0, expectedOffset),
          expectedValue.slice(0, expectedOffset),
        );
      } else {
        assert.strictEqual(value as number, expectedValue);
      }
    }

    it('readUint8', () => {
      const values = [0, 1, 0xfe, 0xff];
      const buffer = Buffer.from([0, 1, 0xfe, 0xff]);
      const bufferReader = new BufferReader(buffer);
      values.forEach((v: number) => {
        const expectedOffset = bufferReader.offset + 1;
        const val = bufferReader.readUInt8();
        testValue(bufferReader, val, v, expectedOffset);
      });
    });

    it('readInt32', () => {
      const values = [
        0,
        1,
        Math.pow(2, 31) - 2,
        Math.pow(2, 31) - 1,
        -1,
        -Math.pow(2, 31),
      ];
      const buffer = concatToBuffer([
        [0, 0, 0, 0],
        [1, 0, 0, 0],
        [0xfe, 0xff, 0xff, 0x7f],
        [0xff, 0xff, 0xff, 0x7f],
        [0xff, 0xff, 0xff, 0xff],
        [0x00, 0x00, 0x00, 0x80],
      ]);
      const bufferReader = new BufferReader(buffer);
      values.forEach((value: number) => {
        const expectedOffset = bufferReader.offset + 4;
        const val = bufferReader.readInt32();
        testValue(bufferReader, val, value, expectedOffset);
      });
    });

    it('readUInt32', () => {
      const maxUInt32 = Math.pow(2, 32) - 1;
      const values = [0, 1, Math.pow(2, 16), maxUInt32];
      const buffer = concatToBuffer([
        [0, 0, 0, 0],
        [1, 0, 0, 0],
        [0, 0, 1, 0],
        [0xff, 0xff, 0xff, 0xff],
      ]);
      const bufferReader = new BufferReader(buffer);
      values.forEach((value: number) => {
        const expectedOffset = bufferReader.offset + 4;
        const val = bufferReader.readUInt32();
        testValue(bufferReader, val, value, expectedOffset);
      });
    });

    it('readUInt64', () => {
      const values = [
        0,
        1,
        Math.pow(2, 32),
        Number.MAX_SAFE_INTEGER /* 2^53 - 1 */,
      ];
      const buffer = concatToBuffer([
        [0, 0, 0, 0, 0, 0, 0, 0],
        [1, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 1, 0, 0, 0],
        [0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x1f, 0x00],
      ]);
      const bufferReader = new BufferReader(buffer);
      values.forEach((value: number) => {
        const expectedOffset = bufferReader.offset + 8;
        const val = bufferReader.readUInt64();
        testValue(bufferReader, val, value, expectedOffset);
      });
    });

    it('readVarInt', () => {
      const values = [
        0,
        1,
        252,
        253,
        254,
        255,
        256,
        Math.pow(2, 16) - 2,
        Math.pow(2, 16) - 1,
        Math.pow(2, 16),
        Math.pow(2, 32) - 2,
        Math.pow(2, 32) - 1,
        Math.pow(2, 32),
        Number.MAX_SAFE_INTEGER,
      ];
      const buffer = concatToBuffer([
        [0x00],
        [0x01],
        [0xfc],
        [0xfd, 0xfd, 0x00],
        [0xfd, 0xfe, 0x00],
        [0xfd, 0xff, 0x00],
        [0xfd, 0x00, 0x01],
        [0xfd, 0xfe, 0xff],
        [0xfd, 0xff, 0xff],
        [0xfe, 0x00, 0x00, 0x01, 0x00],
        [0xfe, 0xfe, 0xff, 0xff, 0xff],
        [0xfe, 0xff, 0xff, 0xff, 0xff],
        [0xff, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00],
        [0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x1f, 0x00],
      ]);
      const bufferReader = new BufferReader(buffer);
      values.forEach((value: number) => {
        const expectedOffset =
          bufferReader.offset + varuint.encodingLength(value);
        const val = bufferReader.readVarInt();
        testValue(bufferReader, val, value, expectedOffset);
      });
    });

    it('readSlice', () => {
      const values = [[1], [1, 2, 3, 4], [254, 255]];
      const buffer = concatToBuffer(values);
      const bufferReader = new BufferReader(buffer);
      values.forEach((v: number[]) => {
        const expectedOffset = bufferReader.offset + v.length;
        const val = bufferReader.readSlice(v.length);
        testValue(bufferReader, val, Buffer.from(v), expectedOffset);
      });
      assert.throws(() => {
        bufferReader.readSlice(2);
      }, /^Error: Cannot read slice out of bounds$/);
    });

    it('readVarSlice', () => {
      const values = [
        Buffer.alloc(1, 1),
        Buffer.alloc(252, 2),
        Buffer.alloc(253, 3),
      ];
      const buffer = Buffer.concat([
        Buffer.from([0x01, 0x01]),
        Buffer.from([0xfc]),
        Buffer.alloc(252, 0x02),
        Buffer.from([0xfd, 0xfd, 0x00]),
        Buffer.alloc(253, 0x03),
      ]);
      const bufferReader = new BufferReader(buffer);
      values.forEach((value: Buffer) => {
        const expectedOffset =
          bufferReader.offset +
          varuint.encodingLength(value.length) +
          value.length;
        const val = bufferReader.readVarSlice();
        testValue(bufferReader, val, value, expectedOffset);
      });
    });

    it('readVector', () => {
      const values = [
        [Buffer.alloc(1, 4), Buffer.alloc(253, 5)],
        Array(253).fill(Buffer.alloc(1, 6)),
      ];
      const buffer = Buffer.concat([
        Buffer.from([0x02]),
        Buffer.from([0x01, 0x04]),
        Buffer.from([0xfd, 0xfd, 0x00]),
        Buffer.alloc(253, 5),

        Buffer.from([0xfd, 0xfd, 0x00]),
        Buffer.concat(
          Array(253)
            .fill(0)
            .map(() => Buffer.from([0x01, 0x06])),
        ),
      ]);

      const bufferReader = new BufferReader(buffer);
      values.forEach((value: Buffer[]) => {
        const expectedOffset =
          bufferReader.offset +
          varuint.encodingLength(value.length) +
          value.reduce(
            (sum: number, v) =>
              sum + varuint.encodingLength(v.length) + v.length,
            0,
          );
        const val = bufferReader.readVector();
        testValue(
          bufferReader,
          Buffer.concat(val),
          Buffer.concat(value),
          expectedOffset,
        );
      });
    });
  });
});