/**
 * 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 clone = require('clone');
var util = require("util");

var flowUtils = require("../../../../../red/runtime/nodes/flows/util");
var Flow = require("../../../../../red/runtime/nodes/flows/Flow");
var flows = require("../../../../../red/runtime/nodes/flows");
var Node = require("../../../../../red/runtime/nodes/Node");
var typeRegistry = require("../../../../../red/runtime/nodes/registry");


describe('Flow', function() {
    var getType;
    var getNode;

    var stoppedNodes = {};
    var currentNodes = {};
    var rewiredNodes = {};
    var createCount = 0;

    beforeEach(function() {
        currentNodes = {};
        stoppedNodes = {};
        rewiredNodes = {};
        createCount = 0;
        Flow.init({});
    });

    var TestNode = function(n) {
        Node.call(this,n);
        this._index = createCount++;
        this.scope = n.scope;
        var node = this;
        this.foo = n.foo;
        this.handled = 0;
        this.stopped = false;
        currentNodes[node.id] = node;
        this.on('input',function(msg) {
            node.handled++;
            node.send(msg);
        });
        this.on('close',function() {
            node.stopped = true;
            stoppedNodes[node.id] = node;
            delete currentNodes[node.id];
        });
        this.__updateWires = this.updateWires;
        this.updateWires = function(newWires) {
            rewiredNodes[node.id] = node;
            node.newWires = newWires;
            node.__updateWires(newWires);
        };
    }
    util.inherits(TestNode,Node);

    var TestAsyncNode = function(n) {
        Node.call(this,n);
        var node = this;
        this.scope = n.scope;
        this.foo = n.foo;
        this.handled = 0;
        this.messages = [];
        this.stopped = false;
        this.closeDelay = n.closeDelay || 50;
        currentNodes[node.id] = node;
        this.on('input',function(msg) {
            node.handled++;
            node.messages.push(msg);
            node.send(msg);
        });
        this.on('close',function(done) {
            setTimeout(function() {
                node.stopped = true;
                stoppedNodes[node.id] = node;
                delete currentNodes[node.id];
                done();
            },node.closeDelay);
        });
    }
    util.inherits(TestAsyncNode,Node);

    before(function() {
        getType = sinon.stub(typeRegistry,"get",function(type) {
            if (type=="test") {
                return TestNode;
            } else {
                return TestAsyncNode;
            }
        });
        getNode = sinon.stub(flows,"get",function(id) {
            return currentNodes[id];
        });

    });
    after(function() {
        getType.restore();
        getNode.restore();
    });



    describe('#constructor',function() {
        it('called with an empty flow',function() {
            var config = flowUtils.parseConfig([]);
            var flow = Flow.create(config);

            var nodeCount = 0;
            Object.keys(flow.getActiveNodes()).length.should.equal(0);
        });
    });
    describe('#start',function() {
        it("instantiates an initial configuration and stops it",function(done) {
            var config = flowUtils.parseConfig([
                {id:"t1",type:"tab"},
                {id:"1",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["2"]},
                {id:"2",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["3"]},
                {id:"3",x:10,y:10,z:"t1",type:"test",foo:"a",wires:[]},
                {id:"4",z:"t1",type:"test",foo:"a"}
            ]);
            var flow = Flow.create(config,config.flows["t1"]);
            flow.start();

            Object.keys(flow.getActiveNodes()).should.have.length(4);

            flow.getNode('1').should.have.a.property('id','1');
            flow.getNode('2').should.have.a.property('id','2');
            flow.getNode('3').should.have.a.property('id','3');
            flow.getNode('4').should.have.a.property('id','4');

            currentNodes.should.have.a.property("1");
            currentNodes.should.have.a.property("2");
            currentNodes.should.have.a.property("3");
            currentNodes.should.have.a.property("4");

            currentNodes["1"].should.have.a.property("handled",0);
            currentNodes["2"].should.have.a.property("handled",0);
            currentNodes["3"].should.have.a.property("handled",0);


            currentNodes["1"].receive({payload:"test"});

            currentNodes["1"].should.have.a.property("handled",1);
            currentNodes["2"].should.have.a.property("handled",1);
            currentNodes["3"].should.have.a.property("handled",1);

            flow.stop().then(function() {
                currentNodes.should.not.have.a.property("1");
                currentNodes.should.not.have.a.property("2");
                currentNodes.should.not.have.a.property("3");
                currentNodes.should.not.have.a.property("4");
                stoppedNodes.should.have.a.property("1");
                stoppedNodes.should.have.a.property("2");
                stoppedNodes.should.have.a.property("3");
                stoppedNodes.should.have.a.property("4");
                done();
            });
        });


        it("instantiates config nodes in the right order",function(done) {
            var config = flowUtils.parseConfig([
                {id:"t1",type:"tab"},
                {id:"1",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["2"]},
                {id:"2",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["3"]},
                {id:"3",x:10,y:10,z:"t1",type:"test",foo:"a",wires:[]},
                {id:"4",z:"t1",type:"test",foo:"5"}, // This node depends on #5
                {id:"5",z:"t1",type:"test"}
            ]);
            var flow = Flow.create(config,config.flows["t1"]);
            flow.start();

            Object.keys(flow.getActiveNodes()).should.have.length(5);


            currentNodes.should.have.a.property("1");
            currentNodes.should.have.a.property("2");
            currentNodes.should.have.a.property("3");
            currentNodes.should.have.a.property("4");
            currentNodes.should.have.a.property("5");

            currentNodes["1"].should.have.a.property("_index",2);
            currentNodes["2"].should.have.a.property("_index",3);
            currentNodes["3"].should.have.a.property("_index",4);
            currentNodes["4"].should.have.a.property("_index",1);
            currentNodes["5"].should.have.a.property("_index",0);

            flow.stop().then(function() {
                currentNodes.should.not.have.a.property("1");
                currentNodes.should.not.have.a.property("2");
                currentNodes.should.not.have.a.property("3");
                currentNodes.should.not.have.a.property("4");
                currentNodes.should.not.have.a.property("5");
                stoppedNodes.should.have.a.property("1");
                stoppedNodes.should.have.a.property("2");
                stoppedNodes.should.have.a.property("3");
                stoppedNodes.should.have.a.property("4");
                stoppedNodes.should.have.a.property("5");
                done();
            });
        });


        it("detects dependency loops in config nodes",function() {
            var config = flowUtils.parseConfig([
                {id:"t1",type:"tab"},
                {id:"node1",z:"t1",type:"test",foo:"node2"}, // This node depends on #5
                {id:"node2",z:"t1",type:"test",foo:"node1"}
            ]);
            var flow = Flow.create(config,config.flows["t1"]);
            /*jshint immed: false */
            (function(){
                flow.start();
            }).should.throw("Circular config node dependency detected: node1");
        });
        it("instantiates a subflow and stops it",function(done) {
            var config = flowUtils.parseConfig([
                {id:"t1",type:"tab"},
                {id:"1",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["2"]},
                {id:"2",x:10,y:10,z:"t1",type:"subflow:sf1",wires:["3","4"]},
                {id:"3",x:10,y:10,z:"t1",type:"test",foo:"a",wires:[]},
                {id:"4",x:10,y:10,z:"t1",type:"test",foo:"a",wires:[]},
                {id:"sf1",type:"subflow","name":"Subflow 2","info":"",
                    "in":[{"wires":[{"id":"sf1-1"}]}],"out":[{"wires":[{"id":"sf1-2","port":0}]},{"wires":[{"id":"sf1","port":0}]}]},
                {id:"sf1-1",type:"test","z":"sf1",x:166,y:99,"wires":[["sf1-2"]]},
                {id:"sf1-2",type:"test","z":"sf1",foo:"sf1-cn",x:166,y:99,"wires":[[]]},
                {id:"sf1-cn",type:"test","z":"sf1"}
            ]);
            var flow = Flow.create(config,config.flows["t1"]);

            getNode.restore();
            getNode = sinon.stub(flows,"get",function(id) {
                return flow.getNode(id);
            });

            flow.start();

            var activeNodes = flow.getActiveNodes();
            Object.keys(activeNodes).should.have.length(7);
            var sfInstanceId = Object.keys(activeNodes)[5];
            var sfInstanceId2 = Object.keys(activeNodes)[6];
            var sfConfigId = Object.keys(activeNodes)[4];

            flow.getNode('1').should.have.a.property('id','1');
            flow.getNode('2').should.have.a.property('id','2');
            flow.getNode('3').should.have.a.property('id','3');
            flow.getNode('4').should.have.a.property('id','4');
            flow.getNode(sfInstanceId).should.have.a.property('id',sfInstanceId);
            flow.getNode(sfInstanceId2).should.have.a.property('id',sfInstanceId2);
            flow.getNode(sfConfigId).should.have.a.property('id',sfConfigId);

            flow.getNode(sfInstanceId2).should.have.a.property('foo',sfConfigId);

            currentNodes.should.have.a.property("1");
            currentNodes.should.not.have.a.property("2");
            currentNodes.should.have.a.property("3");
            currentNodes.should.have.a.property("4");
            currentNodes.should.have.a.property(sfInstanceId);
            currentNodes.should.have.a.property(sfInstanceId2);
            currentNodes.should.have.a.property(sfConfigId);

            currentNodes["1"].should.have.a.property("handled",0);
            currentNodes["3"].should.have.a.property("handled",0);
            currentNodes["4"].should.have.a.property("handled",0);
            currentNodes[sfInstanceId].should.have.a.property("handled",0);
            currentNodes[sfInstanceId2].should.have.a.property("handled",0);

            currentNodes["1"].receive({payload:"test"});

            currentNodes["1"].should.have.a.property("handled",1);
            currentNodes[sfInstanceId].should.have.a.property("handled",1);
            currentNodes[sfInstanceId2].should.have.a.property("handled",1);
            currentNodes["3"].should.have.a.property("handled",1);
            currentNodes["4"].should.have.a.property("handled",1);



            flow.stop().then(function() {
                currentNodes.should.not.have.a.property("1");
                currentNodes.should.not.have.a.property("3");
                currentNodes.should.not.have.a.property("4");
                currentNodes.should.not.have.a.property(sfInstanceId);
                currentNodes.should.not.have.a.property(sfInstanceId2);
                currentNodes.should.not.have.a.property(sfConfigId);
                stoppedNodes.should.have.a.property("1");
                stoppedNodes.should.have.a.property("3");
                stoppedNodes.should.have.a.property("4");
                stoppedNodes.should.have.a.property(sfInstanceId);
                stoppedNodes.should.have.a.property(sfInstanceId2);
                stoppedNodes.should.have.a.property(sfConfigId);
                done();
            });
        });

        it("instantiates a subflow inside a subflow and stops it",function(done) {
            var config = flowUtils.parseConfig([
                {id:"t1",type:"tab"},
                {id:"1",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["2"]},
                {id:"2",x:10,y:10,z:"t1",type:"subflow:sf1",wires:["3","4"]},
                {id:"3",x:10,y:10,z:"t1",type:"test",foo:"a",wires:[]},
                {id:"4",x:10,y:10,z:"t1",type:"test",foo:"a",wires:[]},
                {id:"sf1",type:"subflow","name":"Subflow 1","info":"",
                    "in":[{"wires":[{"id":"sf1-1"}]}],"out":[{"wires":[{"id":"sf1-2","port":0}]}]},
                {id:"sf2",type:"subflow","name":"Subflow 2","info":"",
                    "in":[{wires:[]}],"out":[{"wires":[{"id":"sf2","port":0}]}]},
                {id:"sf1-1",type:"test","z":"sf1",x:166,y:99,"wires":[["sf1-2"]]},
                {id:"sf1-2",type:"subflow:sf2","z":"sf1",x:166,y:99,"wires":[[]]}

            ]);
            var flow = Flow.create(config,config.flows["t1"]);

            getNode.restore();
            getNode = sinon.stub(flows,"get",function(id) {
                return flow.getNode(id);
            });

            flow.start();

            currentNodes["1"].should.have.a.property("handled",0);
            currentNodes["3"].should.have.a.property("handled",0);

            currentNodes["1"].receive({payload:"test"});

            currentNodes["1"].should.have.a.property("handled",1);
            currentNodes["3"].should.have.a.property("handled",1);



            flow.stop().then(function() {
                Object.keys(currentNodes).should.have.length(0);
                done();
            });
        });

        it("rewires nodes specified by diff",function(done) {
            var config = flowUtils.parseConfig([
                {id:"t1",type:"tab"},
                {id:"1",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["2"]},
                {id:"2",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["3"]},
                {id:"3",x:10,y:10,z:"t1",type:"test",foo:"a",wires:[]}
            ]);

            var flow = Flow.create(config,config.flows["t1"]);
            createCount.should.equal(0);
            flow.start();
            //TODO: use update to pass in new wiring and verify the change
            createCount.should.equal(3);
            flow.start({rewired:["2"]});
            createCount.should.equal(3);
            rewiredNodes.should.have.a.property("2");
            done();
        });

        it("rewires a subflow node on update/start",function(done){

            var rawConfig = [
                {id:"t1",type:"tab"},
                {id:"1",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["2"]},
                {id:"2",x:10,y:10,z:"t1",type:"subflow:sf1",wires:["3"]},
                {id:"3",x:10,y:10,z:"t1",type:"test",foo:"a",wires:[]},
                {id:"4",x:10,y:10,z:"t1",type:"test",foo:"a",wires:[]},
                {id:"sf1",type:"subflow","name":"Subflow 2","info":"",
                    "in":[{"wires":[{"id":"sf1-1"}]}],"out":[{"wires":[{"id":"sf1-2","port":0}]}]},
                {id:"sf1-1",type:"test1","z":"sf1",x:166,y:99,"wires":[["sf1-2"]]},
                {id:"sf1-2",type:"test2","z":"sf1",x:166,y:99,"wires":[[]]}
            ];

            var config = flowUtils.parseConfig(clone(rawConfig));

            rawConfig[2].wires = [["4"]];

            var newConfig = flowUtils.parseConfig(rawConfig);
            var diff = flowUtils.diffConfigs(config,newConfig);
            var flow = Flow.create(config,config.flows["t1"]);

            getNode.restore();
            getNode = sinon.stub(flows,"get",function(id) {
                return flow.getNode(id);
            });

            flow.start();

            var activeNodes = flow.getActiveNodes();
            Object.keys(activeNodes).should.have.length(6);
            var sfInstanceId = Object.keys(activeNodes)[4];
            var sfInstanceId2 = Object.keys(activeNodes)[5];

            currentNodes["1"].should.have.a.property("handled",0);
            currentNodes["3"].should.have.a.property("handled",0);
            currentNodes["4"].should.have.a.property("handled",0);

            currentNodes["1"].receive({payload:"test"});

            currentNodes["1"].should.have.a.property("handled",1);
            currentNodes[sfInstanceId].should.have.a.property("handled",1);
            currentNodes[sfInstanceId2].should.have.a.property("handled",1);
            currentNodes["3"].should.have.a.property("handled",1);
            currentNodes["4"].should.have.a.property("handled",0);

            flow.update(newConfig,newConfig.flows["t1"]);
            flow.start(diff)

            currentNodes["1"].receive({payload:"test2"});

            currentNodes["1"].should.have.a.property("handled",2);
            currentNodes[sfInstanceId].should.have.a.property("handled",2);
            currentNodes[sfInstanceId2].should.have.a.property("handled",2);
            currentNodes["3"].should.have.a.property("handled",1);
            currentNodes["4"].should.have.a.property("handled",1);


            flow.stop().then(function() {
                done();
            });


        });


        it("instantiates a node with environment variable property values",function(done) {
            after(function() {
                delete process.env.NODE_RED_TEST_VALUE;
            })
            process.env.NODE_RED_TEST_VALUE = "a-value";
            var config = flowUtils.parseConfig([
                {id:"t1",type:"tab"},
                {id:"1",x:10,y:10,z:"t1",type:"test",foo:"$(NODE_RED_TEST_VALUE)",wires:[]},
                {id:"2",x:10,y:10,z:"t1",type:"test",foo:{a:"$(NODE_RED_TEST_VALUE)"},wires:[]},
                {id:"3",x:10,y:10,z:"t1",type:"test",foo:" $(NODE_RED_TEST_VALUE)",wires:[]},
                {id:"4",x:10,y:10,z:"t1",type:"test",foo:"$(NODE_RED_TEST_VALUE) ",wires:[]},
                {id:"5",x:10,y:10,z:"t1",type:"test",foo:"$(NODE_RED_TEST_VALUE_NONE)",wires:[]},
                {id:"6",x:10,y:10,z:"t1",type:"test",foo:["$(NODE_RED_TEST_VALUE)"],wires:[]}
            ]);
            var flow = Flow.create(config,config.flows["t1"]);
            flow.start();

            var activeNodes = flow.getActiveNodes();

            activeNodes["1"].foo.should.equal("a-value");
            activeNodes["2"].foo.a.should.equal("a-value");
            activeNodes["3"].foo.should.equal(" $(NODE_RED_TEST_VALUE)");
            activeNodes["4"].foo.should.equal("$(NODE_RED_TEST_VALUE) ");
            activeNodes["5"].foo.should.equal("$(NODE_RED_TEST_VALUE_NONE)");
            activeNodes["6"].foo[0].should.equal("a-value");

            flow.stop().then(function() {
                done();
            });
        });


    });

    describe('#stop', function() {


        it("stops all nodes",function(done) {
            var config = flowUtils.parseConfig([
                {id:"t1",type:"tab"},
                {id:"1",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["2"]},
                {id:"2",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["3"]},
                {id:"3",x:10,y:10,z:"t1",type:"asyncTest",foo:"a",wires:[]}
            ]);
            var flow = Flow.create(config,config.flows["t1"]);
            flow.start();


            currentNodes.should.have.a.property("1");
            currentNodes.should.have.a.property("2");
            currentNodes.should.have.a.property("3");

            flow.stop().then(function() {
                currentNodes.should.not.have.a.property("1");
                currentNodes.should.not.have.a.property("2");
                currentNodes.should.not.have.a.property("3");
                stoppedNodes.should.have.a.property("1");
                stoppedNodes.should.have.a.property("2");
                stoppedNodes.should.have.a.property("3");
                done();
            });
        });

        it("stops specified nodes",function(done) {
            var config = flowUtils.parseConfig([
                {id:"t1",type:"tab"},
                {id:"1",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["2"]},
                {id:"2",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["3"]},
                {id:"3",x:10,y:10,z:"t1",type:"test",foo:"a",wires:[]}
            ]);
            var flow = Flow.create(config,config.flows["t1"]);
            flow.start();

            currentNodes.should.have.a.property("1");
            currentNodes.should.have.a.property("2");
            currentNodes.should.have.a.property("3");

            flow.stop(["2"]).then(function() {
                currentNodes.should.have.a.property("1");
                currentNodes.should.not.have.a.property("2");
                currentNodes.should.have.a.property("3");
                stoppedNodes.should.not.have.a.property("1");
                stoppedNodes.should.have.a.property("2");
                stoppedNodes.should.not.have.a.property("3");
                done();
            });
        });

        it("stops subflow instance nodes",function(done) {
            var config = flowUtils.parseConfig([
                {id:"t1",type:"tab"},
                {id:"1",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["2"]},
                {id:"2",x:10,y:10,z:"t1",type:"subflow:sf1",wires:["3"]},
                {id:"3",x:10,y:10,z:"t1",type:"test",foo:"a",wires:[]},
                {id:"sf1",type:"subflow","name":"Subflow 2","info":"",
                    "in":[{"wires":[{"id":"sf1-1"}]}],"out":[{"wires":[{"id":"sf1-1","port":0}]}]},
                {id:"sf1-1",type:"test","z":"sf1",x:166,y:99,"wires":[[]]}
            ]);
            var flow = Flow.create(config,config.flows["t1"]);

            getNode.restore();
            getNode = sinon.stub(flows,"get",function(id) {
                return flow.getNode(id);
            });

            flow.start();

            var activeNodes = flow.getActiveNodes();
            Object.keys(activeNodes).should.have.length(4);
            var sfInstanceId = Object.keys(activeNodes)[3];
            flow.stop(["2"]).then(function() {
                currentNodes.should.not.have.a.property(sfInstanceId);
                stoppedNodes.should.have.a.property(sfInstanceId);
                done();
            });
        });

        it("Times out a node that fails to close", function(done) {

            Flow.init({nodeCloseTimeout:50});


            var config = flowUtils.parseConfig([
                {id:"t1",type:"tab"},
                {id:"1",x:10,y:10,z:"t1",type:"testAsync",closeDelay: 80, foo:"a",wires:["2"]},
                {id:"2",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["3"]},
                {id:"3",x:10,y:10,z:"t1",type:"test",foo:"a",wires:[]}
            ]);
            var flow = Flow.create(config,config.flows["t1"]);
            flow.start();

            currentNodes.should.have.a.property("1");
            currentNodes.should.have.a.property("2");
            currentNodes.should.have.a.property("3");

            flow.stop().then(function() {
                currentNodes.should.have.a.property("1");
                currentNodes.should.not.have.a.property("2");
                currentNodes.should.not.have.a.property("3");
                stoppedNodes.should.not.have.a.property("1");
                stoppedNodes.should.have.a.property("2");
                stoppedNodes.should.have.a.property("3");
                setTimeout(function() {
                    currentNodes.should.not.have.a.property("1");
                    stoppedNodes.should.have.a.property("1");
                    done();
                },40)
            });
        });

    });

    describe("#handleStatus",function() {
        it("passes a status event to the adjacent status node",function(done) {
            var config = flowUtils.parseConfig([
                {id:"t1",type:"tab"},
                {id:"1",x:10,y:10,z:"t1",type:"test",name:"a",wires:["2"]},
                {id:"2",x:10,y:10,z:"t1",type:"test",wires:["3"]},
                {id:"3",x:10,y:10,z:"t1",type:"test",foo:"a",wires:[]},
                {id:"sn",x:10,y:10,z:"t1",type:"status",foo:"a",wires:[]},
                {id:"sn2",x:10,y:10,z:"t1",type:"status",foo:"a",wires:[]}
            ]);
            var flow = Flow.create(config,config.flows["t1"]);

            getNode.restore();
            getNode = sinon.stub(flows,"get",function(id) {
                return flow.getNode(id);
            });

            flow.start();

            var activeNodes = flow.getActiveNodes();
            Object.keys(activeNodes).should.have.length(5);


            flow.handleStatus(config.flows["t1"].nodes["1"],{text:"my-status"});

            currentNodes["sn"].should.have.a.property("handled",1);
            var statusMessage = currentNodes["sn"].messages[0];

            statusMessage.should.have.a.property("status");
            statusMessage.status.should.have.a.property("text","my-status");
            statusMessage.status.should.have.a.property("source");
            statusMessage.status.source.should.have.a.property("id","1");
            statusMessage.status.source.should.have.a.property("type","test");
            statusMessage.status.source.should.have.a.property("name","a");

            currentNodes["sn2"].should.have.a.property("handled",1);
            statusMessage = currentNodes["sn2"].messages[0];

            statusMessage.should.have.a.property("status");
            statusMessage.status.should.have.a.property("text","my-status");
            statusMessage.status.should.have.a.property("source");
            statusMessage.status.source.should.have.a.property("id","1");
            statusMessage.status.source.should.have.a.property("type","test");
            statusMessage.status.source.should.have.a.property("name","a");


            flow.stop().then(function() {
                done();
            });
        });
        it("passes a status event to the adjacent scoped status node ",function(done) {
            var config = flowUtils.parseConfig([
                {id:"t1",type:"tab"},
                {id:"1",x:10,y:10,z:"t1",type:"test",name:"a",wires:["2"]},
                {id:"2",x:10,y:10,z:"t1",type:"test",wires:["3"]},
                {id:"3",x:10,y:10,z:"t1",type:"test",foo:"a",wires:[]},
                {id:"sn",x:10,y:10,z:"t1",type:"status",scope:["2"],foo:"a",wires:[]},
                {id:"sn2",x:10,y:10,z:"t1",type:"status",scope:["1"],foo:"a",wires:[]}
            ]);
            var flow = Flow.create(config,config.flows["t1"]);

            getNode.restore();
            getNode = sinon.stub(flows,"get",function(id) {
                return flow.getNode(id);
            });

            flow.start();

            var activeNodes = flow.getActiveNodes();
            Object.keys(activeNodes).should.have.length(5);


            flow.handleStatus(config.flows["t1"].nodes["1"],{text:"my-status"});

            currentNodes["sn"].should.have.a.property("handled",0);
            currentNodes["sn2"].should.have.a.property("handled",1);
            var statusMessage = currentNodes["sn2"].messages[0];

            statusMessage.should.have.a.property("status");
            statusMessage.status.should.have.a.property("text","my-status");
            statusMessage.status.should.have.a.property("source");
            statusMessage.status.source.should.have.a.property("id","1");
            statusMessage.status.source.should.have.a.property("type","test");
            statusMessage.status.source.should.have.a.property("name","a");


            flow.stop().then(function() {
                done();
            });
        });

        it("passes a status event to the adjacent status node in subflow",function(done) {
            var config = flowUtils.parseConfig([
                {id:"t1",type:"tab"},
                {id:"1",x:10,y:10,z:"t1",type:"test",name:"a",wires:["2"]},
                {id:"2",x:10,y:10,z:"t1",type:"subflow:sf1",wires:["3"]},
                {id:"3",x:10,y:10,z:"t1",type:"test",foo:"a",wires:[]},
                {id:"sf1",type:"subflow","name":"Subflow 2","info":"",
                    "in":[{"wires":[{"id":"sf1-1"}]}],"out":[{"wires":[{"id":"sf1-1","port":0}]}]},
                {id:"sf1-1",type:"test2","z":"sf1",x:166,y:99,"wires":[[]]},
                {id:"sf1-sn",x:10,y:10,z:"sf1",type:"status",foo:"a",wires:[]}
            ]);
            var flow = Flow.create(config,config.flows["t1"]);

            getNode.restore();
            getNode = sinon.stub(flows,"get",function(id) {
                return flow.getNode(id);
            });

            flow.start();

            var activeNodes = flow.getActiveNodes();
            var sfInstanceId = Object.keys(activeNodes)[3];
            var statusInstanceId = Object.keys(activeNodes)[4];

            flow.handleStatus(activeNodes[sfInstanceId],{text:"my-status"});

            currentNodes[statusInstanceId].should.have.a.property("handled",1);
            var statusMessage = currentNodes[statusInstanceId].messages[0];

            statusMessage.should.have.a.property("status");
            statusMessage.status.should.have.a.property("text","my-status");
            statusMessage.status.should.have.a.property("source");
            statusMessage.status.source.should.have.a.property("id",sfInstanceId);
            statusMessage.status.source.should.have.a.property("type","test2");
            statusMessage.status.source.should.have.a.property("name",undefined);
            flow.stop().then(function() {

                done();
            });
        });
        it("passes a status event to the multiple adjacent status nodes in subflow",function(done) {
            var config = flowUtils.parseConfig([
                {id:"t1",type:"tab"},
                {id:"1",x:10,y:10,z:"t1",type:"test",name:"a",wires:["2"]},
                {id:"2",x:10,y:10,z:"t1",type:"subflow:sf1",wires:["3"]},
                {id:"3",x:10,y:10,z:"t1",type:"test",foo:"a",wires:[]},
                {id:"4",x:10,y:10,z:"t1",type:"status",foo:"a",wires:[]},
                {id:"sf1",type:"subflow","name":"Subflow 2","info":"",
                    "in":[{"wires":[{"id":"sf1-1"}]}],"out":[{"wires":[{"id":"sf1-1","port":0}]}]},
                {id:"sf1-1",type:"test2","z":"sf1",x:166,y:99,"wires":[[]]},
                {id:"sf1-sn",x:10,y:10,z:"sf1",type:"status",foo:"a",wires:[]},
                {id:"sf1-sn2",x:10,y:10,z:"sf1",type:"status",scope:["none"],wires:[]},
                {id:"sf1-sn3",x:10,y:10,z:"sf1",type:"status",scope:["sf1-1"],wires:[]}
            ]);
            var flow = Flow.create(config,config.flows["t1"]);

            getNode.restore();
            getNode = sinon.stub(flows,"get",function(id) {
                return flow.getNode(id);
            });

            flow.start();

            var activeNodes = flow.getActiveNodes();

            var sfInstanceId = Object.keys(activeNodes)[4];
            var statusInstanceId = Object.keys(activeNodes)[5];
            var statusInstanceId2 = Object.keys(activeNodes)[6];
            var statusInstanceId3 = Object.keys(activeNodes)[7];

            flow.handleStatus(activeNodes[sfInstanceId],{text:"my-status"});

            currentNodes[statusInstanceId].should.have.a.property("handled",1);
            var statusMessage = currentNodes[statusInstanceId].messages[0];

            statusMessage.should.have.a.property("status");
            statusMessage.status.should.have.a.property("text","my-status");
            statusMessage.status.should.have.a.property("source");
            statusMessage.status.source.should.have.a.property("id",sfInstanceId);
            statusMessage.status.source.should.have.a.property("type","test2");
            statusMessage.status.source.should.have.a.property("name",undefined);

            activeNodes["4"].should.have.a.property("handled",0);

            currentNodes[statusInstanceId2].should.have.a.property("handled",0);

            currentNodes[statusInstanceId3].should.have.a.property("handled",1);
            statusMessage = currentNodes[statusInstanceId3].messages[0];

            statusMessage.should.have.a.property("status");
            statusMessage.status.should.have.a.property("text","my-status");
            statusMessage.status.should.have.a.property("source");
            statusMessage.status.source.should.have.a.property("id",sfInstanceId);
            statusMessage.status.source.should.have.a.property("type","test2");
            statusMessage.status.source.should.have.a.property("name",undefined);

            flow.stop().then(function() {

                done();
            });
        });
        it("passes a status event to the subflow's parent tab status node",function(done) {
            var config = flowUtils.parseConfig([
                {id:"t1",type:"tab"},
                {id:"1",x:10,y:10,z:"t1",type:"test",name:"a",wires:["2"]},
                {id:"2",x:10,y:10,z:"t1",type:"subflow:sf1",wires:["3"]},
                {id:"3",x:10,y:10,z:"t1",type:"test",foo:"a",wires:[]},
                {id:"sf1",type:"subflow","name":"Subflow 2","info":"",
                    "in":[{"wires":[{"id":"sf1-1"}]}],"out":[{"wires":[{"id":"sf1-1","port":0}]}]},
                {id:"sf1-1",type:"test2","z":"sf1",x:166,y:99,"wires":[[]]},
                {id:"sn",x:10,y:10,z:"t1",type:"status",foo:"a",wires:[]}
            ]);
            var flow = Flow.create(config,config.flows["t1"]);

            getNode.restore();
            getNode = sinon.stub(flows,"get",function(id) {
                return flow.getNode(id);
            });

            flow.start();

            var activeNodes = flow.getActiveNodes();
            var sfInstanceId = Object.keys(activeNodes)[3];

            flow.handleStatus(activeNodes[sfInstanceId],{text:"my-status"});

            currentNodes["sn"].should.have.a.property("handled",1);
            var statusMessage = currentNodes["sn"].messages[0];

            statusMessage.should.have.a.property("status");
            statusMessage.status.should.have.a.property("text","my-status");
            statusMessage.status.should.have.a.property("source");
            statusMessage.status.source.should.have.a.property("id",sfInstanceId);
            statusMessage.status.source.should.have.a.property("type","test2");
            statusMessage.status.source.should.have.a.property("name",undefined);

            flow.stop().then(function() {

                done();
            });
        });
    });


    describe("#handleError",function() {
        it("passes an error event to the adjacent catch node",function(done) {
            var config = flowUtils.parseConfig([
                {id:"t1",type:"tab"},
                {id:"1",x:10,y:10,z:"t1",type:"test",name:"a",wires:["2"]},
                {id:"2",x:10,y:10,z:"t1",type:"test",wires:["3"]},
                {id:"3",x:10,y:10,z:"t1",type:"test",foo:"a",wires:[]},
                {id:"sn",x:10,y:10,z:"t1",type:"catch",foo:"a",wires:[]},
                {id:"sn2",x:10,y:10,z:"t1",type:"catch",foo:"a",wires:[]}
            ]);
            var flow = Flow.create(config,config.flows["t1"]);

            getNode.restore();
            getNode = sinon.stub(flows,"get",function(id) {
                return flow.getNode(id);
            });

            flow.start();

            var activeNodes = flow.getActiveNodes();
            Object.keys(activeNodes).should.have.length(5);


            flow.handleError(config.flows["t1"].nodes["1"],"my-error",{a:"foo"});

            currentNodes["sn"].should.have.a.property("handled",1);
            var statusMessage = currentNodes["sn"].messages[0];

            statusMessage.should.have.a.property("error");
            statusMessage.error.should.have.a.property("message","my-error");
            statusMessage.error.should.have.a.property("source");
            statusMessage.error.source.should.have.a.property("id","1");
            statusMessage.error.source.should.have.a.property("type","test");
            statusMessage.error.source.should.have.a.property("name","a");

            currentNodes["sn2"].should.have.a.property("handled",1);
            statusMessage = currentNodes["sn2"].messages[0];

            statusMessage.should.have.a.property("error");
            statusMessage.error.should.have.a.property("message","my-error");
            statusMessage.error.should.have.a.property("source");
            statusMessage.error.source.should.have.a.property("id","1");
            statusMessage.error.source.should.have.a.property("type","test");
            statusMessage.error.source.should.have.a.property("name","a");


            flow.stop().then(function() {
                done();
            });
        });
        it("passes an error event to the adjacent scoped catch node ",function(done) {
            var config = flowUtils.parseConfig([
                {id:"t1",type:"tab"},
                {id:"1",x:10,y:10,z:"t1",type:"test",name:"a",wires:["2"]},
                {id:"2",x:10,y:10,z:"t1",type:"test",wires:["3"]},
                {id:"3",x:10,y:10,z:"t1",type:"test",foo:"a",wires:[]},
                {id:"sn",x:10,y:10,z:"t1",type:"catch",scope:["2"],foo:"a",wires:[]},
                {id:"sn2",x:10,y:10,z:"t1",type:"catch",scope:["1"],foo:"a",wires:[]}
            ]);
            var flow = Flow.create(config,config.flows["t1"]);

            getNode.restore();
            getNode = sinon.stub(flows,"get",function(id) {
                return flow.getNode(id);
            });

            flow.start();

            var activeNodes = flow.getActiveNodes();
            Object.keys(activeNodes).should.have.length(5);

            flow.handleError(config.flows["t1"].nodes["1"],"my-error",{a:"foo"});

            currentNodes["sn"].should.have.a.property("handled",0);
            currentNodes["sn2"].should.have.a.property("handled",1);
            var statusMessage = currentNodes["sn2"].messages[0];

            statusMessage.should.have.a.property("error");
            statusMessage.error.should.have.a.property("message","my-error");
            statusMessage.error.should.have.a.property("source");
            statusMessage.error.source.should.have.a.property("id","1");
            statusMessage.error.source.should.have.a.property("type","test");
            statusMessage.error.source.should.have.a.property("name","a");


            flow.stop().then(function() {
                done();
            });
        });

        it("passes an error event to the adjacent catch node in subflow",function(done) {
            var config = flowUtils.parseConfig([
                {id:"t1",type:"tab"},
                {id:"1",x:10,y:10,z:"t1",type:"test",name:"a",wires:["2"]},
                {id:"2",x:10,y:10,z:"t1",type:"subflow:sf1",wires:["3"]},
                {id:"3",x:10,y:10,z:"t1",type:"test",foo:"a",wires:[]},
                {id:"sf1",type:"subflow","name":"Subflow 2","info":"",
                    "in":[{"wires":[{"id":"sf1-1"}]}],"out":[{"wires":[{"id":"sf1-1","port":0}]}]},
                {id:"sf1-1",type:"test2","z":"sf1",x:166,y:99,"wires":[[]]},
                {id:"sf1-sn",x:10,y:10,z:"sf1",type:"catch",foo:"a",wires:[]}
            ]);
            var flow = Flow.create(config,config.flows["t1"]);

            getNode.restore();
            getNode = sinon.stub(flows,"get",function(id) {
                return flow.getNode(id);
            });

            flow.start();

            var activeNodes = flow.getActiveNodes();
            var sfInstanceId = Object.keys(activeNodes)[3];
            var catchInstanceId = Object.keys(activeNodes)[4];

            flow.handleError(activeNodes[sfInstanceId],"my-error",{a:"foo"});

            currentNodes[catchInstanceId].should.have.a.property("handled",1);
            var statusMessage = currentNodes[catchInstanceId].messages[0];

            statusMessage.should.have.a.property("error");
            statusMessage.error.should.have.a.property("message","my-error");
            statusMessage.error.should.have.a.property("source");
            statusMessage.error.source.should.have.a.property("id",sfInstanceId);
            statusMessage.error.source.should.have.a.property("type","test2");
            statusMessage.error.source.should.have.a.property("name",undefined);

            flow.stop().then(function() {
                done();
            });
        });

        it("passes an error event to the multiple adjacent catch nodes in subflow",function(done) {
            var config = flowUtils.parseConfig([
                {id:"t1",type:"tab"},
                {id:"1",x:10,y:10,z:"t1",type:"test",name:"a",wires:["2"]},
                {id:"2",x:10,y:10,z:"t1",type:"subflow:sf1",wires:["3"]},
                {id:"3",x:10,y:10,z:"t1",type:"test",foo:"a",wires:[]},
                {id:"sf1",type:"subflow","name":"Subflow 2","info":"",
                    "in":[{"wires":[{"id":"sf1-1"}]}],"out":[{"wires":[{"id":"sf1-1","port":0}]}]},
                {id:"sf1-1",type:"test2","z":"sf1",x:166,y:99,"wires":[[]]},
                {id:"sf1-sn",x:10,y:10,z:"sf1",type:"catch",foo:"a",wires:[]},
                {id:"sf1-sn2",x:10,y:10,z:"sf1",type:"catch",scope:["none"],wires:[]},
                {id:"sf1-sn3",x:10,y:10,z:"sf1",type:"catch",scope:["sf1-1"],wires:[]}
            ]);
            var flow = Flow.create(config,config.flows["t1"]);

            getNode.restore();
            getNode = sinon.stub(flows,"get",function(id) {
                return flow.getNode(id);
            });

            flow.start();

            var activeNodes = flow.getActiveNodes();
            var sfInstanceId = Object.keys(activeNodes)[3];
            var catchInstanceId = Object.keys(activeNodes)[4];
            var catchInstanceId2 = Object.keys(activeNodes)[5];
            var catchInstanceId3 = Object.keys(activeNodes)[6];

            flow.handleError(activeNodes[sfInstanceId],"my-error",{a:"foo"});

            currentNodes[catchInstanceId].should.have.a.property("handled",1);
            var statusMessage = currentNodes[catchInstanceId].messages[0];

            statusMessage.should.have.a.property("error");
            statusMessage.error.should.have.a.property("message","my-error");
            statusMessage.error.should.have.a.property("source");
            statusMessage.error.source.should.have.a.property("id",sfInstanceId);
            statusMessage.error.source.should.have.a.property("type","test2");
            statusMessage.error.source.should.have.a.property("name",undefined);

            currentNodes[catchInstanceId2].should.have.a.property("handled",0);

            currentNodes[catchInstanceId3].should.have.a.property("handled",1);
            statusMessage = currentNodes[catchInstanceId3].messages[0];

            statusMessage.should.have.a.property("error");
            statusMessage.error.should.have.a.property("message","my-error");
            statusMessage.error.should.have.a.property("source");
            statusMessage.error.source.should.have.a.property("id",sfInstanceId);
            statusMessage.error.source.should.have.a.property("type","test2");
            statusMessage.error.source.should.have.a.property("name",undefined);

            flow.stop().then(function() {

                done();
            });
        });
        it("passes an error event to the subflow's parent tab catch node",function(done) {
            var config = flowUtils.parseConfig([
                {id:"t1",type:"tab"},
                {id:"1",x:10,y:10,z:"t1",type:"test",name:"a",wires:["2"]},
                {id:"2",x:10,y:10,z:"t1",type:"subflow:sf1",wires:["3"]},
                {id:"3",x:10,y:10,z:"t1",type:"test",foo:"a",wires:[]},
                {id:"sf1",type:"subflow","name":"Subflow 2","info":"",
                    "in":[{"wires":[{"id":"sf1-1"}]}],"out":[{"wires":[{"id":"sf1-1","port":0}]}]},
                {id:"sf1-1",type:"test2","z":"sf1",x:166,y:99,"wires":[[]]},
                {id:"sn",x:10,y:10,z:"t1",type:"catch",foo:"a",wires:[]}
            ]);
            var flow = Flow.create(config,config.flows["t1"]);

            getNode.restore();
            getNode = sinon.stub(flows,"get",function(id) {
                return flow.getNode(id);
            });

            flow.start();

            var activeNodes = flow.getActiveNodes();
            var sfInstanceId = Object.keys(activeNodes)[3];

            flow.handleError(activeNodes[sfInstanceId],"my-error",{a:"foo"});

            currentNodes["sn"].should.have.a.property("handled",1);
            var statusMessage = currentNodes["sn"].messages[0];

            statusMessage.should.have.a.property("error");
            statusMessage.error.should.have.a.property("message","my-error");
            statusMessage.error.should.have.a.property("source");
            statusMessage.error.source.should.have.a.property("id",sfInstanceId);
            statusMessage.error.source.should.have.a.property("type","test2");
            statusMessage.error.source.should.have.a.property("name",undefined);

            flow.stop().then(function() {
                done();
            });
        });
        it("moves any existing error object sideways",function(done){
            var config = flowUtils.parseConfig([
                {id:"t1",type:"tab"},
                {id:"1",x:10,y:10,z:"t1",type:"test",name:"a",wires:["2"]},
                {id:"2",x:10,y:10,z:"t1",type:"test",wires:["3"]},
                {id:"3",x:10,y:10,z:"t1",type:"test",foo:"a",wires:[]},
                {id:"sn",x:10,y:10,z:"t1",type:"catch",foo:"a",wires:[]}
            ]);
            var flow = Flow.create(config,config.flows["t1"]);

            getNode.restore();
            getNode = sinon.stub(flows,"get",function(id) {
                return flow.getNode(id);
            });

            flow.start();

            var activeNodes = flow.getActiveNodes();

            flow.handleError(config.flows["t1"].nodes["1"],"my-error",{a:"foo",error:"existing"});

            currentNodes["sn"].should.have.a.property("handled",1);
            var statusMessage = currentNodes["sn"].messages[0];

            statusMessage.should.have.a.property("_error","existing");
            statusMessage.should.have.a.property("error");
            statusMessage.error.should.have.a.property("message","my-error");
            statusMessage.error.should.have.a.property("source");
            statusMessage.error.source.should.have.a.property("id","1");
            statusMessage.error.source.should.have.a.property("type","test");
            statusMessage.error.source.should.have.a.property("name","a");

            flow.stop().then(function() {
                done();
            });
        });
        it.skip("prevents an error looping more than 10 times",function(){});
    });
});