/**
 * Copyright JS Foundation and other contributors, http://js.foundation
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 **/

var should = require("should");
var batchNode = require("nr-test-utils").require("@node-red/nodes/core/sequence/19-batch.js");
var helper = require("node-red-node-test-helper");
var RED = require("nr-test-utils").require("node-red/lib/red.js");

describe('BATCH node', function() {
    this.timeout(8000);

    before(function(done) {
        helper.startServer(done);
    });

    after(function(done) {
        helper.stopServer(done);
    });

    afterEach(function() {
        helper.unload();
        RED.settings.nodeMessageBufferMaxLength = 0;
    });

    it('should be loaded with defaults', function(done) {
        var flow = [{id:"n1", type:"batch", name: "BatchNode", wires:[["n2"]]},
                    {id:"n2", type:"helper"}];
        helper.load(batchNode, flow, function() {
            var n1 = helper.getNode("n1");
            n1.should.have.property('name', 'BatchNode');
            done();
        });
    });

    function check_parts(msg, id, idx, count) {
        msg.should.have.property("parts");
        var parts = msg.parts;
        parts.should.have.property("id", id);
        parts.should.have.property("index", idx);
        parts.should.have.property("count", count);
    }

    function check_data(n1, n2, results, done) {
        var id = undefined;
        var ix0 = 0; // seq no
        var ix1 = 0; // loc. in seq
        var seq = undefined;
        var msgs = [];
        n2.on("input", function(msg) {
            try {
                for (var i = 0; i < msgs.length; i++) {
                    msg.should.not.equal(msgs[i]);
                }
                msgs.push(msg);
                if (seq === undefined) {
                    seq = results[ix0];
                }
                var val = seq[ix1];
                msg.should.have.property("payload", val);
                if (id === undefined) {
                    id = msg.parts.id;
                }
                check_parts(msg, id, ix1, seq.length);
                ix1++;
                if (ix1 === seq.length) {
                    ix0++;
                    ix1 = 0;
                    seq = undefined;
                    id = undefined;
                    if (ix0 === results.length) {
                        done();
                    }
                }
            }
            catch (e) {
                done(e);
            }
        });
    }

    function check_count(flow, results, done) {
        try {
            helper.load(batchNode, flow, function() {
                var n1 = helper.getNode("n1");
                var n2 = helper.getNode("n2");
                check_data(n1, n2, results, done);
                for(var i = 0; i < 6; i++) {
                    n1.receive({payload: i, parts: { count:6, index:i }});
                }
            });
        }
        catch (e) {
            done(e);
        }
    }

    function delayed_send(receiver, index, count, delay, done) {
        if (index < count) {
            setTimeout(function() {
                receiver.receive({payload: index});
                delayed_send(receiver, index+1, count, delay, done);
            }, delay);
        }
        else if(index === count) {
            if (done) {
                done();
            }
        }
    }

    function check_interval(flow, results, delay, done) {
        helper.load(batchNode, flow, function() {
            var n1 = helper.getNode("n1");
            var n2 = helper.getNode("n2");
            check_data(n1, n2, results, done);
            delayed_send(n1, 0, 4, delay);
        });
    }

    function check_concat(flow, results, inputs, done) {
        try {
            helper.load(batchNode, flow, function() {
                var n1 = helper.getNode("n1");
                var n2 = helper.getNode("n2");
                check_data(n1, n2, results, done);
                for(var data of inputs) {
                    var msg = {
                        topic: data[0],
                        payload: data[1],
                        parts: {
                            id: data[0],
                            index: data[2],
                            count: data[3]
                        }
                    };
                    n1.receive(msg);
                }
            });
        }
        catch (e) {
            done(e);
        }
    }

    describe('mode: count', function() {

        it('should create seq. with count', function(done) {
            var flow = [{id:"n1", type:"batch", name: "BatchNode", mode: "count", count: 2, overlap: 0, interval: 10, allowEmptySequence: false, topics: [], wires:[["n2"]]},
                        {id:"n2", type:"helper"}];
            var results = [
                [0, 1],
                [2, 3],
                [4, 5]
            ];
            check_count(flow, results, done);
        });

        it('should create seq. with count (more sent than count)', function(done) {
            var flow = [{id:"n1", type:"batch", name: "BatchNode", mode: "count", count: 4, overlap: 0, interval: 10, allowEmptySequence: false, topics: [], wires:[["n2"]]},
                        {id:"n2", type:"helper"}];
            var results = [
                [0, 1, 2, 3]
            ];
            check_count(flow, results, done);
        });

        it('should create seq. with count and terminate early if parts honoured', function(done) {
            var flow = [{id:"n1", type:"batch", name: "BatchNode", mode: "count", count: 4, overlap: 0, interval: 10, allowEmptySequence:false, honourParts:true, topics: [], wires:[["n2"]]},
                        {id:"n2", type:"helper"}];
            var results = [
                [0, 1, 2, 3],
                [4, 5]
            ];
            check_count(flow, results, done);
        });

        it('should create seq. with count and overlap', function(done) {
            var flow = [{id:"n1", type:"batch", name: "BatchNode", mode: "count", count: 3, overlap: 2, interval: 10, allowEmptySequence: false, topics: [], wires:[["n2"]]},
                        {id:"n2", type:"helper"}];
            var results = [
                [0, 1, 2],
                [1, 2, 3],
                [2, 3, 4],
                [3, 4, 5]
            ];
            check_count(flow, results, done);
        });

        it('should handle too many pending messages', function(done) {
            var flow = [{id:"n1", type:"batch", name: "BatchNode", mode: "count", count: 5, overlap: 0, interval: 10, allowEmptySequence: false, topics: [], wires:[["n2"]]},
                        {id:"n2", type:"helper"}];
            helper.load(batchNode, flow, function() {
                var n1 = helper.getNode("n1");
                var n2 = helper.getNode("n2");
                RED.settings.nodeMessageBufferMaxLength = 2;
                setTimeout(function() {
                    var logEvents = helper.log().args.filter(function (evt) {
                        return evt[0].type == "batch";
                    });
                    var evt = logEvents[0][0];
                    evt.should.have.property('id', "n1");
                    evt.should.have.property('type', "batch");
                    evt.should.have.property('msg', "batch.too-many");
                    done();
                }, 150);
                for(var i = 0; i < 3; i++) {
                    n1.receive({payload: i});
                }
            });
        });

        it('should handle reset', function(done) {
            var flow = [{id:"n1", type:"batch", name: "BatchNode", mode: "count", count: 2, overlap: 0, interval: 0, allowEmptySequence: false, topics: [], wires:[["n2"]]},
                        {id:"n2", type:"helper"}];
            helper.load(batchNode, flow, function() {
                var n1 = helper.getNode("n1");
                var n2 = helper.getNode("n2");
                var results = [
                    [0, 1],
                    [4, 5]
                ];
                check_data(n1, n2, results, done);
                n1.receive({payload:0});
                n1.receive({payload:1});
                n1.receive({payload:2});
                n1.receive({payload:3, reset: true});
                n1.receive({payload:4});
                n1.receive({payload:5});
            });
        });
    });

    describe('mode: interval', function() {
        it('should create seq. with interval', function(done) {
            var flow = [{id:"n1", type:"batch", name: "BatchNode", mode: "interval", count: 0, overlap: 0, interval: 1, allowEmptySequence: false, topics: [], wires:[["n2"]]},
                        {id:"n2", type:"helper"}];
            var results = [
                [0, 1],
                [2, 3]
            ];
            check_interval(flow, results, 450, done);
        });

        it('should create seq. with interval (in float)', function(done) {
            var flow = [{id:"n1", type:"batch", name: "BatchNode", mode: "interval", count: 0, overlap: 0, interval: 0.5, allowEmptySequence: false, topics: [], wires:[["n2"]]},
                        {id:"n2", type:"helper"}];
            var results = [
                [0, 1],
                [2, 3]
            ];
            check_interval(flow, results, 225, done);
        });

        it('should create seq. with interval & not send empty seq', function(done) {
            var flow = [{id:"n1", type:"batch", name: "BatchNode", mode: "interval", count: 0, overlap: 0, interval: 1, allowEmptySequence: false, topics: [], wires:[["n2"]]},
                        {id:"n2", type:"helper"}];
            var results = [
                // 1300, 2600, 3900, 5200,
                [0], [1], [2], [3]
            ];
            check_interval(flow, results, 1300, done);
        });

        it('should create seq. with interval & send empty seq', function(done) {
            var flow = [{id:"n1", type:"batch", name: "BatchNode", mode: "interval", count: 0, overlap: 0, interval: 1, allowEmptySequence: true, topics: [], wires:[["n2"]]},
                        {id:"n2", type:"helper"}];
            var results = [
                // 1300, 2600, 3900, 5200,
                [null], [0], [1], [2], [null], [3]
            ];
            check_interval(flow, results, 1300, done);
        });

        it('should handle too many pending messages', function(done) {
            var flow = [{id:"n1", type:"batch", name: "BatchNode", mode: "interval", count: 0, overlap: 0, interval: 1, allowEmptySequence: false, topics: [], wires:[["n2"]]},
                        {id:"n2", type:"helper"}];
            helper.load(batchNode, flow, function() {
                var n1 = helper.getNode("n1");
                var n2 = helper.getNode("n2");
                RED.settings.nodeMessageBufferMaxLength = 2;
                setTimeout(function() {
                    var logEvents = helper.log().args.filter(function (evt) {
                        return evt[0].type == "batch";
                    });
                    var evt = logEvents[0][0];
                    evt.should.have.property('id', "n1");
                    evt.should.have.property('type', "batch");
                    evt.should.have.property('msg', "batch.too-many");
                    done();
                }, 150);
                for(var i = 0; i < 3; i++) {
                    n1.receive({payload: i});
                }
            });
        });

        it('should handle reset', function(done) {
            var flow = [{id:"n1", type:"batch", name: "BatchNode", mode: "interval", count: 0, overlap: 0, interval: 1, allowEmptySequence: false, topics: [], wires:[["n2"]]},
                        {id:"n2", type:"helper"}];
            helper.load(batchNode, flow, function() {
                var n1 = helper.getNode("n1");
                var n2 = helper.getNode("n2");
                var results = [
                    [0, 1],
                    [4, 5]
                ];
                check_data(n1, n2, results, done);
                delayed_send(n1, 0, 3, 400, function () {
                    setTimeout(function () {
                        n1.receive({payload: "3", reset: true});
                        delayed_send(n1, 4, 7, 400);
                    }, 10);
                });
            });
        });

    });

    describe('mode: concat', function() {
        it('should concat two seq. (series)', function(done) {
            var flow = [{id:"n1", type:"batch", name: "BatchNode", mode: "concat", count: 0, overlap: 0, interval: 1, allowEmptySequence: false, topics: [{topic: "TA"}, {topic: "TB"}], wires:[["n2"]]},
                        {id:"n2", type:"helper"}];
            var results = [
                [2, 3, 0, 1]
            ];
            var inputs = [
                ["TB", 0, 0, 2],
                ["TB", 1, 1, 2],
                ["TA", 2, 0, 2],
                ["TA", 3, 1, 2]
            ];
            check_concat(flow, results, inputs, done);
        });

        it('should concat two seq. (mixed)', function(done) {
            var flow = [{id:"n1", type:"batch", name: "BatchNode", mode: "concat", count: 0, overlap: 0, interval: 1, allowEmptySequence: false, topics: [{topic: "TA"}, {topic: "TB"}], wires:[["n2"]]},
                        {id:"n2", type:"helper"}];
            var results = [
                [2, 3, 0, 1]
            ];
            var inputs = [
                ["TA", 2, 0, 2],
                ["TB", 0, 0, 2],
                ["TA", 3, 1, 2],
                ["TB", 1, 1, 2]
            ];
            check_concat(flow, results, inputs, done);
        });

        it('should concat three seq.', function(done) {
            var flow = [{id:"n1", type:"batch", name: "BatchNode", mode: "concat", count: 0, overlap: 0, interval: 1, allowEmptySequence: false, topics: [{topic: "TA"}, {topic: "TB"}, {topic: "TC"}], wires:[["n2"]]},
                        {id:"n2", type:"helper"}];
            var results = [
                [2, 3, 0, 1, 4]
            ];
            var inputs = [
                ["TC", 4, 0, 1],
                ["TB", 0, 0, 2],
                ["TB", 1, 1, 2],
                ["TA", 2, 0, 2],
                ["TA", 3, 1, 2]
            ];
            check_concat(flow, results, inputs, done);
        });

        it('should concat same seq.', function(done) {
            var flow = [{id:"n1", type:"batch", name: "BatchNode", mode: "concat", count: 0, overlap: 0, interval: 1, allowEmptySequence: false, topics: [{topic: "TA"}, {topic: "TA"}], wires:[["n2"]]},
                        {id:"n2", type:"helper"}];
            var results = [
                [9, 8, 9, 8]
            ];
            var inputs = [
                ["TA", 9, 0, 2],
                ["TA", 8, 1, 2]
            ];
            check_concat(flow, results, inputs, done);
        });

        it('should handle too many pending messages', function(done) {
            var flow = [{id:"n1", type:"batch", name: "BatchNode", mode: "concat", count: 0, overlap: 0, interval: 1, allowEmptySequence: false, topics: [{topic: "TA"}, {topic: "TB"}], wires:[["n2"]]},
                        {id:"n2", type:"helper"}];
            helper.load(batchNode, flow, function() {
                var n1 = helper.getNode("n1");
                var n2 = helper.getNode("n2");
                RED.settings.nodeMessageBufferMaxLength = 2;
                setTimeout(function() {
                    var logEvents = helper.log().args.filter(function (evt) {
                        return evt[0].type == "batch";
                    });
                    var evt = logEvents[0][0];
                    evt.should.have.property('id', "n1");
                    evt.should.have.property('type', "batch");
                    evt.should.have.property('msg', "batch.too-many");
                    done();
                }, 150);
                var C = 3;
                for(var i = 0; i < C; i++) {
                    var parts_a = {index:i, count:C, id:"A"};
                    var parts_b = {index:i, count:C, id:"B"};
                    n1.receive({payload: i, topic: "TA", parts:parts_a});
                    n1.receive({payload: i, topic: "TB", parts:parts_b});
                }
            });
        });

        it('should handle reset', function(done) {
            var flow = [{id:"n1", type:"batch", name: "BatchNode", mode: "concat", count: 0, overlap: 0, interval: 1, allowEmptySequence: false, topics: [{topic: "TA"}, {topic: "TB"}], wires:[["n2"]]},
                        {id:"n2", type:"helper"}];
            try {
                helper.load(batchNode, flow, function() {
                    var n1 = helper.getNode("n1");
                    var n2 = helper.getNode("n2");
                    var results = [
                        [2, 3, 0, 1]
                    ];
                    check_data(n1, n2, results, done);
                    var inputs0 = [
                        ["TB", 0, 0, 2],
                        ["TA", 1, 0, 2],
                    ];
                    for(var data of inputs0) {
                        var msg = {
                            topic: data[0],
                            payload: data[1],
                            parts: {
                                id: data[0],
                                index: data[2],
                                count: data[3]
                            }
                        };
                        n1.receive(msg);
                    }
                    n1.receive({payload: undefined, reset: true});
                    var inputs1 = [
                        ["TB", 0, 0, 2],
                        ["TB", 1, 1, 2],
                        ["TA", 2, 0, 2],
                        ["TA", 3, 1, 2]
                    ];
                    for(var data of inputs1) {
                        var msg = {
                            topic: data[0],
                            payload: data[1],
                            parts: {
                                id: data[0],
                                index: data[2],
                                count: data[3]
                            }
                        };
                        n1.receive(msg);
                    }
                });
            }
            catch (e) {
                done(e);
            }
        });
    });

    describe('messaging API', function() {
        function mapiDoneTestHelper(done, mode, count, overlap, interval, allowEmptySequence, msgAndTimings) {
            const completeNode = require("nr-test-utils").require("@node-red/nodes/core/common/24-complete.js");
            const catchNode = require("nr-test-utils").require("@node-red/nodes/core/common/25-catch.js");
            const flow = [{id:"batchNode1", type:"batch", name: "BatchNode", mode, count, overlap, interval,
                           allowEmptySequence, topics: [{topic: "TA"}], wires:[[]]},
                          {id:"completeNode1",type:"complete",scope: ["batchNode1"],uncaught:false,wires:[["helperNode1"]]},
                          {id:"catchNode1", type:"catch",scope: ["batchNode1"],uncaught:false,wires:[["helperNode1"]]},
                          {id:"helperNode1",type:"helper", wires:[[]]}];
            const numMsgs = msgAndTimings.length;
            helper.load([batchNode, completeNode, catchNode], flow, function () {
                const batchNode1 = helper.getNode("batchNode1");
                const helperNode1 = helper.getNode("helperNode1");
                RED.settings.nodeMessageBufferMaxLength = 2;
                const t = Date.now();
                let c = 0;
                helperNode1.on("input", function (msg) {
                    msg.should.have.a.property('payload');
                    (Date.now() - t).should.be.approximately(msgAndTimings[msg.payload].avr, msgAndTimings[msg.payload].var);
                    c += 1;
                    if ( c === numMsgs) {
                        done();
                    }
                });
                for (let i = 0; i < numMsgs; i++) {
                    setTimeout( function() { batchNode1.receive(msgAndTimings[i].msg); }, msgAndTimings[i].delay);
                }
            });
        }

        it('should call done() when message is sent (mode: count)', function(done) {
            mapiDoneTestHelper(done, "count", 2, 0, 2, false, [
                { msg: {payload: 0}, delay: 0, avr: 0, var: 100},
                { msg: {payload: 1}, delay: 0, avr: 0, var: 100}
            ]);
        });
        it('should call done() when reset (mode: count)', function(done) {
            mapiDoneTestHelper(done, "count", 2, 0, 2, false, [
                { msg: {payload: 0}, delay: 0, avr: 200, var: 100},
                { msg: {payload: 1, reset:true}, delay: 200, avr: 200, var: 100}
            ]);
        });
        it('should call done() regardless of buffer overflow (mode: count)', function(done) {
            mapiDoneTestHelper(done, "count", 10, 0, 2, false, [
                { msg: {payload: 0}, delay: 0, avr: 500, var: 100},
                { msg: {payload: 1}, delay: 100, avr: 500, var: 100},
                { msg: {payload: 2}, delay: 500, avr: 500, var: 100}
            ]);
        });
        it('should call done() when message is sent (mode: interval)', function(done) {
            mapiDoneTestHelper(done, "interval", 2, 0, 2, false, [
                { msg: {payload: 0}, delay: 0, avr: 2000, var: 100},
                { msg: {payload: 1}, delay: 500, avr: 2000, var: 100}
            ]);
        });
        it('should call done() when reset (mode: interval)', function(done) {
            mapiDoneTestHelper(done, "interval", 2, 0, 2, false, [
                { msg: {payload: 0}, delay: 0, avr: 200, var: 100},
                { msg: {payload: 1, reset:true}, delay: 200, avr: 200, var: 100}
            ]);
        });
        it('should call done() regardless of buffer overflow (mode: interval)', function(done) {
            mapiDoneTestHelper(done, "interval", 2, 0, 2, false, [
                { msg: {payload: 0}, delay: 0, avr: 500, var: 100},
                { msg: {payload: 1}, delay: 100, avr: 500, var: 100},
                { msg: {payload: 2}, delay: 500, avr: 500, var: 100}
            ]);
        });
        it('should call done() when message is sent (mode: concat)', function(done) {
            mapiDoneTestHelper(done, "concat", 2, 0, 2, false, [
                { msg: {topic:"TA", payload: 0, parts: {id: "TA", index: 0, count: 2}}, delay: 0, avr: 1000, var: 100},
                { msg: {topic:"TA", payload: 1, parts: {id: "TA", index: 1, count: 2}}, delay: 1000, avr: 1000, var: 100},
            ]);
        });
        it('should call done() when reset (mode: concat)', function(done) {
            mapiDoneTestHelper(done, "concat", 2, 0, 2, false, [
                { msg: {topic:"TA", payload: 0, parts: {id: "TA", index: 0, count: 2}}, delay: 0, avr: 1000, var: 100},
                { msg: {payload: 1, reset:true}, delay: 1000, avr: 1000, var: 100},
            ]);
        });
        it('should call done() regardless of buffer overflow (mode: concat)', function(done) {
            mapiDoneTestHelper(done, "concat", 2, 0, 2, false, [
                { msg: {topic:"TA", payload: 0, parts: {id: "TA", index: 0, count: 3}}, delay: 0, avr: 1000, var: 100},
                { msg: {topic:"TA", payload: 0, parts: {id: "TA", index: 1, count: 3}}, delay: 500, avr: 1000, var: 100},
                { msg: {topic:"TA", payload: 0, parts: {id: "TA", index: 2, count: 3}}, delay: 1000, avr: 1000, var: 100}
            ]);
        });
    });
});
