// tslint:disable: max-classes-per-file
// tslint:disable: no-unused-expression
import { BitField } from "@node-lightning/core";
import { ILogger } from "@node-lightning/logger";
import * as noise from "@node-lightning/noise";
import { expect } from "chai";
import sinon from "sinon";
import { Duplex } from "stream";
import { IWireMessage } from "../lib";
import { InitFeatureFlags } from "../lib/flags/InitFeatureFlags";
import { Peer } from "../lib/Peer";
import { PeerState } from "../lib/PeerState";
import { PingPongState } from "../lib/PingPongState";
import { createFakeLogger } from "./_test-utils";

class FakeSocket extends Duplex {
    [x: string]: any;
    public outgoing: Buffer[] = [];

    constructor() {
        super();
        this.end = sinon.spy(this.end.bind(this));
        this.rpk = Buffer.alloc(33);
    }

    public _write(data: any, encoding: BufferEncoding, cb: (err?: Error) => void) {
        this.outgoing.push(data);
        cb();
    }

    public _read() {
        // nada
    }

    public simReceive(data: Buffer) {
        this.push(data);
    }
}

class FakeMessage implements IWireMessage {
    public type = 3;

    [x: string]: any;

    constructor(msg) {
        this.msg = msg;
    }
    public serialize() {
        return Buffer.from(this.msg);
    }
}

