// tslint:disable: no-unused-expression
import { ShortChannelId } from '@node-dlc/common';
import { expect, use } from 'chai';

import { GossipRelay } from '../../lib/gossip/GossipRelay';
import { GossipRelayState } from '../../lib/gossip/GossipRelay';
import { ChannelAnnouncementMessage } from '../../lib/messages/ChannelAnnouncementMessage';
import { ChannelUpdateMessage } from '../../lib/messages/ChannelUpdateMessage';
import { NodeAnnouncementMessage } from '../../lib/messages/NodeAnnouncementMessage';
import { createFakeLogger, createFakePeer, wait } from '../_test-utils';

describe('GossipRelay', () => {
  let sut: GossipRelay;

  function createChanAnn(
    block: number,
    tx: number,
    output: number,
  ): ChannelAnnouncementMessage {
    const msg = new ChannelAnnouncementMessage();
    msg.shortChannelId = new ShortChannelId(block, tx, output);
    return msg;
  }

  function createChanUpd(
    block: number,
    tx: number,
    output: number,
    timestamp: number,
  ): ChannelUpdateMessage {
    const msg = new ChannelUpdateMessage();
    msg.shortChannelId = new ShortChannelId(block, tx, output);
    msg.timestamp = timestamp;
    return msg;
  }

  function createNodeAnn(
    nodeIdHex: string,
    timestamp: number,
  ): NodeAnnouncementMessage {
    const msg = new NodeAnnouncementMessage();
    msg.nodeId = Buffer.from(nodeIdHex, 'hex');
    msg.timestamp = timestamp;
    return msg;
  }

  beforeEach(() => {
    sut = new GossipRelay(createFakeLogger(), 100, 3);
  });

  describe('state: Inactive', () => {
    describe('on .start()', () => {
      beforeEach(() => {
        sut.start();
      });

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

      it('starts periodic flushing', async () => {
        const peer1 = createFakePeer();
        sut.addPeer(peer1);

        const msg1 = createChanAnn(0, 0, 0);
        sut.enqueue(msg1);

        await wait(200);

        expect(peer1.sendMessage.called).to.be.true;
        expect(peer1.sendMessage.args[0][0]).to.equal(msg1);
      });
      it('transitions state to Active', () => {
        sut.start();
        expect(sut.state).to.equal(GossipRelayState.Active);
      });
    });

    describe('on .stop()', () => {
      it('no-op', () => {
        sut.stop();
      });
    });

    describe('on enqueue', () => {
      it('ignores message', () => {
        sut.enqueue(createChanAnn(0, 0, 0));
        expect((sut as any)._queue.length).to.equal(0);
      });
    });
  });

  describe('state: Active', () => {
    beforeEach(() => {
      sut.start();
    });

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

    describe('on .start()', () => {
      it('no-op', () => {
        sut.start();
      });
    });

    describe('on .stop()', () => {
      it('stops flushing', async () => {
        // create a peer
        const peer1 = createFakePeer();
        sut.addPeer(peer1);

        // enqueue a message that normally would go to a peer
        const msg1 = createChanAnn(0, 0, 0);
        sut.enqueue(msg1);

        // stop activity before the timer would tick
        await wait(50);
        sut.stop();

        // wait until after the timer would normally have ticked
        await wait(200);

        // ensure no message was sent
        expect(peer1.sendMessage.called).to.be.false;
      });

      it('changes state to Inactive', () => {
        expect(sut.state).to.equal(GossipRelayState.Active);
        sut.stop();
        expect(sut.state).to.equal(GossipRelayState.Inactive);
      });
    });

    describe('on enqueue', () => {
      describe('channel_announcement', () => {
        it('adds new message', () => {
          sut.enqueue(createChanAnn(0, 0, 0));
          expect((sut as any)._queue.length).to.equal(1);
        });

        it('ignores duplicate', () => {
          sut.enqueue(createChanAnn(0, 0, 0));
          sut.enqueue(createChanAnn(0, 1, 0));
          expect((sut as any)._queue.length).to.equal(2);

          sut.enqueue(createChanAnn(0, 0, 0));
          expect((sut as any)._queue.length).to.equal(
            2,
            'should ignore duplicate',
          );
          expect((sut as any)._queue[0].shortChannelId.txIdx).to.equal(0, "duplicate should maintain original position"); // prettier-ignore
        });
      });

      describe('channel_update', () => {
        it('adds new message', () => {
          sut.enqueue(createChanUpd(0, 0, 0, 0));
          expect((sut as any)._queue.length).to.equal(1);
        });

        it('ignores duplicate', () => {
          sut.enqueue(createChanUpd(0, 0, 0, 0));
          sut.enqueue(createChanUpd(0, 1, 0, 0));
          expect((sut as any)._queue.length).to.equal(2);

          sut.enqueue(createChanUpd(0, 0, 0, 0));
          expect((sut as any)._queue.length).to.equal(
            2,
            'should ignore update',
          );
          expect((sut as any)._queue[0].shortChannelId.txIdx).to.equal(0, "duplicate should maintain original position"); // prettier-ignore
        });

        it('replaces outdated', () => {
          sut.enqueue(createChanUpd(0, 0, 0, 0));
          sut.enqueue(createChanUpd(1, 0, 0, 0));
          expect((sut as any)._queue.length).to.equal(2);

          sut.enqueue(createChanUpd(0, 0, 0, 1));
          expect((sut as any)._queue[0].shortChannelId.block).to.equal(1);
          expect((sut as any)._queue[1].shortChannelId.block).to.equal(0);
        });
      });

      describe('node_announcement', () => {
        it('adds new message', () => {
          sut.enqueue(createNodeAnn('00'.repeat(32), 0));
          expect((sut as any)._queue.length).to.equal(1);
        });

        it('ignores duplicate', () => {
          sut.enqueue(createNodeAnn('00'.repeat(32), 0));
          sut.enqueue(createNodeAnn('01'.repeat(32), 0));
          expect((sut as any)._queue.length).to.equal(2);

          sut.enqueue(createNodeAnn('00'.repeat(32), 0));
          expect((sut as any)._queue[0].nodeId.toString('hex')).to.equal(
            '00'.repeat(32),
          );
          expect((sut as any)._queue[1].nodeId.toString('hex')).to.equal(
            '01'.repeat(32),
          );
        });

        it('replaces outdated', () => {
          sut.enqueue(createNodeAnn('00'.repeat(32), 0));
          sut.enqueue(createNodeAnn('01'.repeat(32), 0));
          expect((sut as any)._queue.length).to.equal(2);

          sut.enqueue(createNodeAnn('00'.repeat(32), 1));
          expect((sut as any)._queue[0].nodeId.toString('hex')).to.equal(
            '01'.repeat(32),
          );
          expect((sut as any)._queue[1].nodeId.toString('hex')).to.equal(
            '00'.repeat(32),
          );
          expect((sut as any)._queue[1].timestamp).to.equal(1);
        });
      });
    });

    describe('on timer', () => {
      it('flushes to single message peer', () => {
        // create a fake peer and add
        const peer1 = createFakePeer();
        sut.addPeer(peer1);

        // push a new message
        const msg1 = createChanAnn(0, 0, 0);
        sut.enqueue(msg1);

        // manually trigger the timer for testing purposes
        (sut as any)._onTimer();

        expect(peer1.sendMessage.called).to.be.true;
        expect(peer1.sendMessage.args[0][0]).to.equal(msg1);
      });

      it('flushes to several messages peer', () => {
        // create a fake peer and add
        const peer1 = createFakePeer();
        sut.addPeer(peer1);

        // push messages
        const msg1 = createChanAnn(0, 0, 0);
        const msg2 = createChanAnn(1, 0, 0);
        sut.enqueue(msg1);
        sut.enqueue(msg2);

        // manually trigger the timer for testing purposes
        (sut as any)._onTimer();

        expect(peer1.sendMessage.called).to.be.true;
        expect(peer1.sendMessage.args[0][0]).to.equal(msg1);
        expect(peer1.sendMessage.args[1][0]).to.equal(msg2);
      });

      it('flushes new messages to peer', () => {
        // create a fake peer and add
        const peer1 = createFakePeer();
        sut.addPeer(peer1);

        // push messages
        const msg1 = createChanAnn(0, 0, 0);
        sut.enqueue(msg1);

        // manually trigger the timer for testing purposes
        (sut as any)._onTimer();

        // reset the stub
        peer1.sendMessage.reset();

        // push new message
        const msg2 = createChanAnn(1, 0, 0);
        sut.enqueue(msg2);

        // manually trigger the timer for testing purposes
        (sut as any)._onTimer();

        expect(peer1.sendMessage.called).to.be.true;
        expect(peer1.sendMessage.args[0][0]).to.equal(msg2);
      });

      it('flushes new messages to peers', () => {
        // create a fake peer and add
        const peer1 = createFakePeer();
        sut.addPeer(peer1);

        // push messages
        const msg1 = createChanAnn(0, 0, 0);
        sut.enqueue(msg1);

        // manually trigger the timer for testing purposes
        (sut as any)._onTimer();

        // reset the stub
        peer1.sendMessage.reset();

        // add a new peer
        const peer2 = createFakePeer();
        sut.addPeer(peer2);

        // push new message
        const msg2 = createChanAnn(1, 0, 0);
        sut.enqueue(msg2);

        // manually trigger the timer for testing purposes
        (sut as any)._onTimer();

        // peer1 should only see the 2nd message
        expect(peer1.sendMessage.called).to.be.true;
        expect(peer1.sendMessage.args[0][0]).to.equal(msg2);

        // peer2 should only see the 2nd message
        expect(peer2.sendMessage.called).to.be.true;
        expect(peer2.sendMessage.args[0][0]).to.equal(msg2);
      });

      it('prunes excess messages', () => {
        sut.enqueue(createChanAnn(0, 0, 0));
        sut.enqueue(createChanAnn(1, 0, 0));
        sut.enqueue(createChanAnn(2, 0, 0));
        sut.enqueue(createChanAnn(3, 0, 0));
        sut.enqueue(createChanAnn(4, 0, 0));

        // manually trigger the timer for testing purposes
        (sut as any)._onTimer();

        expect((sut as any)._queue.length).to.equal(3);
        expect((sut as any)._queue[0].shortChannelId.block).to.equal(2);
      });

      it('it adjusts peer positions based on index count', () => {
        // create a fake peer and add
        const peer1 = createFakePeer();
        sut.addPeer(peer1);

        // enqueue some items - peer is at index 0
        sut.enqueue(createChanAnn(0, 0, 0)); // index 0
        sut.enqueue(createChanAnn(1, 0, 0)); // index 1
        sut.enqueue(createChanAnn(2, 0, 0)); // index 2

        // manually trigger the timer for testing purposes
        (sut as any)._onTimer();

        // enqueue more items - peer is at index 3
        sut.enqueue(createChanAnn(3, 0, 0)); // index 3
        sut.enqueue(createChanAnn(4, 0, 0)); // index 4

        // manually trigger the timer for testing purposes which
        // will cause a prune of and leave 3 items in the queue
        (sut as any)._onTimer();

        expect((sut as any)._peers.get(peer1)).to.equal(3);
      });
    });
  });

  describe('add peer', () => {
    it('adds peer at current depth', () => {
      // create a fake peer and add
      const peer1 = createFakePeer();
      sut.addPeer(peer1);
      expect((sut as any)._peers.size).to.equal(1);
    });
  });

  describe('remove peer', () => {
    it('removes peer from list', () => {
      // create a fake peer and add
      const peer1 = createFakePeer();
      sut.addPeer(peer1);
      sut.removePeer(peer1);
      expect((sut as any)._peers.size).to.equal(0);
    });
  });
});
