/**
 * 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 sinon = require("sinon");
var helper = require("node-red-node-test-helper");
var triggerNode = require("nr-test-utils").require("@node-red/nodes/core/function/89-trigger.js");
var Context = require("nr-test-utils").require("@node-red/runtime/lib/nodes/context");
var RED = require("nr-test-utils").require("node-red/lib/red");

describe('trigger node', function() {

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

    function initContext(done) {
        Context.init({
            contextStorage: {
                memory0: {
                    module: "memory"
                },
                memory1: {
                    module: "memory"
                },
                memory2: {
                    module: "memory"
                }
            }
        });
        Context.load().then(function () {
            done();
        });
    }

    afterEach(function(done) {
        helper.unload().then(function () {
            return Context.clean({allNodes: {}});
        }).then(function () {
            return Context.close();
        }).then(function () {
            helper.stopServer(done);
        });
    });

    it("should be loaded with correct defaults", function(done) {
        var flow = [{"id":"n1", "type":"trigger", "name":"triggerNode", "wires":[[]]}];
        helper.load(triggerNode, flow, function() {
            var n1 = helper.getNode("n1");
            n1.should.have.property('name', 'triggerNode');
            n1.should.have.property('op1', '1');
            n1.should.have.property('op2', '0');
            n1.should.have.property('op1type', 'str');
            n1.should.have.property('op2type', 'str');
            n1.should.have.property('extend', "false");
            n1.should.have.property('units', 'ms');
            n1.should.have.property('duration', 250);
            done();
        });
    });

    it("should be able to set delay in seconds", function(done) {
        var flow = [{"id":"n1", "type":"trigger", "name":"triggerNode", units:"s", duration:"1", "wires":[[]]}];
        helper.load(triggerNode, flow, function() {
            var n1 = helper.getNode("n1");
            n1.should.have.property('duration', 1000);
            done();
        });
    });

    it("should be able to set delay in minutes", function(done) {
        var flow = [{"id":"n1", "type":"trigger", "name":"triggerNode", units:"min", duration:"1", "wires":[[]]}];
        helper.load(triggerNode, flow, function() {
            var n1 = helper.getNode("n1");
            n1.should.have.property('duration', 60000);
            done();
        });
    });

    it("should be able to set delay in hours", function(done) {
        var flow = [{"id":"n1", "type":"trigger", "name":"triggerNode", units:"hr", duration:"1", "wires":[[]]}];
        helper.load(triggerNode, flow, function() {
            var n1 = helper.getNode("n1");
            n1.should.have.property('duration', 3600000);
            done();
        });
    });

    function basicTest(type, val, rval) {
        it('should output 1st value when triggered ('+type+')', function(done) {
            var flow = [{"id":"n1", "type":"trigger", "name":"triggerNode", op1:val, op1type:type, op2:"", op2type:"null", duration:"20", wires:[["n2"]] },
                {id:"n2", type:"helper"} ];
            process.env[val] = rval;
            helper.load(triggerNode, flow, function() {
                var n1 = helper.getNode("n1");
                var n2 = helper.getNode("n2");
                n2.on("input", function(msg) {
                    try {
                        if (rval) {
                            msg.should.have.property("payload");
                            if (type == "date" && val == "1") {
                                should.deepEqual(Math.round(msg.payload/1000000), Math.round(Date.now()/1000000));
                            }
                            else if (type == "date" && val == "iso") {
                                should.deepEqual(msg.payload.substr(0,11), rval.substr(0,11));
                            }
                            else {
                                should.deepEqual(msg.payload, rval);
                            }
                        }
                        else {
                            msg.should.have.property("payload", val);
                        }
                        delete process.env[val];
                        done();
                    }
                    catch(err) { done(err); }
                });
                n1.emit("input", {payload:null});
            });
        });

        it('should output 2st value when triggered ('+type+')', function(done) {
            if (type == "date" && val == "1") { val = "0"; }
            var flow = [{"id":"n1", "type":"trigger", "name":"triggerNode", op1:"foo", op1type:"str", op2:val, op2type:type, duration:"20", wires:[["n2"]] },
                {id:"n2", type:"helper"} ];
            process.env[val] = rval;
            helper.load(triggerNode, flow, function() {
                var n1 = helper.getNode("n1");
                var n2 = helper.getNode("n2");
                var c = 0;
                n2.on("input", function(msg) {
                    try {
                        if (c === 0) {
                            msg.should.have.property("payload", "foo");
                            c++;
                        }
                        else {
                            if (rval) {
                                msg.should.have.property("payload");
                                if (type == "date" && val == "0") {
                                    ;(Math.round(msg.payload/1000000)).should.be.approximately(parseInt(Date.now()/1000000), 1);
                                }
                                else if (type == "date" && val == "iso") {
                                    should.deepEqual(msg.payload.substr(0,11), rval.substr(0,11));
                                }
                                else {
                                    should.deepEqual(msg.payload, rval);
                                }
                            }
                            else {
                                msg.should.have.property("payload", val);
                            }
                            delete process.env[val];
                            done();
                        }
                    }
                    catch(err) { done(err); }
                });
                n1.emit("input", {payload:null});
            });
        });
    }

    basicTest("num", 10);
    basicTest("str", "10");
    basicTest("bool", true);
    var val_json = '{ "x":"vx", "y":"vy", "z":"vz" }';
    basicTest("json", val_json, JSON.parse(val_json));
    var val_buf = "[1,2,3,4,5]";
    basicTest("bin", val_buf, Buffer.from(JSON.parse(val_buf)));
    basicTest("env", "NR-TEST", "env-val");
    basicTest("date", "1", Date.now());
    basicTest("date", "iso", (new Date()).toISOString());
    // basicTest("date", "object", Date.now());

    it('should output 1 then 0 when triggered (default)', function(done) {
        var flow = [{"id":"n1", "type":"trigger", "name":"triggerNode", duration:"20", wires:[["n2"]] },
            {id:"n2", type:"helper"} ];
        helper.load(triggerNode, flow, function() {
            var n1 = helper.getNode("n1");
            var n2 = helper.getNode("n2");
            var c = 0;
            n2.on("input", function(msg) {
                try {
                    if (c === 0) {
                        msg.should.have.a.property("payload", '1');
                        c+=1;
                    }
                    else {
                        msg.should.have.a.property("payload", '0');
                        done();
                    }
                }
                catch(err) { done(err); }
            });
            n1.emit("input", {payload:null});
        });
    });

    it('should ignore any other inputs while triggered if extend is false', function(done) {
        var flow = [{"id":"n1", "type":"trigger", "name":"triggerNode", duration:"50",wires:[["n2"]] },
            {id:"n2", type:"helper"} ];
        helper.load(triggerNode, flow, function() {
            var n1 = helper.getNode("n1");
            var n2 = helper.getNode("n2");
            var c = 0;
            var errored = false;
            n2.on("input", function(msg) {
                try {
                    if (c === 0) {
                        msg.should.have.a.property("payload", '1');
                    }
                    else {
                        msg.should.have.a.property("payload", '0');
                    }
                    c+=1;
                }catch(err) {
                    errored = true;
                    done(err);
                }
            });
            setTimeout( function() {
                if (!errored) {
                    try {
                        c.should.equal(2);
                        done();
                    } catch(err) {
                        done(err);
                    }
                }
            },100);
            n1.emit("input", {payload:null});
            setTimeout( function() {
                n1.emit("input", {payload:null});
            },10);
            setTimeout( function() {
                n1.emit("input", {payload:null});
            },30);
        });
    });

    it('should ignore msg.delay if overrideDelay not set', function(done) {
        var flow = [
            {"id":"n1", "type":"trigger", "name":"triggerNode", duration:"50",wires:[["n2"]] },
            {id:"n2", type:"helper"}
        ];
        helper.load(triggerNode, flow, function() {
            var n1 = helper.getNode("n1");
            var n2 = helper.getNode("n2");
            var c = 0;
            var firstTime;
            n2.on("input", function(msg) {
                if (c === 0) {
                    firstTime = Date.now();
                } else if (c === 1) {
                    try {
                        var delta = Date.now() - firstTime;
                        delta.should.be.greaterThan(30);
                        delta.should.be.lessThan(100);
                        done();
                    } catch(err) {
                        done(err);
                    }
                }
                c++;
            });
            n1.emit("input", {payload:null, delay: 300});
        });
    });

    it('should use msg.delay if overrideDelay is set', function(done) {
        var flow = [
            {"id":"n1", "type":"trigger", "name":"triggerNode", overrideDelay: true, duration:"50",wires:[["n2"]] },
            {id:"n2", type:"helper"}
        ];
        helper.load(triggerNode, flow, function() {
            var n1 = helper.getNode("n1");
            var n2 = helper.getNode("n2");
            var c = 0;
            var firstTime;
            n2.on("input", function(msg) {
                if (c === 0) {
                    firstTime = Date.now();
                } else if (c === 1) {
                    try {
                        var delta = Date.now() - firstTime;
                        delta.should.be.greaterThan(270);
                        delta.should.be.lessThan(380);
                        done();
                    } catch(err) {
                        done(err);
                    }
                }
                c++;
            });
            n1.emit("input", {payload:null, delay: 300});
        });
    });


    it('should handle true and false as strings and delay of 0', function(done) {
        var flow = [{"id":"n1", "type":"trigger", "name":"triggerNode", op1:"true",op1type:"val",op2:"false",op2type:"val",duration:"30", wires:[["n2"]] },
            {id:"n2", type:"helper"} ];
        helper.load(triggerNode, flow, function() {
            var n1 = helper.getNode("n1");
            var n2 = helper.getNode("n2");
            var c = 0;
            n2.on("input", function(msg) {
                try {
                    if (c === 0) {
                        msg.should.have.a.property("payload", true);
                        c+=1;
                    }
                    else {
                        msg.should.have.a.property("payload", false);
                        done();
                    }
                } catch(err) {
                    done(err);
                }
            });
            n1.emit("input", {payload:null});
        });
    });

    it('should handle multiple topics as one if not asked to handle', function(done) {
        var flow = [{"id":"n1", "type":"trigger", "name":"triggerNode", bytopic:"all", op1:"1", op2:"0", op1type:"num", op2type:"num", duration:"30", wires:[["n2"]] },
            {id:"n2", type:"helper"} ];
        helper.load(triggerNode, flow, function() {
            var n1 = helper.getNode("n1");
            var n2 = helper.getNode("n2");
            var c = 0;
            n2.on("input", function(msg) {
                try {
                    c += 1;
                    if (c === 1) {
                        msg.should.have.a.property("payload", 1);
                        msg.should.have.a.property("topic", "A");
                    }
                    else if (c === 2) {
                        msg.should.have.a.property("payload", 0);
                        msg.should.have.a.property("topic", "A");
                        done();
                    }
                } catch(err) {
                    done(err);
                }
            });
            n1.emit("input", {payload:1,topic:"A"});
            n1.emit("input", {payload:2,topic:"B"});
            n1.emit("input", {payload:3,topic:"C"});
        });
    });

    it('should handle multiple topics individually if asked to do so', function(done) {
        var flow = [{"id":"n1", "type":"trigger", "name":"triggerNode", bytopic:"topic", op1:"1", op2:"0", op1type:"num", op2type:"num", duration:"30", wires:[["n2"]] },
            {id:"n2", type:"helper"} ];
        helper.load(triggerNode, flow, function() {
            var n1 = helper.getNode("n1");
            var n2 = helper.getNode("n2");
            var c = 0;
            n2.on("input", function(msg) {
                try {
                    c += 1;
                    if (c === 1) {
                        msg.should.have.a.property("payload", 1);
                        msg.should.have.a.property("topic", "A");
                    }
                    else if (c === 2) {
                        msg.should.have.a.property("payload", 1);
                        msg.should.have.a.property("topic", "B");
                    }
                    else if (c === 3) {
                        msg.should.have.a.property("payload", 1);
                        msg.should.have.a.property("topic", "C");
                    }
                    else if (c === 4) {
                        msg.should.have.a.property("payload", 0);
                        msg.should.have.a.property("topic", "A");
                    }
                    else if (c === 5) {
                        msg.should.have.a.property("payload", 0);
                        msg.should.have.a.property("topic", "B");
                    }
                    else if (c === 6) {
                        msg.should.have.a.property("payload", 0);
                        msg.should.have.a.property("topic", "C");
                        done();
                    }
                } catch(err) {
                    done(err);
                }
            });
            n1.emit("input", {payload:1,topic:"A"});
            n1.emit("input", {payload:2,topic:"B"});
            n1.emit("input", {payload:3,topic:"C"});
        });
    });

    it('should handle multiple topics individually, and extend one, if asked to do so', function(done) {
        var flow = [{"id":"n1", "type":"trigger", "name":"triggerNode", bytopic:"topic", extend:"true", op1:"1", op2:"0", op1type:"num", op2type:"num", duration:"30", wires:[["n2"]] },
            {id:"n2", type:"helper"} ];
        helper.load(triggerNode, flow, function() {
            var n1 = helper.getNode("n1");
            var n2 = helper.getNode("n2");
            var c = 0;
            n2.on("input", function(msg) {
                try {
                    c += 1;
                    if (c === 1) {
                        msg.should.have.a.property("payload", 1);
                        msg.should.have.a.property("topic", "A");
                    }
                    else if (c === 2) {
                        msg.should.have.a.property("payload", 1);
                        msg.should.have.a.property("topic", "B");
                    }
                    else if (c === 3) {
                        msg.should.have.a.property("payload", 1);
                        msg.should.have.a.property("topic", "C");
                    }
                    else if (c === 4) {
                        msg.should.have.a.property("payload", 0);
                        msg.should.have.a.property("topic", "A");
                    }
                    else if (c === 5) {
                        msg.should.have.a.property("payload", 0);
                        msg.should.have.a.property("topic", "C");
                    }
                    else if (c === 6) {
                        msg.should.have.a.property("payload", 0);
                        msg.should.have.a.property("topic", "B");
                        done();
                    }
                } catch(err) {
                    done(err);
                }
            });
            n1.emit("input", {payload:1,topic:"A"});
            n1.emit("input", {payload:2,topic:"B"});
            n1.emit("input", {payload:3,topic:"C"});
            setTimeout( function() { n1.emit("input", {payload:2,topic:"B"})}, 20 );
        });
    });

    it('should handle multiple other properties individually if asked to do so', function(done) {
        var flow = [{"id":"n1", "type":"trigger", "name":"triggerNode", bytopic:"topic", topic:"foo", op1:"1", op2:"0", op1type:"num", op2type:"num", duration:"30", wires:[["n2"]] },
            {id:"n2", type:"helper"} ];
        helper.load(triggerNode, flow, function() {
            var n1 = helper.getNode("n1");
            var n2 = helper.getNode("n2");
            var c = 0;
            n2.on("input", function(msg) {
                try {
                    c += 1;
                    if (c === 1) {
                        msg.should.have.a.property("payload", 1);
                        msg.should.have.a.property("foo", "A");
                    }
                    else if (c === 2) {
                        msg.should.have.a.property("payload", 1);
                        msg.should.have.a.property("foo", "B");
                    }
                    else if (c === 3) {
                        msg.should.have.a.property("payload", 1);
                        msg.should.have.a.property("foo", "C");
                    }
                    else if (c === 4) {
                        msg.should.have.a.property("payload", 0);
                        msg.should.have.a.property("foo", "A");
                    }
                    else if (c === 5) {
                        msg.should.have.a.property("payload", 0);
                        msg.should.have.a.property("foo", "B");
                    }
                    else if (c === 6) {
                        msg.should.have.a.property("payload", 0);
                        msg.should.have.a.property("foo", "C");
                        done();
                    }
                } catch(err) {
                    done(err);
                }
            });
            n1.emit("input", {payload:1,foo:"A"});
            n1.emit("input", {payload:2,foo:"B"});
            n1.emit("input", {payload:3,foo:"C"});
        });
    });

    it('should be able to return things from flow and global context variables', function(done) {
        var spy = sinon.stub(RED.util, 'evaluateNodeProperty').callsFake(
            function(arg1, arg2, arg3, arg4, arg5) { if (arg5) { arg5(null, arg1) } else { return arg1; } }
        );
        var flow = [{"id":"n1", "type":"trigger", "name":"triggerNode", op1:"foo", op1type:"flow", op2:"bar", op2type:"global", duration:"20", wires:[["n2"]] },
            {id:"n2", type:"helper"} ];
        helper.load(triggerNode, flow, function() {
            var n1 = helper.getNode("n1");
            var n2 = helper.getNode("n2");
            var c = 0;
            n2.on("input", function(msg) {
                try {
                    if (c === 0) {
                        msg.should.have.a.property("payload", "foo");
                        c+=1;
                    }
                    else {
                        msg.should.have.a.property("payload", "bar");
                        RED.util.evaluateNodeProperty.restore();
                        done();
                    }
                }
                catch(err) { RED.util.evaluateNodeProperty.restore(); done(err); }
            });
            n1.emit("input", {payload:null});
        });
    });

    it('should be able to return things from persistable flow and global context variables', function (done) {
        var flow = [{"id": "n1", "type": "trigger", "name": "triggerNode", "op1": "#:(memory1)::foo", "op1type": "flow",
            "op2": "#:(memory1)::bar", "op2type": "global", "duration": "20", "wires": [["n2"]], "z": "flow" },
        {"id": "n2", "type": "helper"}];
        helper.load(triggerNode, flow, function () {
            initContext(function () {
                var n1 = helper.getNode("n1");
                var n2 = helper.getNode("n2");
                var c = 0;
                n2.on("input", function (msg) {
                    try {
                        if (c === 0) {
                            msg.should.have.a.property("payload", "foo");
                            c += 1;
                        } else {
                            msg.should.have.a.property("payload", "bar");
                            done();
                        }
                    } catch (err) {
                        done(err);
                    }
                });
                var context = n1.context();
                var flow = context.flow;
                var global = context.global;
                flow.set("foo", "foo", "memory1", function (err) {
                    global.set("bar", "bar", "memory1", function (err) {
                        n1.emit("input", { payload: null });
                    });
                });
            });
        });
    });

    it('should be able to return things from multiple persistable global context variables', function (done) {
        var flow = [{"id": "n1", "z": "flow", "type": "trigger",
            "duration": "20", "wires": [["n2"]],
            "op1": "#:(memory1)::val", "op1type": "global",
            "op2": "#:(memory2)::val", "op2type": "global"
        },
        {"id": "n2", "type": "helper"}];
        helper.load(triggerNode, flow, function () {
            initContext(function () {
                var n1 = helper.getNode("n1");
                var n2 = helper.getNode("n2");
                var count = 0;
                n2.on("input", function (msg) {
                    try {
                        if (count === 0) {
                            msg.should.have.a.property("payload", "foo");
                        }
                        else {
                            msg.should.have.a.property("payload", "bar");
                        }
                        count++;
                        if (count === 1) {
                            done();
                        }
                    }
                    catch (err) {
                        done(err);
                    }
                });
                var global = n1.context().global;
                global.set("val", "foo", "memory1", function (err) {
                    global.set("val", "bar", "memory2", function (err) {
                        n1.emit("input", { payload: null });
                    });
                });
            });
        });
    });

    it('should be able to return things from multiple persistable flow context variables', function (done) {
        var flow = [{"id": "n1", "z": "flow", "type": "trigger",
            "duration": "20", "wires": [["n2"]],
            "op1": "#:(memory1)::val", "op1type": "flow",
            "op2": "#:(memory2)::val", "op2type": "flow"
        },
        {"id": "n2", "type": "helper"}];
        helper.load(triggerNode, flow, function () {
            initContext(function () {
                var n1 = helper.getNode("n1");
                var n2 = helper.getNode("n2");
                var count = 0;
                n2.on("input", function (msg) {
                    try {
                        if (count === 0) {
                            msg.should.have.a.property("payload", "foo");
                        }
                        else {
                            msg.should.have.a.property("payload", "bar");
                        }
                        count++;
                        if (count === 1) {
                            done();
                        }
                    }
                    catch (err) {
                        done(err);
                    }
                });
                var flow = n1.context().flow;
                flow.set("val", "foo", "memory1", function (err) {
                    flow.set("val", "bar", "memory2", function (err) {
                        n1.emit("input", { payload: null });
                    });
                });
            });
        });
    });

    it('should be able to return things from multiple persistable flow & global context variables', function (done) {
        var flow = [{"id": "n1", "z": "flow", "type": "trigger",
            "duration": "20", "wires": [["n2"]],
            "op1": "#:(memory1)::val", "op1type": "flow",
            "op2": "#:(memory2)::val", "op2type": "global"
        },
        {"id": "n2", "type": "helper"}];
        helper.load(triggerNode, flow, function () {
            initContext(function () {
                var n1 = helper.getNode("n1");
                var n2 = helper.getNode("n2");
                var count = 0;
                n2.on("input", function (msg) {
                    try {
                        if (count === 0) {
                            msg.should.have.a.property("payload", "foo");
                        }
                        else {
                            msg.should.have.a.property("payload", "bar");
                        }
                        count++;
                        if (count === 1) {
                            done();
                        }
                    }
                    catch (err) {
                        done(err);
                    }
                });
                var context = n1.context();
                var flow = context.flow;
                var global = context.flow;
                flow.set("val", "foo", "memory1", function (err) {
                    global.set("val", "bar", "memory2", function (err) {
                        n1.emit("input", { payload: null });
                    });
                });
            });
        });
    });

    it('should be able to not output anything on first trigger', function(done) {
        var flow = [{"id":"n1", "type":"trigger", "name":"triggerNode", op1type:"nul", op1:"true",op2:"false",op2type:"val",duration:"30", wires:[["n2"]] },
            {id:"n2", type:"helper"} ];
        helper.load(triggerNode, flow, function() {
            var n1 = helper.getNode("n1");
            var n2 = helper.getNode("n2");
            n2.on("input", function(msg) {
                try {
                    msg.should.have.a.property("payload", false);
                    done();
                } catch(err) {
                    done(err);
                }
            });
            n1.emit("input", {payload:null});
        });
    });

    it('should be able to not output anything on second edge', function(done) {
        var flow = [{"id":"n1", "type":"trigger", "name":"triggerNode", op2type:"nul", op1:"true",op1type:"val", op2:"false", duration:"30", wires:[["n2"]] },
            {id:"n2", type:"helper"} ];
        helper.load(triggerNode, flow, function() {
            var n1 = helper.getNode("n1");
            var n2 = helper.getNode("n2");
            var c = 0;
            n2.on("input", function(msg) {
                try {
                    msg.should.have.a.property("payload", true);
                    c += 1;
                }
                catch(err) { done(err); }
            });
            setTimeout( function() {
                c.should.equal(1); // should only have had one output.
                done();
            },90);
            n1.emit("input", {payload:null});
        });
    });

    it('should be able to reset correctly having not output anything on second edge', function(done) {
        var flow = [{"id":"n1", "type":"trigger", "name":"triggerNode", op2type:"nul", op1:"true",op1type:"val", op2:"false", duration:"100", wires:[["n2"]] },
            {id:"n2", type:"helper"} ];
        helper.load(triggerNode, flow, function() {
            var n1 = helper.getNode("n1");
            var n2 = helper.getNode("n2");
            var c = 0;
            var errors = [];
            n2.on("input", function(msg) {
                try {
                    msg.should.have.a.property("topic", "pass")
                    msg.should.have.a.property("payload", true);
                    c += 1;
                }
                catch(err) { errors.push(err) }
            });
            setTimeout( function() {
                if (errors.length > 0) {
                    done(errors[0])
                } else {
                    c.should.equal(2);
                    done();
                }
            },350);
            n1.emit("input", {payload:1, topic:"pass"});
            setTimeout( function() {
                n1.emit("input", {payload:2, topic:"should-block"});
            },50);
            setTimeout( function() {
                n1.emit("input", {payload:3, topic:"pass"});
            },200);
            setTimeout( function() {
                n1.emit("input", {payload:2, topic:"should-block"});
            },250);
        });
    });

    it('should be able to extend the delay', function(done) {
        this.timeout(5000); // add extra time for flake
        var spy = sinon.stub(RED.util, 'evaluateNodeProperty').callsFake(
            function(arg1, arg2, arg3, arg4, arg5) { if (arg5) { arg5(null, arg1) } else { return arg1; } }
        );
        var flow = [{"id":"n1", "type":"trigger", "name":"triggerNode", extend:"true", op1type:"flow", op1:"foo",  op2:"bar", op2type:"global", duration:"100", wires:[["n2"]] },
            {id:"n2", type:"helper"} ];
        helper.load(triggerNode, flow, function() {
            var n1 = helper.getNode("n1");
            var n2 = helper.getNode("n2");
            var c = 0;
            n2.on("input", function(msg) {
                try {
                    if (c === 0) {
                        msg.should.have.a.property("payload", "foo");
                        c += 1;
                    }
                    else {
                        msg.should.have.a.property("payload", "bar");
                        //console.log(Date.now() - ss);
                        (Date.now() - ss).should.be.greaterThan(149);
                        spy.restore();
                        done();
                    }
                }
                catch(err) { spy.restore(); done(err); }
            });
            var ss = Date.now();
            n1.emit("input", {payload:"Hello"});
            setTimeout( function() {
                n1.emit("input", {payload:null});
            },50);
        });
    });

    it('should be able to extend the delay (but with no 2nd output)', function(done) {
        var flow = [{"id":"n1", "type":"trigger", "name":"triggerNode", extend:"true", op1type:"pay", op2type:"nul", op1:"false",  op2:"true", duration:"200", wires:[["n2"]] },
            {id:"n2", type:"helper"} ];
        helper.load(triggerNode, flow, function() {
            var n1 = helper.getNode("n1");
            var n2 = helper.getNode("n2");
            var c = 0;
            n2.on("input", function(msg) {
                try {
                    if (c === 0) {
                        msg.should.have.a.property("payload", "Hello");
                        c += 1;
                    }
                    else {
                        msg.should.have.a.property("payload", "World");
                        (Date.now() - ss).should.be.greaterThan(300);
                        done();
                    }
                } catch(err) {
                    console.log(err);
                    done(err);
                }
            });
            var ss = Date.now();
            n1.emit("input", {payload:"Hello"});
            setTimeout( function() {
                n1.emit("input", {payload:"Error"});
            },50);
            setTimeout( function() {
                n1.emit("input", {payload:"Error"});
            },100);
            setTimeout( function() {
                n1.emit("input", {payload:"World"});
            },330);
        });
    });

    it('should be able to extend the delay and output the most recent payload', function(done) {
        var flow = [{"id":"n1", "type":"trigger", "name":"triggerNode", extend:"true", op1type:"nul", op2type:"payl", op1:"false", op2:"true", duration:"60", wires:[["n2"]] },
            {id:"n2", type:"helper"} ];
        helper.load(triggerNode, flow, function() {
            var n1 = helper.getNode("n1");
            var n2 = helper.getNode("n2");
            var c = 0;
            n2.on("input", function(msg) {
                try {
                    msg.should.have.a.property("payload", "World");
                    (Date.now() - ss).should.be.greaterThan(120);
                    done();
                }
                catch(err) { done(err); }
            });
            var ss = Date.now();
            n1.emit("input", {payload:"Hello"});
            setTimeout( function() {
                n1.emit("input", {payload:"Goodbye"});
            },40);
            setTimeout( function() {
                n1.emit("input", {payload:"World"});
            },80);
        });
    });

    it('should be able output the 2nd payload', function(done) {
        var flow = [{"id":"n1", "type":"trigger", "name":"triggerNode", extend:"false", op1type:"nul", op2type:"payl", op1:"false", op2:"true", duration:"50", wires:[["n2"]] },
            {id:"n2", type:"helper"} ];
        helper.load(triggerNode, flow, function() {
            var n1 = helper.getNode("n1");
            var n2 = helper.getNode("n2");
            var c = 0;
            n2.on("input", function(msg) {
                try {
                    if (c === 0) {
                        msg.should.have.a.property("payload", "Goodbye");
                        msg.should.have.a.property("topic", "test2");
                        c += 1;
                    }
                    else {
                        msg.should.have.a.property("payload", "World");
                        msg.should.have.a.property("topic", "test3");
                        (Date.now() - ss).should.be.greaterThan(70);
                        done();
                    }
                }
                catch(err) { done(err); }
            });
            var ss = Date.now();
            n1.emit("input", {payload:"Hello", topic:"test1"});
            setTimeout( function() {
                n1.emit("input", {payload:"Goodbye", topic:"test2"});
            },20);
            setTimeout( function() {
                n1.emit("input", {payload:"World", topic:"test3"});
            },80);
        });
    });

    it('should be able output the 2nd payload and handle multiple topics', function(done) {
        var flow = [{"id":"n1", "type":"trigger", "name":"triggerNode", extend:"false", op1type:"nul", op2type:"payl", op1:"false", op2:"true", duration:"80", bytopic:"topic", wires:[["n2"]] },
            {id:"n2", type:"helper"} ];
        helper.load(triggerNode, flow, function() {
            var n1 = helper.getNode("n1");
            var n2 = helper.getNode("n2");
            var c = 0;
            n2.on("input", function(msg) {
                try {
                    if (c === 0) {
                        msg.should.have.a.property("payload", "Goodbye1");
                        msg.should.have.a.property("topic", "test1");
                        c += 1;
                    }
                    else {
                        msg.should.have.a.property("payload", "Goodbye2");
                        msg.should.have.a.property("topic", "test2");
                        done();
                    }
                }
                catch(err) { done(err); }
            });
            n1.emit("input", {payload:"Hello1", topic:"test1"});
            setTimeout( function() {
                n1.emit("input", {payload:"Hello2", topic:"test2"});
            },20);
            setTimeout( function() {
                n1.emit("input", {payload:"Goodbye2", topic:"test2"});
            },20);
            setTimeout( function() {
                n1.emit("input", {payload:"Goodbye1", topic:"test1"});
            },20);
        });
    });

    it('should be able to apply mustache templates to payloads', function(done) {
        var flow = [{"id":"n1", "type":"trigger", "name":"triggerNode", op1type:"val", op2type:"val", op1:"{{payload}}",  op2:"{{topic}}", duration:"50", wires:[["n2"]] },
            {id:"n2", type:"helper"} ];
        helper.load(triggerNode, flow, function() {
            var n1 = helper.getNode("n1");
            var n2 = helper.getNode("n2");
            var c = 0;
            n2.on("input", function(msg) {
                try {
                    if (c === 0) {
                        msg.should.have.a.property("payload", "Hello");
                        c+=1;
                    }
                    else {
                        msg.should.have.a.property("payload", "World");
                        done();
                    }
                }
                catch(err) { done(err); }
            });
            n1.emit("input", {payload:"Hello",topic:"World"});
        });
    });

    it('should be able to send 2nd message to a 2nd output', function(done) {
        var flow = [{"id":"n1", "type":"trigger", "name":"triggerNode", op1type:"val", op2type:"val", op1:"hello",  op2:"world", duration:"50", outputs:2, wires:[["n2"],["n3"]] },
            {id:"n2", type:"helper"}, {id:"n3", type:"helper"} ];
        helper.load(triggerNode, flow, function() {
            var n1 = helper.getNode("n1");
            var n2 = helper.getNode("n2");
            var n3 = helper.getNode("n3");
            var c = 0;
            n2.on("input", function(msg) {
                try {
                    if (c === 0) {
                        msg.should.have.a.property("payload", "hello");
                        msg.should.have.a.property("topic", "test");
                        c+=1;
                    }
                    else { done(err); }
                }
                catch(err) { done(err); }
            });
            n3.on("input", function(msg) {
                try {
                    if (c === 1) {
                        msg.should.have.a.property("payload", "world");
                        msg.should.have.a.property("topic", "test");
                        done();
                    }
                    else { done(err); }
                }
                catch(err) { done(err); }
            });
            n1.emit("input", {payload:"go",topic:"test"});
        });
    });

    it('should handle string null as null', function(done) {
        var flow = [{"id":"n1", "type":"trigger", "name":"triggerNode", op1type:"val", op2type:"pay", op1:"null", op2:"null", duration:"40", wires:[["n2"]] },
            {id:"n2", type:"helper"} ];
        helper.load(triggerNode, flow, function() {
            var n1 = helper.getNode("n1");
            var n2 = helper.getNode("n2");
            var c = 0;
            n2.on("input", function(msg) {
                try {
                    if (c === 0) {
                        msg.should.have.a.property("payload", null);
                        c+=1;
                    }
                    else {
                        msg.should.have.a.property("payload", "World");
                        done();
                    }
                }
                catch(err) { done(err); }
            });
            n1.emit("input", {payload:"World"});
        });
    });

    it('should handle string null as null on op2', function(done) {
        var flow = [{"id":"n1", "type":"trigger", "name":"triggerNode", op1type:"val", op2type:"val", op1:"null", op2:"null", duration:"40", wires:[["n2"]] },
            {id:"n2", type:"helper"} ];
        helper.load(triggerNode, flow, function() {
            var n1 = helper.getNode("n1");
            var n2 = helper.getNode("n2");
            var c = 0;
            n2.on("input", function(msg) {
                try {
                    if (c === 0) {
                        msg.should.have.a.property("payload", null);
                        c+=1;
                    }
                    else {
                        msg.should.have.a.property("payload", null);
                        done();
                    }
                }
                catch(err) { done(err); }
            });
            n1.emit("input", {payload:"null"});
        });
    });

    it('should be able to set infinite timeout, and clear timeout', function(done) {
        var flow = [{"id":"n1", "type":"trigger", "name":"triggerNode", duration:"0", extend: false, wires:[["n2"]] },
            {id:"n2", type:"helper"} ];
        helper.load(triggerNode, flow, function() {
            var n1 = helper.getNode("n1");
            var n2 = helper.getNode("n2");
            var c = 0;
            n2.on("input", function(msg) {
                try {
                    c += 1;
                    msg.should.have.a.property("payload", "1");
                }
                catch(err) { done(err); }
            });
            setTimeout( function() {
                if (c === 2) { done(); }
                else {
                    done(new Error("Too many messages received"));
                }
            },20);
            n1.emit("input", {payload:null});   // trigger
            n1.emit("input", {payload:null});   // blocked
            n1.emit("input", {payload:null});   // blocked
            n1.emit("input", {reset:true});     // clear the blockage
            n1.emit("input", {payload:null});   // trigger
        });
    });

    it('should be able to set infinite timeout, and clear timeout by message', function(done) {
        var flow = [{"id":"n1", "type":"trigger", "name":"triggerNode", reset:"boo", duration:"0", wires:[["n2"]] },
            {id:"n2", type:"helper"} ];
        helper.load(triggerNode, flow, function() {
            var n1 = helper.getNode("n1");
            var n2 = helper.getNode("n2");
            var c = 0;
            n2.on("input", function(msg) {
                try {
                    c += 1;
                    msg.should.have.a.property("payload", "1");
                }
                catch(err) { done(err); }
            });
            n1.emit("input", {payload:null});   // trigger
            n1.emit("input", {payload:null});   // blocked
            n1.emit("input", {payload:null});   // blocked
            n1.emit("input", {payload:"foo"});  // don't clear the blockage
            n1.emit("input", {payload:"boo"});  // clear the blockage
            n1.emit("input", {payload:null});   // trigger
            setTimeout( function() {
                if (c === 2) { done(); }
                else {
                    done(new Error("Too many messages received"));
                }
            },50);
        });
    });

    it('should be able to set infinite timeout, and clear timeout by boolean true', function(done) {
        var flow = [{"id":"n1", "type":"trigger", "name":"triggerNode", reset:"true", duration:"0", wires:[["n2"]] },
            {id:"n2", type:"helper"} ];
        helper.load(triggerNode, flow, function() {
            var n1 = helper.getNode("n1");
            var n2 = helper.getNode("n2");
            var c = 0;
            n2.on("input", function(msg) {
                try {
                    c += 1;
                    msg.should.have.a.property("payload", "1");
                }
                catch(err) { done(err); }
            });
            setTimeout( function() {
                if (c === 2) { done(); }
                else {
                    done(new Error("Too many messages received"));
                }
            },20);
            n1.emit("input", {payload:null});   // trigger
            n1.emit("input", {payload:null});   // blocked
            n1.emit("input", {payload:null});   // blocked
            n1.emit("input", {payload:false});  // don't clear the blockage
            n1.emit("input", {payload:true});  // clear the blockage
            n1.emit("input", {payload:null});   // trigger
        });
    });

    it('should be able to set infinite timeout, and clear timeout by boolean false', function(done) {
        var flow = [{"id":"n1", "type":"trigger", "name":"triggerNode", reset:"false", duration:"0", wires:[["n2"]] },
            {id:"n2", type:"helper"} ];
        helper.load(triggerNode, flow, function() {
            var n1 = helper.getNode("n1");
            var n2 = helper.getNode("n2");
            var c = 0;
            n2.on("input", function(msg) {
                try {
                    c += 1;
                    msg.should.have.a.property("payload", "1");
                }
                catch(err) { done(err); }
            });
            setTimeout( function() {
                if (c === 2) { done(); }
                else {
                    done(new Error("Too many messages received"));
                }
            },20);
            n1.emit("input", {payload:null});   // trigger
            n1.emit("input", {payload:null});   // blocked
            n1.emit("input", {payload:null});   // blocked
            n1.emit("input", {payload:"foo"});  // don't clear the blockage
            n1.emit("input", {payload:false});  // clear the blockage
            n1.emit("input", {payload:null});   // trigger
        });
    });

    it('should be able to set a repeat, and clear loop by reset', function(done) {
        var flow = [{"id":"n1", "type":"trigger", "name":"triggerNode", reset:"boo", op1:"", op1type:"pay", duration:-25, wires:[["n2"]] },
            {id:"n2", type:"helper"} ];
        helper.load(triggerNode, flow, function() {
            var n1 = helper.getNode("n1");
            var n2 = helper.getNode("n2");
            var c = 0;
            n2.on("input", function(msg) {
                c += 1;
                try {
                    msg.should.have.property('payload','foo');
                    msg.payload = "bar"; // try to provoke pass by reference error
                }
                catch(err) { done(err); }
            });
            n1.emit("input", {payload:"foo"});   // trigger
            n1.emit("input", {payload:"foo"});   // trigger
            setTimeout( function() {
                n1.emit("input", {reset:true});   // reset
            },90);
            setTimeout( function() {
                c.should.within(2,5);  // should send foo between 2 and 5 times.
                done();
            },180);
        });
    });
    describe('messaging API', function () {
        function mapiDoneTriggerTestHelper(done, nodeSetting, 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 = [
                { ...nodeSetting, id: "triggerNode1", type: "trigger", wires: [[]] },
                { id: "completeNode1", type: "complete", scope: ["triggerNode1"], uncaught: false, wires: [["helperNode1"]] },
                { id: "catchNode1", type: "catch", scope: ["triggerNode1"], uncaught: false, wires: [["helperNode1"]] },
                { id: "helperNode1", type: "helper", wires: [[]] }];
            const numMsgs = msgAndTimings.length;
            helper.load([triggerNode, completeNode, catchNode], flow, function () {
                const triggerNode1 = helper.getNode("triggerNode1");
                const helperNode1 = helper.getNode("helperNode1");
                RED.settings.nodeMessageBufferMaxLength = 3;
                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.seq].avr, msgAndTimings[msg.seq].var);
                    c += 1;
                    if (c === numMsgs) {
                        done();
                    }
                });
                for (let i = 0; i < numMsgs; i++) {
                    setTimeout(function () { triggerNode1.receive(msgAndTimings[i].msg); }, msgAndTimings[i].delay);
                }
            });
        }
        it('should call done() when first message has been processed', function (done) {
            // not when second and more messages are emitted.
            mapiDoneTriggerTestHelper(done, { units:"s", duration:"1" }, [
                { msg: { seq: 0, payload: "A"}, delay: 0, avr: 0, var: 100}
            ]);
        });
        it('should call done() when it receives reset message', function (done) {
            mapiDoneTriggerTestHelper(done, {units:"s", duration:"1"}, [
                {msg: { seq: 0, payload: "A", reset:true}, delay: 0, avr: 0, var:100}
            ]);
        })
    });
});