describe("Peer", () => {
    let chainHashes: Buffer[];
    let sut: Peer;
    let ls: Buffer;
    let rpk: Buffer;
    let logger: ILogger;
    let sandbox: sinon.SinonSandbox;
    let socket: FakeSocket;
    let localFeatures: BitField<InitFeatureFlags>;

    beforeEach(() => {
        chainHashes = [Buffer.alloc(32, 0xff)];
        localFeatures = new BitField<InitFeatureFlags>();
        localFeatures.set(InitFeatureFlags.optionDataLossProtectOptional);
        ls = Buffer.alloc(32, 0);
        rpk = Buffer.alloc(32, 1);
        logger = createFakeLogger();
        sut = new Peer(ls, localFeatures, chainHashes, logger, 1);
        socket = new FakeSocket();
        sut.pingPongState = sinon.createStubInstance(PingPongState) as any;
        sandbox = sinon.createSandbox();
    });

    afterEach(() => {
        sandbox.restore();
    });

    describe(".connect()", () => {
        beforeEach(() => {
            sandbox.stub(noise, "connect").returns(socket as any);
            sandbox.stub(sut, "_onSocketReady" as any);
            sandbox.stub(sut, "_onSocketClose" as any);
            sandbox.stub(sut, "_onSocketError" as any);
            sandbox.stub(sut, "_onSocketReadable" as any);
            sut.connect(rpk, "127.0.0.1", 9735);
        });

        it("should be initiator", () => {
            expect(sut.isInitiator).to.equal(true);
        });

        it("should bind to ready", () => {
            socket.emit("ready");
            expect((sut as any)._onSocketReady.called).to.be.true;
        });

        it("should bind close", () => {
            socket.emit("close");
            expect((sut as any)._onSocketClose.called).to.be.true;
        });

        it("should bind error", () => {
            socket.emit("error");
            expect((sut as any)._onSocketError.called).to.be.true;
        });

        it("should bind readable", () => {
            socket.emit("readable");
            expect((sut as any)._onSocketReadable.called).to.be.true;
        });
    });

    describe(".attach()", () => {
        beforeEach(() => {
            sandbox.stub(sut, "_onSocketReady" as any);
            sandbox.stub(sut, "_onSocketClose" as any);
            sandbox.stub(sut, "_onSocketError" as any);
            sandbox.stub(sut, "_onSocketReadable" as any);
            sut.attach(socket as any);
        });

        it("should not be initiator", () => {
            expect(sut.isInitiator).to.equal(false);
        });

        it("should bind to ready", () => {
            socket.emit("ready");
            expect((sut as any)._onSocketReady.called).to.be.true;
        });

        it("should bind close", () => {
            socket.emit("close");
            expect((sut as any)._onSocketClose.called).to.be.true;
        });

        it("should bind error", () => {
            socket.emit("error");
            expect((sut as any)._onSocketError.called).to.be.true;
        });

        it("should bind readable", () => {
            socket.emit("readable");
            expect((sut as any)._onSocketReadable.called).to.be.true;
        });
    });

    describe("when connected", () => {
        beforeEach(() => {
            sut.attach(socket as any);
        });

        describe(".sendMessage()", () => {
            it("should throw when not ready", () => {
                expect(() => sut.sendMessage(new FakeMessage("hello") as any)).to.throw();
            });

            it("should send the serialized message", () => {
                const input = new FakeMessage("test");
                sut.state = Peer.states.Ready;
                sut.sendMessage(input);
                expect((socket as any).outgoing[0]).to.deep.equal(Buffer.from("test"));
            });

            it("should emit a sending message", done => {
                const input = new FakeMessage("test");
                sut.state = Peer.states.Ready;
                sut.on("sending", () => done());
                sut.sendMessage(input);
            });
        });

        describe(".disconnect()", () => {
            it("should stop the socket", () => {
                sut.disconnect();
                expect((sut.socket as any).end.called).to.be.true;
            });

            it("should change the peer state to disconnecting", () => {
                sut.disconnect();
                expect(sut.state).to.equal(PeerState.Disconnecting);
            });
        });

        describe(".reconnect()", () => {
            it("should stop the socket", () => {
                sut.reconnect();
                expect((sut.socket as any).end.called).to.be.true;
            });

            it("should retain the peer state", () => {
                const beforeState = sut.state;
                sut.reconnect();
                expect(sut.state).to.equal(beforeState);
            });
        });

        describe("._onSocketReady()", () => {
            it("should transition state to awaiting_peer_init", () => {
                (sut as any)._onSocketReady();
                expect(sut.state).to.equal(Peer.states.AwaitingPeerInit);
            });

            it("should send the init message to the peer", () => {
                (sut as any)._onSocketReady();
                expect((socket as any).outgoing[0]).to.deep.equal(
                    Buffer.from(
                        "001000000001020120ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
                        "hex",
                    ),
                );
            });
        });

        describe("_onSocketClose", () => {
            it("should stop the ping pong state", () => {
                sut.state = PeerState.Disconnecting;
                (sut as any)._onSocketClose();
                expect((sut as any).pingPongState.onDisconnecting.called).to.be.true;
            });

            describe("when disconnecting", () => {
                it("should emit the close event", done => {
                    sut.state = PeerState.Disconnecting;
                    sut.on("close", () => done());
                    (sut as any)._onSocketClose();
                });
            });

            describe("when initiator", () => {
                it("should trigger reconnect", done => {
                    sut.state = PeerState.Ready;
                    sut.reconnectTimeoutMs = 0;
                    sut.connect = sandbox.stub(sut, "connect");
                    sut.isInitiator = true;
                    (sut as any)._onSocketClose();
                    setTimeout(() => {
                        expect((sut.connect as any).called).to.be.true;
                        done();
                    }, 50);
                });
            });

            describe("when not initiator", () => {
                it("should not trigger reconnect", done => {
                    sut.state = PeerState.Ready;
                    sut.reconnectTimeoutMs = 0;
                    sut.connect = sandbox.stub(sut, "connect");
                    sut.isInitiator = false;
                    (sut as any)._onSocketClose();
                    setTimeout(() => {
                        expect((sut.connect as any).called).to.be.false;
                        done();
                    }, 50);
                });
            });
        });

        describe("_onSocketError", () => {
            it("should emit error event", done => {
                sut.on("error", () => done());
                (sut as any)._onSocketError();
            });
        });

        describe("_sendInitMessage", () => {
            it("should send the initialization message", () => {
                (sut as any)._sendInitMessage();
                expect((socket as any).outgoing[0]).to.deep.equal(
                    Buffer.from(
                        "001000000001020120ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
                        "hex",
                    ),
                );
            });
        });

        describe("_processPeerInitMessage", () => {
            let input;

            beforeEach(() => {
                input = Buffer.from("001000000000", "hex");
            });

            it("it should fail if not init message", () => {
                input = Buffer.from("001100000000", "hex");
                expect(() => (sut as any)._processPeerInitMessage(input)).to.throw();
            });

            it("should start ping state", () => {
                (sut as any)._processPeerInitMessage(input);
                expect((sut as any).pingPongState.start.called).to.be.true;
            });

            it("should change the state to ready", () => {
                (sut as any)._processPeerInitMessage(input);
                expect(sut.state).to.equal(Peer.states.Ready);
            });

            it("should capture the init features", () => {
                input = Buffer.from("00100000000109", "hex");
                (sut as any)._processPeerInitMessage(input);
                expect(sut.remoteFeatures).to.be.instanceof(BitField);
                expect(sut.remoteFeatures.isSet(InitFeatureFlags.optionDataLossProtectRequired));
                expect(sut.remoteFeatures.isSet(InitFeatureFlags.initialRoutingSyncOptional));
            });

            it("should emit ready", done => {
                sut.on("ready", () => done());
                (sut as any)._processPeerInitMessage(input);
            });

            it("should be ok with no remote chainhash", () => {
                input = Buffer.from("00100000000109", "hex");
                (sut as any)._processPeerInitMessage(input);
            });

            it("should be ok with single chain_hash", () => {
                input = Buffer.from(
                    "00100000000109" +
                        "0120" +
                        "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
                    "hex",
                );
                (sut as any)._processPeerInitMessage(input);
                expect(sut.remoteChains.length).to.equal(1);
                expect(sut.remoteChains[0].toString("hex")).to.equal("ff".repeat(32));
            });

            it("should be ok with multiple chain_hashes", () => {
                input = Buffer.from(
                    "00100000000109" +
                        "0140" +
                        "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" +
                        "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
                    "hex",
                );
                (sut as any)._processPeerInitMessage(input);
                expect(sut.remoteChains.length).to.equal(2);
                expect(sut.remoteChains[0].toString("hex")).to.equal("ff".repeat(32));
                expect(sut.remoteChains[1].toString("hex")).to.equal("ee".repeat(32));
            });

            it("should disconnect with no chainhash match", () => {
                input = Buffer.from(
                    "00100000000109" +
                        "0120" +
                        "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
                    "hex",
                );
                (sut as any)._processPeerInitMessage(input);
                expect(sut.state).to.equal(PeerState.Disconnecting);
            });

            it("should stay connected when local odd no remote feature", () => {
                // enable compulsory (bit 0)
                (sut as any).localFeatures = new BitField<InitFeatureFlags>();
                sut.localFeatures.set(InitFeatureFlags.optionDataLossProtectOptional);

                // remote optional (bit 1)
                input = Buffer.from("00100000000100", "hex");
                (sut as any)._processPeerInitMessage(input);
                expect(sut.state).to.equal(PeerState.Ready);
            });

            it("should stay connected when local even/remote even feature", () => {
                // enable compulsory (bit 0)
                sut.localFeatures.set(InitFeatureFlags.optionDataLossProtectRequired);

                // remote optional (bit 1)
                input = Buffer.from("00100000000102", "hex");
                (sut as any)._processPeerInitMessage(input);
                expect(sut.state).to.equal(PeerState.Ready);
            });

            it("should stay connected with local even/remote even feature", () => {
                // enable compulsory (bit 0)
                sut.localFeatures.set(InitFeatureFlags.optionDataLossProtectRequired);

                // remote optional (bit 1)
                input = Buffer.from("00100000000101", "hex");
                (sut as any)._processPeerInitMessage(input);
                expect(sut.state).to.equal(PeerState.Ready);
            });

            it("should disconnect with local even missing remote feature", () => {
                // enable compulsory (bit 0)
                sut.localFeatures.set(InitFeatureFlags.optionDataLossProtectRequired);

                // remote optional (bit 1)
                input = Buffer.from("00100000000100", "hex");
                (sut as any)._processPeerInitMessage(input);
                expect(sut.state).to.equal(PeerState.Disconnecting);
            });
        });

        describe("_processMessage", () => {
            let input;
            let msg;

            beforeEach(() => {
                input = Buffer.from("001000000000", "hex");
            });

            describe("when valid message", () => {
                it("should log with ping service", () => {
                    msg = (sut as any)._processMessage(input);
                    expect((sut as any).pingPongState.onMessage.called).to.be.true;
                });

                it("should emit the message", () => {
                    expect(msg.type).to.equal(16);
                });
            });
        });

        describe("_onSocketReadable", () => {
            it("should read peer init message when awaiting_peer_init state", done => {
                sut.state = Peer.states.AwaitingPeerInit;
                sut.on("ready", () => done());
                socket.simReceive(Buffer.from("001000000000", "hex"));
            });

            it("should process message when in ready state", done => {
                sut.state = Peer.states.Ready;
                sut.on("readable", () => {
                    done();
                });
                socket.simReceive(Buffer.from("001000000000", "hex"));
            });

            describe("on error", () => {
                it("should close the socket", done => {
                    sut.state = Peer.states.Ready;
                    sut.on("error", () => {
                        expect((socket as any).end.called).to.be.true;
                        done();
                    });
                    socket.simReceive(Buffer.from("0010", "hex"));
                });
                it("should emit an error event", done => {
                    sut.state = Peer.states.Ready;
                    sut.on("error", () => done());
                    socket.simReceive(Buffer.from("0010", "hex"));
                });
            });
        });
    });
});
