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

var NR_TEST_UTILS = require("nr-test-utils");
var index = NR_TEST_UTILS.require("@node-red/runtime/lib/nodes/index");
var credentials = NR_TEST_UTILS.require("@node-red/runtime/lib/nodes/credentials");
var log = NR_TEST_UTILS.require("@node-red/util").log;


describe('red/runtime/nodes/credentials', function() {

    var encryptionDisabledSettings = {
        get: function(key) {
            return false;
        }
    }

    afterEach(function() {
        index.clearRegistry();
    });

    it('loads provided credentials',function() {
        credentials.init({
            log: log,
            settings: encryptionDisabledSettings
        });

        return credentials.load({"a":{"b":1,"c":2}}).then(function() {
            credentials.get("a").should.have.property('b',1);
            credentials.get("a").should.have.property('c',2);
        });
    });
    it('adds a new credential',function() {
        credentials.init({
            log: log,
            settings: encryptionDisabledSettings
        });
        return credentials.load({"a":{"b":1,"c":2}}).then(function() {
            credentials.dirty().should.be.false();
            should.not.exist(credentials.get("b"));
            return credentials.add("b",{"foo":"bar"}).then(function() {
                credentials.get("b").should.have.property("foo","bar");
                credentials.dirty().should.be.true();
            });
        });
    });
    it('deletes an existing credential',function() {
        credentials.init({
            log: log,
            settings: encryptionDisabledSettings
        });
        return credentials.load({"a":{"b":1,"c":2}}).then(function() {
            credentials.dirty().should.be.false();
            credentials.delete("a");
            should.not.exist(credentials.get("a"));
            credentials.dirty().should.be.true();
        });
    });

    it('exports the credentials, clearing dirty flag', function() {
        credentials.init({
            log: log,
            settings: encryptionDisabledSettings
        });
        var creds = {"a":{"b":1,"c":2}};
        return credentials.load(creds).then(function() {
            return credentials.add("b",{"foo":"bar"})
        }).then(function() {
            credentials.dirty().should.be.true();
            return credentials.export().then(function(exported) {
                exported.should.eql(creds);
                credentials.dirty().should.be.false();
            })
        });
    })

    describe("#clean",function() {
        it("removes credentials of unknown nodes",function() {
            credentials.init({
                log: log,
                settings: encryptionDisabledSettings,
                nodes: { getType: () => function(){} }
            });
            var creds = {"a":{"b":1,"c":2},"b":{"d":3}};
            return credentials.load(creds).then(function() {
                credentials.dirty().should.be.false();
                should.exist(credentials.get("a"));
                should.exist(credentials.get("b"));
                return credentials.clean([{id:"b"}]).then(function() {
                    credentials.dirty().should.be.true();
                    should.not.exist(credentials.get("a"));
                    should.exist(credentials.get("b"));
                });
            });
        });
        it("extracts credentials of known nodes",function() {
            credentials.init({
                log: log,
                settings: encryptionDisabledSettings,
                nodes: { getType: () => function(){} }
            });
            credentials.register("testNode",{"b":"text","c":"password"})
            var creds = {"a":{"b":1,"c":2}};
            var newConfig = [{id:"a",type:"testNode",credentials:{"b":"newBValue","c":"newCValue"}}];
            return credentials.load(creds).then(function() {
                credentials.dirty().should.be.false();
                return credentials.clean(newConfig).then(function() {
                    credentials.dirty().should.be.true();
                    credentials.get("a").should.have.property('b',"newBValue");
                    credentials.get("a").should.have.property('c',"newCValue");
                    should.not.exist(newConfig[0].credentials);
                });
            });
        });


    });

    it('warns if a node has no credential definition', function() {
        credentials.init({
            log: log,
            settings: encryptionDisabledSettings,
            nodes: { getType: () => function(){} }
        });
        return credentials.load({}).then(function() {
            var node = {id:"node",type:"test",credentials:{
                user1:"newUser",
                password1:"newPassword"
            }};
            sinon.spy(log,"warn");
            credentials.extract(node);
            log.warn.called.should.be.true();
            should.not.exist(node.credentials);
            log.warn.restore();
        });
    })

    it('extract credential updates in the provided node', function(done) {
        credentials.init({
            log: log,
            settings: encryptionDisabledSettings,
            nodes: { getType: () => function(){} }
        });
        var defintion = {
            user1:{type:"text"},
            password1:{type:"password"},
            user2:{type:"text"},
            password2:{type:"password"},
            user3:{type:"text"},
            password3:{type:"password"}

        };
        credentials.register("test",defintion);
        var def = credentials.getDefinition("test");
        defintion.should.eql(def);

        credentials.load({"node":{user1:"abc",password1:"123",user2:"def",password2:"456",user3:"ghi",password3:"789"}}).then(function() {
            var node = {id:"node",type:"test",credentials:{
                // user1 unchanged
                password1:"__PWRD__",
                user2: "",
                password2:"   ",
                user3:"newUser",
                password3:"newPassword"
            }};
            credentials.dirty().should.be.false();
            credentials.extract(node);

            node.should.not.have.a.property("credentials");

            credentials.dirty().should.be.true();
            var newCreds = credentials.get("node");
            newCreds.should.have.a.property("user1","abc");
            newCreds.should.have.a.property("password1","123");
            newCreds.should.not.have.a.property("user2");
            newCreds.should.not.have.a.property("password2");
            newCreds.should.have.a.property("user3","newUser");
            newCreds.should.have.a.property("password3","newPassword");

            done();
        });
    });
    it('extract ignores node without credentials', function(done) {
        credentials.init({
            log: log,
            settings: encryptionDisabledSettings,
            nodes: { getType: () => function(){} }
        });
        credentials.load({"node":{user1:"abc",password1:"123"}}).then(function() {
            var node = {id:"node",type:"test"};

            credentials.dirty().should.be.false();
            credentials.extract(node);
            credentials.dirty().should.be.false();
            done();
        });
    });

    describe("encryption",function() {
        var settings = {};
        var runtime = {
            log: log,
            settings: {
                get: function(key) {
                    return settings[key];
                },
                set: function(key,value) {
                    settings[key] = value;
                    return Promise.resolve();
                },
                delete: function(key) {
                    delete settings[key];
                    return Promise.resolve();
                }
            },
            nodes: { getType: () => function(){} }
        }
        it('migrates to encrypted and generates default key', function(done) {
            settings = {};
            credentials.init(runtime);
            credentials.load({"node":{user1:"abc",password1:"123"}}).then(function() {
                settings.should.have.a.property("_credentialSecret");
                settings._credentialSecret.should.have.a.length(64);
                credentials.dirty().should.be.true();
                credentials.export().then(function(result) {
                    result.should.have.a.property("$");
                    // reset everything - but with _credentialSecret still set
                    credentials.init(runtime);
                    // load the freshly encrypted version
                    credentials.load(result).then(function() {
                        should.exist(credentials.get("node"));
                        done();
                    })
                });
            });
        });
        it('uses default key', function(done) {
            settings = {
                _credentialSecret: "e3a36f47f005bf2aaa51ce3fc6fcaafd79da8d03f2b1a9281f8fb0a285e6255a"
            };
            // {"node":{user1:"abc",password1:"123"}}
            var cryptedFlows = {"$":"5b89d8209b5158a3c313675561b1a5b5phN1gDBe81Zv98KqS/hVDmc9EKvaKqRIvcyXYvBlFNzzzJtvN7qfw06i"};
            credentials.init(runtime);
            credentials.load(cryptedFlows).then(function() {
                should.exist(credentials.get("node"));
                credentials.dirty().should.be.false();
                credentials.add("node",{user1:"def",password1:"456"});
                credentials.export().then(function(result) {
                    result.should.have.a.property("$");
                    // reset everything - but with _credentialSecret still set
                    credentials.init(runtime);
                    // load the freshly encrypted version
                    credentials.load(result).then(function() {
                        should.exist(credentials.get("node"));
                        credentials.get("node").should.have.a.property("user1","def");
                        credentials.get("node").should.have.a.property("password1","456");
                        done();
                    })
                });
            });
        });
        it('uses user key', function(done) {
            settings = {
                credentialSecret: "e3a36f47f005bf2aaa51ce3fc6fcaafd79da8d03f2b1a9281f8fb0a285e6255a"
            };
            // {"node":{user1:"abc",password1:"123"}}
            var cryptedFlows = {"$":"5b89d8209b5158a3c313675561b1a5b5phN1gDBe81Zv98KqS/hVDmc9EKvaKqRIvcyXYvBlFNzzzJtvN7qfw06i"};
            credentials.init(runtime);
            credentials.load(cryptedFlows).then(function() {
                credentials.dirty().should.be.false();
                should.exist(credentials.get("node"));
                credentials.add("node",{user1:"def",password1:"456"});
                credentials.export().then(function(result) {
                    result.should.have.a.property("$");

                    // reset everything - but with _credentialSecret still set
                    credentials.init(runtime);
                    // load the freshly encrypted version
                    credentials.load(result).then(function() {
                        should.exist(credentials.get("node"));
                        credentials.get("node").should.have.a.property("user1","def");
                        credentials.get("node").should.have.a.property("password1","456");
                        done();
                    })
                });
            });
        });
        it('uses user key - when settings are otherwise unavailable', function(done) {
            var runtime = {
                log: log,
                settings: {
                    get: function(key) {
                        if (key === 'credentialSecret') {
                            return "e3a36f47f005bf2aaa51ce3fc6fcaafd79da8d03f2b1a9281f8fb0a285e6255a";
                        }
                        throw new Error();
                    },
                    set: function(key,value) {
                        throw new Error();
                    }
                }
            }
            // {"node":{user1:"abc",password1:"123"}}
            var cryptedFlows = {"$":"5b89d8209b5158a3c313675561b1a5b5phN1gDBe81Zv98KqS/hVDmc9EKvaKqRIvcyXYvBlFNzzzJtvN7qfw06i"};
            credentials.init(runtime);
            credentials.load(cryptedFlows).then(function() {
                should.exist(credentials.get("node"));
                credentials.add("node",{user1:"def",password1:"456"});
                credentials.export().then(function(result) {
                    result.should.have.a.property("$");

                    // reset everything - but with _credentialSecret still set
                    credentials.init(runtime);
                    // load the freshly encrypted version
                    credentials.load(result).then(function() {
                        should.exist(credentials.get("node"));
                        credentials.get("node").should.have.a.property("user1","def");
                        credentials.get("node").should.have.a.property("password1","456");
                        done();
                    })
                });
            });
        });
        it('migrates from default key to user key', function() {
            settings = {
                _credentialSecret: "e3a36f47f005bf2aaa51ce3fc6fcaafd79da8d03f2b1a9281f8fb0a285e6255a",
                credentialSecret:  "aaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbcccccccccccccddddddddddddeeeee"
            };
            // {"node":{user1:"abc",password1:"123"}}
            var cryptedFlows = {"$":"5b89d8209b5158a3c313675561b1a5b5phN1gDBe81Zv98KqS/hVDmc9EKvaKqRIvcyXYvBlFNzzzJtvN7qfw06i"};
            credentials.init(runtime);
            return credentials.load(cryptedFlows).then(function() {
                credentials.dirty().should.be.true();
                should.exist(credentials.get("node"));
                return credentials.export().then(function(result) {
                    result.should.have.a.property("$");
                    settings.should.not.have.a.property("_credentialSecret");

                    // reset everything - but with _credentialSecret still set
                    credentials.init(runtime);
                    // load the freshly encrypted version
                    return credentials.load(result).then(function() {
                        should.exist(credentials.get("node"));
                        credentials.get("node").should.have.a.property("user1","abc");
                        credentials.get("node").should.have.a.property("password1","123");
                    })
                });
            });
        });

        it('migrates from default key to user key - unencrypted original', function(done) {
            settings = {
                _credentialSecret: "e3a36f47f005bf2aaa51ce3fc6fcaafd79da8d03f2b1a9281f8fb0a285e6255a",
                credentialSecret:  "aaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbcccccccccccccddddddddddddeeeee"
            };
            // {"node":{user1:"abc",password1:"123"}}
            var unencryptedFlows = {"node":{user1:"abc",password1:"123"}};
            credentials.init(runtime);
            credentials.load(unencryptedFlows).then(function() {
                credentials.dirty().should.be.true();
                should.exist(credentials.get("node"));
                credentials.export().then(function(result) {
                    result.should.have.a.property("$");
                    settings.should.not.have.a.property("_credentialSecret");
                    // reset everything - but with _credentialSecret still set
                    credentials.init(runtime);
                    // load the freshly encrypted version
                    credentials.load(result).then(function() {
                        should.exist(credentials.get("node"));
                        credentials.get("node").should.have.a.property("user1","abc");
                        credentials.get("node").should.have.a.property("password1","123");
                        done();
                    })
                });
            });
        });

        it('migrates from default key to unencrypted', function(done) {
            settings = {
                _credentialSecret: "e3a36f47f005bf2aaa51ce3fc6fcaafd79da8d03f2b1a9281f8fb0a285e6255a",
                credentialSecret:  false
            };
            // {"node":{user1:"abc",password1:"123"}}
            var cryptedFlows = {"$":"5b89d8209b5158a3c313675561b1a5b5phN1gDBe81Zv98KqS/hVDmc9EKvaKqRIvcyXYvBlFNzzzJtvN7qfw06i"};
            credentials.init(runtime);
            credentials.load(cryptedFlows).then(function() {
                credentials.dirty().should.be.true();
                should.exist(credentials.get("node"));
                credentials.export().then(function(result) {
                    result.should.not.have.a.property("$");
                    settings.should.not.have.a.property("_credentialSecret");
                    result.should.eql({"node":{user1:"abc",password1:"123"}});
                    done();
                });
            });
        });
        it('handles bad default key - resets credentials', function(done) {
            settings = {
                _credentialSecret: "badbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadb"
            };
            // {"node":{user1:"abc",password1:"123"}}
            var cryptedFlows = {"$":"5b89d8209b5158a3c313675561b1a5b5phN1gDBe81Zv98KqS/hVDmc9EKvaKqRIvcyXYvBlFNzzzJtvN7qfw06i"};
            credentials.init(runtime);
            credentials.load(cryptedFlows).then(function() {
                // credentials.dirty().should.be.true();
                // should.not.exist(credentials.get("node"));
                done();
            }).catch(function(err) {
                err.should.have.property('code','credentials_load_failed');
                done();
            });
        });
        it('handles bad user key - resets credentials', function(done) {
            settings = {
                credentialSecret: "badbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadb"
            };
            // {"node":{user1:"abc",password1:"123"}}
            var cryptedFlows = {"$":"5b89d8209b5158a3c313675561b1a5b5phN1gDBe81Zv98KqS/hVDmc9EKvaKqRIvcyXYvBlFNzzzJtvN7qfw06i"};
            credentials.init(runtime);
            credentials.load(cryptedFlows).then(function() {
                // credentials.dirty().should.be.true();
                // should.not.exist(credentials.get("node"));
                done();
            }).catch(function(err) {
                err.should.have.property('code','credentials_load_failed');
                done();
            });
        });

        it('handles bad credentials object - resets credentials', function(done) {
            settings = {
                credentialSecret: "e3a36f47f005bf2aaa51ce3fc6fcaafd79da8d03f2b1a9281f8fb0a285e6255a"
            };
            // {"node":{user1:"abc",password1:"123"}}
            var cryptedFlows = {"BADKEY":"5b89d8209b5158a3c313675561b1a5b5phN1gDBe81Zv98KqS/hVDmc9EKvaKqRIvcyXYvBlFNzzzJtvN7qfw06i"};
            credentials.init(runtime);
            credentials.load(cryptedFlows).then(function() {
                done();
            }).catch(function(err) {
                err.should.have.property('code','credentials_load_failed');
                done();
            });
        });

        it('handles unavailable settings - leaves creds unencrypted', function(done) {
            var runtime = {
                log: log,
                settings: {
                    get: function(key) {
                        throw new Error();
                    },
                    set: function(key,value) {
                        throw new Error();
                    }
                },
                nodes: { getType: () => function(){} }
            }
            // {"node":{user1:"abc",password1:"123"}}
            credentials.init(runtime);
            credentials.load({"node":{user1:"abc",password1:"123"}}).then(function() {
                credentials.dirty().should.be.false();
                should.exist(credentials.get("node"));
                credentials.export().then(function(result) {
                    result.should.not.have.a.property("$");
                    result.should.have.a.property("node");
                    done();
                });
            });
        });
    })
})
