/**
 * 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 NR_TEST_UTILS = require("nr-test-utils");
var flows = NR_TEST_UTILS.require("@node-red/runtime/lib/api/flows")

var mockLog = () => ({
    log: sinon.stub(),
    debug: sinon.stub(),
    trace: sinon.stub(),
    warn: sinon.stub(),
    info: sinon.stub(),
    metric: sinon.stub(),
    audit: sinon.stub(),
    _: function() { return "abc"}
})

describe("runtime-api/flows", function() {
    describe("getFlows", function() {
        it("returns the current flow configuration", function(done) {
            flows.init({
                log: mockLog(),
                flows: {
                    getFlows: function() { return [1,2,3] }
                }
            });
            flows.getFlows({}).then(function(result) {
                result.should.eql([1,2,3]);
                done();
            }).catch(done);
        });
    });

    describe("setFlows", function() {
        var setFlows;
        var loadFlows;
        var reloadError = false;
        beforeEach(function() {
            setFlows = sinon.spy(function(flows,credentials,type) {
                if (flows[0] === "error") {
                    var err = new Error("error");
                    err.code = "error";
                    var p = Promise.reject(err);
                    p.catch(()=>{});
                    return p;
                }
                return Promise.resolve("newRev");
            });
            loadFlows = sinon.spy(function() {
                if (!reloadError) {
                    return Promise.resolve("newLoadRev");
                } else {
                    var err = new Error("error");
                    err.code = "error";
                    var p = Promise.reject(err);
                    p.catch(()=>{});
                    return p;
                }
            })
            flows.init({
                log: mockLog(),
                flows: {
                    getFlows: function() { return {rev:"currentRev",flows:[]} },
                    setFlows: setFlows,
                    loadFlows: loadFlows
                }
            })

        })
        it("defaults to full deploy", function(done) {
            flows.setFlows({
                flows: {flows:[4,5,6]}
            }).then(function(result) {
                result.should.eql({rev:"newRev"});
                setFlows.called.should.be.true();
                setFlows.lastCall.args[0].should.eql([4,5,6]);
                setFlows.lastCall.args[2].should.eql("full");
                done();
            }).catch(done);
        });
        it("includes credentials when part of the request", function(done) {
            flows.setFlows({
                flows: {flows:[4,5,6], credentials: {$:"creds"}},
            }).then(function(result) {
                result.should.eql({rev:"newRev"});
                setFlows.called.should.be.true();
                setFlows.lastCall.args[0].should.eql([4,5,6]);
                setFlows.lastCall.args[1].should.eql({$:"creds"});
                setFlows.lastCall.args[2].should.eql("full");
                done();
            }).catch(done);
        });
        it("passes through other deploy types", function(done) {
            flows.setFlows({
                deploymentType: "nodes",
                flows: {flows:[4,5,6]}
            }).then(function(result) {
                result.should.eql({rev:"newRev"});
                setFlows.called.should.be.true();
                setFlows.lastCall.args[0].should.eql([4,5,6]);
                setFlows.lastCall.args[2].should.eql("nodes");
                done();
            }).catch(done);
        });
        it("triggers a flow reload", function(done) {
            flows.setFlows({
                deploymentType: "reload"
            }).then(function(result) {
                result.should.eql({rev:"newLoadRev"});
                setFlows.called.should.be.false();
                loadFlows.called.should.be.true();
                done();
            }).catch(done);
        });
        it("allows update when revision matches", function(done) {
            flows.setFlows({
                deploymentType: "nodes",
                flows: {flows:[4,5,6],rev:"currentRev"}
            }).then(function(result) {
                result.should.eql({rev:"newRev"});
                setFlows.called.should.be.true();
                setFlows.lastCall.args[0].should.eql([4,5,6]);
                setFlows.lastCall.args[2].should.eql("nodes");
                done();
            }).catch(done);
        });
        it("rejects update when revision does not match", function(done) {
            flows.setFlows({
                deploymentType: "nodes",
                flows: {flows:[4,5,6],rev:"notTheCurrentRev"}
            }).then(function(result) {
                done(new Error("Did not reject rev mismatch"));
            }).catch(function(err) {
                err.should.have.property('code','version_mismatch');
                err.should.have.property('status',409);
                done();
            }).catch(done);
        });
        it("rejects when reload fails",function(done) {
            reloadError = true;
            flows.setFlows({
                deploymentType: "reload"
            }).then(function(result) {
                done(new Error("Did not return internal error"));
            }).catch(function(err) {
                err.should.have.property('code','error');
                done();
            }).catch(done);
        });
        it("rejects when update fails",function(done) {
            flows.setFlows({
                deploymentType: "full",
                flows: {flows:["error",5,6]}
            }).then(function(result) {
                done(new Error("Did not return internal error"));
            }).catch(function(err) {
                err.should.have.property('code','error');
                done();
            }).catch(done);
        });
    });

    describe("addFlow", function() {
        var addFlow;
        beforeEach(function() {
            addFlow = sinon.spy(function(flow) {
                if (flow === "error") {
                    var err = new Error("error");
                    err.code = "error";
                    var p = Promise.reject(err);
                    p.catch(()=>{});
                    return p;
                }
                return Promise.resolve("newId");
            });
            flows.init({
                log: mockLog(),
                flows: {
                    addFlow: addFlow
                }
            });
        })
        it("adds a flow", function(done) {
            flows.addFlow({flow:{a:"123"}}).then(function(id) {
                addFlow.called.should.be.true();
                addFlow.lastCall.args[0].should.eql({a:"123"});
                id.should.eql("newId");
                done()
            }).catch(done);
        });
        it("rejects when add fails", function(done) {
            flows.addFlow({flow:"error"}).then(function(id) {
                done(new Error("Did not return internal error"));
            }).catch(function(err) {
                err.should.have.property('code','error');
                done();
            }).catch(done);
        });
    });
    describe("getFlow", function() {
        var getFlow;
        beforeEach(function() {
            getFlow = sinon.spy(function(flow) {
                if (flow === "unknown") {
                    return null;
                }
                return [1,2,3];
            });
            flows.init({
                log: mockLog(),
                flows: {
                    getFlow: getFlow
                }
            });
        })
        it("gets a flow", function(done) {
            flows.getFlow({id:"123"}).then(function(flow) {
                flow.should.eql([1,2,3]);
                done()
            }).catch(done);
        });
        it("rejects when flow not found", function(done) {
            flows.getFlow({id:"unknown"}).then(function(flow) {
                done(new Error("Did not return internal error"));
            }).catch(function(err) {
                err.should.have.property('code','not_found');
                err.should.have.property('status',404);
                done();
            }).catch(done);
        });
    });

    describe("updateFlow", function() {
        var updateFlow;
        beforeEach(function() {
            updateFlow = sinon.spy(function(id,flow) {
                if (id === "unknown") {
                    var err = new Error();
                    // TODO: quirk of internal api - uses .code for .status
                    err.code = 404;
                    var p = Promise.reject(err);
                    p.catch(()=>{});
                    return p;
                } else if (id === "error") {
                    var err = new Error();
                    // TODO: quirk of internal api - uses .code for .status
                    err.code = "error";
                    var p = Promise.reject(err);
                    p.catch(()=>{});
                    return p;
                }
                return Promise.resolve();
            });
            flows.init({
                log: mockLog(),
                flows: {
                    updateFlow: updateFlow
                }
            });
        })
        it("updates a flow", function(done) {
            flows.updateFlow({id:"123",flow:[1,2,3]}).then(function(id) {
                id.should.eql("123");
                updateFlow.called.should.be.true();
                updateFlow.lastCall.args[0].should.eql("123");
                updateFlow.lastCall.args[1].should.eql([1,2,3]);
                done()
            }).catch(done);
        });
        it("rejects when flow not found", function(done) {
            flows.updateFlow({id:"unknown"}).then(function(flow) {
                done(new Error("Did not return internal error"));
            }).catch(function(err) {
                err.should.have.property('code','not_found');
                err.should.have.property('status',404);
                done();
            }).catch(done);
        });
        it("rejects when update fails", function(done) {
            flows.updateFlow({id:"error"}).then(function(flow) {
                done(new Error("Did not return internal error"));
            }).catch(function(err) {
                err.should.have.property('code','error');
                err.should.have.property('status',400);
                done();
            }).catch(done);
        });
    });


    describe("deleteFlow", function() {
        var removeFlow;
        beforeEach(function() {
            removeFlow = sinon.spy(function(flow) {
                if (flow === "unknown") {
                    var err = new Error();
                    // TODO: quirk of internal api - uses .code for .status
                    err.code = 404;
                    var p = Promise.reject(err);
                    p.catch(()=>{});
                    return p;
                } else if (flow === "error") {
                    var err = new Error();
                    // TODO: quirk of internal api - uses .code for .status
                    err.code = "error";
                    var p = Promise.reject(err);
                    p.catch(()=>{});
                    return p;
                }
                return Promise.resolve();
            });
            flows.init({
                log: mockLog(),
                flows: {
                    removeFlow: removeFlow
                }
            });
        })
        it("deletes a flow", function(done) {
            flows.deleteFlow({id:"123"}).then(function() {
                removeFlow.called.should.be.true();
                removeFlow.lastCall.args[0].should.eql("123");
                done()
            }).catch(done);
        });
        it("rejects when flow not found", function(done) {
            flows.deleteFlow({id:"unknown"}).then(function(flow) {
                done(new Error("Did not return internal error"));
            }).catch(function(err) {
                err.should.have.property('code','not_found');
                err.should.have.property('status',404);
                done();
            }).catch(done);
        });
        it("rejects when delete fails", function(done) {
            flows.deleteFlow({id:"error"}).then(function(flow) {
                done(new Error("Did not return internal error"));
            }).catch(function(err) {
                err.should.have.property('code','error');
                err.should.have.property('status',400);
                done();
            }).catch(done);
        });
    });

    describe("getNodeCredentials", function() {
        beforeEach(function() {
            flows.init({
                log: mockLog(),
                nodes: {
                    getCredentials: function(id) {
                        if (id === "unknown") {
                            return undefined;
                        } else if (id === "known") {
                            return {
                                username: "abc",
                                password: "123"
                            }
                        } else if (id === "known2") {
                            return {
                                username: "abc",
                                password: ""
                            }
                        } else {
                            return {};
                        }
                    },
                    getCredentialDefinition: function(type) {
                        if (type === "node") {
                            return {
                                username: {type:"text"},
                                password: {type:"password"}
                            }
                        } else {
                            return null;
                        }
                    }
                }
            });
        })
        it("returns an empty object for an unknown node", function(done) {
            flows.getNodeCredentials({id:"unknown", type:"node"}).then(function(result) {
                result.should.eql({});
                done();
            }).catch(done);
        });
        it("gets the filtered credentials for a known node with password", function(done) {
            flows.getNodeCredentials({id:"known", type:"node"}).then(function(result) {
                result.should.eql({
                    username: "abc",
                    has_password: true
                });
                done();
            }).catch(done);
        });
        it("gets the filtered credentials for a known node without password", function(done) {
            flows.getNodeCredentials({id:"known2", type:"node"}).then(function(result) {
                result.should.eql({
                    username: "abc",
                    has_password: false
                });
                done();
            }).catch(done);
        });
        it("gets the empty credentials for a known node without a registered definition", function(done) {
            flows.getNodeCredentials({id:"known2", type:"unknown-type"}).then(function(result) {
                result.should.eql({});
                done();
            }).catch(done);
        });
    });

    describe("flow run state", function() {
        var startFlows, stopFlows, runtime;
        beforeEach(function() {
            let flowsStarted = true;
            let flowsState = "start";
            startFlows = sinon.spy(function(type) {
                if (type !== "full") {
                    var err = new Error();
                    // TODO: quirk of internal api - uses .code for .status
                    err.code = 400;
                    var p = Promise.reject(err);
                    p.catch(()=>{});
                    return p;
                }
                flowsStarted = true;
                flowsState = "start";
                return Promise.resolve();
            });
            stopFlows = sinon.spy(function(type) {
                if (type !== "full") {
                    var err = new Error();
                    // TODO: quirk of internal api - uses .code for .status
                    err.code = 400;
                    var p = Promise.reject(err);
                    p.catch(()=>{});
                    return p;
                }
                flowsStarted = false;
                flowsState = "stop";
                return Promise.resolve();
            });
            runtime = {
                log: mockLog(),
                settings: {
                    runtimeState: {
                        enabled: true,
                        ui: true,
                    },
                },
                flows: {
                    get started() {
                        return flowsStarted;
                    },
                    startFlows,
                    stopFlows,
                    getFlows: function() { return {rev:"currentRev",flows:[]} },
                    state: function() { return flowsState}
                }
            }
        })

        it("gets flows run state", async function() {
            flows.init(runtime);
            const state = await flows.getState({})
            state.should.have.property("state", "start")
        });
        it("permits getting flows run state when setting disabled", async function() {
            runtime.settings.runtimeState.enabled = false;
            flows.init(runtime);
            const state = await flows.getState({})
            state.should.have.property("state", "start")
        });
        it("start flows", async function() {
            flows.init(runtime);
            const state = await flows.setState({state:"start"})
            state.should.have.property("state", "start")
            stopFlows.called.should.not.be.true();
            startFlows.called.should.be.true();
        });
        it("stop flows", async function() {
            flows.init(runtime);
            const state = await flows.setState({state:"stop"})
            state.should.have.property("state", "stop")
            stopFlows.called.should.be.true();
            startFlows.called.should.not.be.true();
        });
        it("rejects starting flows when setting disabled", async function() {
            let err;
            runtime.settings.runtimeState.enabled = false;
            flows.init(runtime);
            try {
                await flows.setState({state:"start"})
            } catch (error) {
                err = error
            }
            stopFlows.called.should.not.be.true();
            startFlows.called.should.not.be.true();
            should(err).have.property("code", "not_allowed")
            should(err).have.property("status", 405)
        });
        it("rejects stopping flows when setting disabled", async function() {
            let err;
            runtime.settings.runtimeState.enabled = false;
            flows.init(runtime);
            try {
                await flows.setState({state:"stop"})
            } catch (error) {
                err = error
            }
            stopFlows.called.should.not.be.true();
            startFlows.called.should.not.be.true();
            should(err).have.property("code", "not_allowed")
            should(err).have.property("status", 405)
        });
        it("rejects setting invalid flows run state", async function() {
            let err;
            flows.init(runtime);
            try {
                await flows.setState({state:"bad-state"})
            } catch (error) {
                err = error
            }
            stopFlows.called.should.not.be.true();
            startFlows.called.should.not.be.true();
            should(err).have.property("code", "invalid_run_state")
            should(err).have.property("status", 400)
        });
    });

});
