// tslint:disable: no-unused-expression
import { ShortChannelId } from "@node-lightning/core";
import { expect } 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);
        });
    });
});
