import { FormValidation } from '../../transforms';
import {
    createSyncChecker,
    formValidateAsPromise } from '../../transforms/form-validation';
import { Model, ModelListener } from 'front';
import lolex from 'lolex';

describe('FormValidation', function() {
    it('should abort if not instance of ModelListener', function() {
        const ml = {};

        expect(function() {
            FormValidation(ml);
        }).to.throw(Error);
    });

    it('should abort if no checker function defined', function() {
        const ml = new ModelListener({
            model: new Model()
        });

        expect(function() {
            FormValidation(ml);
        }).to.throw(Error);
    });

    it('should abort if there is already errors', function() {
        const ml = new ModelListener({
            model: new Model()
        });

        ml.model.errors = {};

        expect(function() {
            FormValidation(ml, function() {});
        }).to.throw(Error);
    });

    it('should call cb on formValidate', function(done) {
        const ml = new ModelListener({
            model: new Model()
        });

        function checker(key, cb) {
            cb(null);
        }

        FormValidation(ml, checker);

        ml.formValidate(function(valid) {
            expect(valid).to.be.true;
            done();
        });
    });

    it('should populate model.errors', function(done) {
        const ml = new ModelListener({
            model: new Model()
        });

        function checker(key, cb) {
            cb({
                fieldname: 'error message'
            });
        }

        FormValidation(ml, checker);

        ml.formValidate(function() {
            expect(ml.model.errors.fieldname).to.equal('error message');
            done();
        });
    });

    it('should remove an error from model.errors on update', function() {
        const ml = new ModelListener({
                model: new Model({
                    fieldname: ''
                })
            }),
            clock = lolex.install();

        function checker(key, cb) {
            cb(null);
        }

        FormValidation(ml, checker);

        ml.model.errors.fieldname = 'error message';
        ml.update('fieldname', 'something valid');

        clock.tick(15);
        expect(ml.model.errors.fieldname).to.not.exist;
        clock.uninstall();
    });

    it('should change error on model.errors on update', function() {
        const ml = new ModelListener({
                model: new Model({
                    fieldname: ''
                })
            }),
            clock = lolex.install();

        function checker(key, cb) {
            if (key === 'fieldname') {
                return cb('new error message');
            }

            cb(null);
        }

        FormValidation(ml, checker);

        ml.model.errors.fieldname = 'original error message';
        ml.update('fieldname', 'something invalid');

        clock.tick(15);
        expect(ml.model.errors.fieldname).to.equal('new error message');
        clock.uninstall();
    });

    it('should not change error if field value did not change', function() {
        const value = 'stays the same',
            ml = new ModelListener({
                model: new Model({
                    fieldname: value
                })
            }),
            clock = lolex.install();

        function checker(key, cb) {
            if (key === 'fieldname') {
                return cb('new error message');
            }

            cb(null);
        }

        FormValidation(ml, checker);

        ml.model.errors.fieldname = 'original error message';
        ml.update('fieldname', value);

        clock.tick(15);
        expect(ml.model.errors.fieldname).to.equal('original error message');
        clock.uninstall();
    });

    it('should not generate an error through update', function() {
        const ml = new ModelListener({
            model: new Model()
        });

        function checker(key, cb) {
            if (key === 'fieldname') {
                return cb('new error message');
            }

            cb(null);
        }

        FormValidation(ml, checker);

        ml.model.errors.fieldname = null;
        ml.update('fieldname', 'something invalid');

        expect(ml.model.errors.fieldname).to.equal(null);
    });

    it('should let you replace the checker', function(done) {
        const ml = new ModelListener({
                model: new Model()
            }),
            spy = sinon.spy(),
            newSpy = sinon.spy();

        function checker(key, cb) {
            spy();
            cb(null);
        }

        function newChecker(key, cb) {
            newSpy();
            cb(null);
        }

        FormValidation(ml, checker);

        formValidateAsPromise(ml)
            .then(() => {
                expect(spy.called).to.be.true;
                ml.formValidate.settings.setChecker(newChecker);
                return formValidateAsPromise(ml);
            })
            .then(() => {
                expect(spy.callCount).to.equal(1);
                expect(newSpy.called).to.be.true;
                done();
            });
    });

    it('should not replace checker with an invalid one', function(done) {
        const ml = new ModelListener({
                model: new Model()
            }),
            spy = sinon.spy();

        function checker(key, cb) {
            spy();
            cb(null);
        }

        FormValidation(ml, checker);

        formValidateAsPromise(ml)
            .then(() => {
                expect(spy.called).to.be.true;
                ml.formValidate.settings.setChecker(null);
                return formValidateAsPromise(ml);
            })
            .then(() => {
                expect(spy.callCount).to.equal(2);
                done();
            });
    });

    it('should also call formValidate on children', function(done) {
        const parent = new ModelListener({ model: new Model() }),
            child = new ModelListener({ model: new Model() }),
            parentSpy = sinon.spy(),
            childSpy = sinon.spy();

        function checkerFromSpy(spy) {
            return function(key, cb) {
                spy();
                cb(null);
            };
        }

        FormValidation(child, checkerFromSpy(childSpy));
        FormValidation(parent, checkerFromSpy(parentSpy), child);

        parent.formValidate.settings.addChildren(child);
        parent.formValidate(() => {
            expect(parentSpy.called).to.be.true;
            expect(childSpy.called).to.be.true;
            done();
        });
    });

    it('should allow adding/removing children', function(done) {
        const parent = new ModelListener({ model: new Model() }),
            child = new ModelListener({ model: new Model() }),
            neighbor = new ModelListener({ model: new Model() }),
            parentSpy = sinon.spy(),
            childSpy = sinon.spy();

        function checkerFromSpy(spy) {
            return function(key, cb) {
                spy();
                cb(null);
            };
        }

        FormValidation(neighbor, checkerFromSpy(sinon.spy()));
        FormValidation(parent, checkerFromSpy(parentSpy));
        FormValidation(child, checkerFromSpy(childSpy));

        formValidateAsPromise(parent)
            .then(() => {
                // verify call count - only parent called
                expect(parentSpy.callCount).to.equal(1);
                expect(childSpy.callCount).to.equal(0);

                // add child
                parent.formValidate.settings.addChildren(child);
                return formValidateAsPromise(parent);
            })
            .then(() => {
                // verify call count - both called
                expect(parentSpy.callCount).to.equal(2);
                expect(childSpy.callCount).to.equal(1);

                // remove child, removing neighbor does nothing
                parent.formValidate.settings.removeChildren(child, neighbor);
                return formValidateAsPromise(parent);
            })
            .then(() => {
                // verify call count - only parent called again
                expect(parentSpy.callCount).to.equal(3);
                expect(childSpy.callCount).to.equal(1);
                done();
            });
    });

    // Check that children that have not gone through transform or are not
    // model listeners simply get ignored instead of creating errors
    it('should work even if I pass it nonsensical things', function(done) {
        const parent = new ModelListener({ model: new Model() }),
            child = new ModelListener({ model: new Model() }),
            sis = new ModelListener({ model: new Model() }), // not transformed
            bro = {}, // not a model listener
            parentSpy = sinon.spy(),
            childSpy = sinon.spy();

        function checkerFromSpy(spy) {
            return function(key, cb) {
                spy();
                cb(null);
            };
        }

        FormValidation(child, checkerFromSpy(childSpy));
        FormValidation(parent, checkerFromSpy(parentSpy), child, sis, bro);

        // non sense inputs for branch coverage
        parent.formValidate.settings.addChildren({});
        parent.formValidate.settings.addChildren([ 'cat', 'dog', 'snake' ]);
        parent.formValidate.settings.removeChildren({});
        parent.formValidate.settings.removeChildren(2);
        parent.formValidate.settings.removeChildren(new ModelListener({
            model: new Model()
        }));

        parent.formValidate(() => {
            expect(parentSpy.called).to.be.true;
            expect(childSpy.called).to.be.true;
            done();
        });
    });

    it('should allow error reporting', function(done) {
        const ml = new ModelListener({
            model: new Model({
                fieldname: 'value'
            })
        });

        function makeChecker() {
            let called = false;

            return function checker(key, cb) {
                if (called) {
                    cb(null);
                } else {
                    cb({ fieldname: 'error message' });
                    called = true;
                }
            };
        }

        FormValidation(ml, makeChecker());

        formValidateAsPromise(ml)
            .then(() => {
                expect(ml.model.errors.fieldname).to.equal('error message');
                return formValidateAsPromise(ml, true);
            })
            .then(() => {
                expect(ml.model.errors.fieldname).to.equal('error message');
                return formValidateAsPromise(ml);
            })
            .then(() => {
                expect(ml.model.errors).to.be.null;
                done();
            });
    });
});

describe('createSyncChecker', function() {
    it('should not work if not given a model listener instance', function() {
        expect(createSyncChecker({}, function() {})).to.not.be.ok;
    });

    it('should not work if field validator is not a function', function() {
        const ml = new ModelListener({
            model: new Model()
        });

        expect(createSyncChecker(ml, null)).to.not.be.ok;
    });

    it('should return a function if given the right params', function() {
        const ml = new ModelListener({
                model: new Model()
            }),
            fn = function() {};

        expect(typeof createSyncChecker(ml, fn)).to.equal('function');
    });

    it('should return a function that works properly', function() {
        const ml = new ModelListener({
                model: new Model({
                    a: '',
                    b: ''
                })
            }),
            fn = function() { return 'error msg'; },
            checker = createSyncChecker(ml, fn);

        expect(typeof checker).to.equal('function');

        checker('b'); // verifying that no errors thrown
        checker('a', (msg) => expect(msg).to.equal('error msg'));
        checker('', (errors) => {
            expect(errors).to.have.property('a', 'error msg');
            expect(errors).to.have.property('b', 'error msg');
        });
    });
});
