import { expect } from 'chai';
import sinon from 'sinon';

import { PingPongState } from '../lib/PingPongState';

function createAndStart({
  PING_INTERVAL_MS,
  PONG_TIMEOUT_MS,
  PING_FLOOD_THRESHOLD,
}: any) {
  const peerClient = {
    reconnect: sinon.stub(),
    disconnect: sinon.stub(),
    sendMessage: sinon.stub(),
    logger: {
      debug: sinon.stub(),
    },
  };
  const sut = new PingPongState(peerClient as any);
  if (PING_INTERVAL_MS) {
    sut.PING_INTERVAL_MS = PING_INTERVAL_MS;
  }
  if (PONG_TIMEOUT_MS) {
    sut.PONG_TIMEOUT_MS = PONG_TIMEOUT_MS;
  }
  if (PING_FLOOD_THRESHOLD) {
    sut.PING_FLOOD_THRESHOLD = PING_FLOOD_THRESHOLD;
  }
  sut.start();
  return sut;
}

function wait(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

describe('pingpong-state', () => {
  let sut;

  afterEach(() => {
    sut.onDisconnecting();
  });

  describe('when ping interval expires', () => {
    describe('when no message has been received', () => {
      it('it should send a ping', async () => {
        sut = createAndStart({ PING_INTERVAL_MS: 10 });
        await wait(15);
        expect(sut._peerClient.sendMessage.args[0][0]).to.deep.equal({
          type: 18,
          numPongBytes: 1,
          ignored: Buffer.alloc(0),
        });
      });
    });
    describe('when last message is old', () => {
      it('it should send a ping', async () => {
        sut = createAndStart({ PING_INTERVAL_MS: 10 });
        await wait(15);
        sut.onMessage({ type: 258 });
        expect(sut._peerClient.sendMessage.args[0][0]).to.deep.equal({
          type: 18,
          numPongBytes: 1,
          ignored: Buffer.alloc(0),
        });
      });
    });
    describe('when message has been received', () => {
      it('it should not send ping', async () => {
        sut = createAndStart({ PING_INTERVAL_MS: 10 });
        await wait(5);
        sut.onMessage({ type: 258 });
        await wait(10);
        expect(sut._peerClient.sendMessage.callCount).to.equal(0);
      });
    });
  });

  describe('when pong expires', () => {
    it('it should reconnect', async () => {
      sut = createAndStart({ PING_INTERVAL_MS: 10, PONG_TIMEOUT_MS: 1 });
      await wait(15);
      expect(sut._peerClient.reconnect.callCount).to.equal(1);
    });
  });

  describe('when pong received', () => {
    it('it should cancel pong timeout', async () => {
      sut = createAndStart({ PING_INTERVAL_MS: 10, PONG_TIMEOUT_MS: 3 });
      await wait(12);
      sut.onMessage({ type: 19, ignored: Buffer.alloc(1) });
      await wait(5);
      expect(sut._peerClient.disconnect.callCount).to.equal(0);
    });
    it('it should be happy if valid pong', async () => {
      sut = createAndStart({ PING_INTERVAL_MS: 10, PONG_TIMEOUT_MS: 3 });
      await wait(12);
      sut.onMessage({ type: 19, ignored: Buffer.alloc(1) });
      expect(sut._peerClient.disconnect.callCount).to.equal(0);
    });
    it('it should disconnect if invalid pong', async () => {
      sut = createAndStart({ PING_INTERVAL_MS: 10, PONG_TIMEOUT_MS: 3 });
      await wait(12);
      sut.onMessage({ type: 19, ignored: Buffer.alloc(2) });
      expect(sut._peerClient.disconnect.callCount).to.equal(1);
    });
  });

  describe('when ping received', () => {
    it('it should not send ping when numPongBytes gte 65532', () => {
      sut = createAndStart({ PING_INTERVAL_MS: 10, PONG_TIMEOUT_MS: 3 });
      sut.onMessage({ type: 18, numPongBytes: 65532 });
      expect(sut._peerClient.disconnect.callCount).to.equal(0);
    });
    it('it should send correct pong', () => {
      sut = createAndStart({ PING_INTERVAL_MS: 10, PONG_TIMEOUT_MS: 3 });
      sut.onMessage({ type: 18, numPongBytes: 1 });
      expect(sut._peerClient.sendMessage.args[0][0]).to.deep.equal({
        type: 19,
        ignored: Buffer.alloc(1),
      });
    });
    it('it should disconnect if ping flood', () => {
      sut = createAndStart({ PING_FLOOD_THRESHOLD: 2 });
      sut.onMessage({ type: 18, numPongBytes: 1 });
      sut.onMessage({ type: 18, numPongBytes: 1 });
      sut.onMessage({ type: 18, numPongBytes: 1 });
      expect(sut._peerClient.disconnect.callCount).to.equal(1);
    });
  });

  describe('normal ping frequency', () => {
    it('should not trigger disconnect', async () => {
      sut = createAndStart({ PING_FLOOD_THRESHOLD: 2, PING_INTERVAL_MS: 10 });
      sut.onMessage({ type: 18, numPongBytes: 1 });
      await wait(10);
      sut.onMessage({ type: 18, numPongBytes: 1 });
      await wait(10);
      sut.onMessage({ type: 18, numPongBytes: 1 });
      await wait(10);
      sut.onMessage({ type: 18, numPongBytes: 1 });
      await wait(10);
      expect(sut._peerClient.disconnect.callCount).to.equal(0);
    });
  });
});
