var chai = require('chai');
chai.use(require('chai-shallow-deep-equal'));
var assert = chai.assert;
var Joi = require('joi');

var ValidationError = require('../lib/validationError');
var validate = require('../lib/index');

describe('Validation Utils', function() {

    describe('Primitive types', function() {
        
        it('can validate and convert number', function() {
            // Valid:
            var vResult = validate('123', Joi.number().integer().required());
            assert.strictEqual(vResult, 123);

            // Invalid:
            var e = assert.throws(function() {
                validate('abc', Joi.number().integer().required());
            });
            var errors = e.message;
            assert.equal(1, errors.length);
            assert.equal(errors[0].message, '\"value\" must be a number');
        });

        it('can validate string', function() {
            // Valid:
            var vResult = validate('abc', Joi.string().max(10).required());
            assert.strictEqual(vResult, 'abc');

            // Invalid:
            var e = assert.throws(function() {
                validate('abcd', Joi.string().max(3).required());
            });
            var errors = e.message;
            assert.equal(1, errors.length);
            assert.equal(errors[0].message, '"value" length must be less than or equal to 3 characters long');
        });

        it('can validate optional string when value is null', function() {
            var vResult = validate(null, Joi.string().optional().allow(null));
            assert.isNull(vResult);
        });

        it('can validate optional string when value is undefined', function() {
            var undefined;
            var vResult = validate(undefined, Joi.string().optional().allow(null));
            assert.isUndefined(vResult);
        });

        it('can validate date', function() {
            var date = new Date();

            // Valid:
            var vResult = validate(date, Joi.date().required());
            assert.strictEqual(vResult, date);

            // Invalid:
            var e = assert.throws(function() {
                validate('abcd', Joi.date().required());
            });
            var errors = e.message;
            assert.equal(1, errors.length);
            assert.equal(errors[0].message, '"value" must be a number of milliseconds or valid date string');
        });

        it('can validate non-null array', function() {
            var schema = Joi.array().min(3).items(Joi.number().integer()).required();

            // Valid:
            var vResult = validate([1,2,3], schema);
            assert.deepEqual(vResult, [1,2,3]);

            // Invalid:
            var e = assert.throws(function() {
                validate([1], schema);
            });
            var errors = e.message;
            assert.equal(1, errors.length);
            assert.equal(errors[0].message, '"value" must contain at least 3 items');
        });

        it('can validate null array', function() {
            var schema = Joi.array().required();

            // Valid:
            var vResult = validate([1,2,3], schema);
            assert.deepEqual(vResult, [1,2,3]);

            // Invalid:
            var e = assert.throws(function() {
                validate(null, schema);
            });
            var errors = e.message;
            assert.equal(1, errors.length);
            assert.equal(errors[0].message, '"value" must be an array');
        });
    });

    it('can use static schema to validate data', function() {
        var sourceARawInput = {
            foo: 1,
            bar: 2
        };

        var schema = Joi.object().keys({ bar: Joi.number().integer().required() });

        var sanitizedInput = validate(sourceARawInput, schema);

        assert.deepEqual(sanitizedInput, {
            bar: 2
        });
    });

    it('can use a custom function to validate data', function() {
        var sourceARawInput = {
            foo: '1',
            bar: 'true'
        };

        var schema = Joi.object().keys({ bar: Joi.boolean() });

        var validationFn = function(rawInput, validData) {
            var rawValue = rawInput.foo;
            if (validData.bar === true) {
                try {
                    return {
                        valid: true,
                        value: {
                            foo: parseInt(rawValue)
                        }
                    };
                } catch (e) {
                    return {
                        valid: false,
                        message: '"foo" must be 1 when "bar" is true',
                        value: {
                            foo: rawValue
                        }
                    };
                }
            }
        };

        var sanitizedInput = validate(sourceARawInput, schema, validationFn);

        assert.deepEqual(sanitizedInput, {
            foo: 1,
            bar: true
        });
    });

    describe('Using custom function for dynamic validation', function() {

        it('can perform dynamic Joi validation', function() {
            var sourceARawInput = {
                a: '1',
                b: '10'
            };

            var schema = Joi.object().keys({
                a: Joi.number().integer(),
                b: Joi.number().integer()
            });

            var validationFn = function(rawInput, validData) {
                var a = validData.a;
                var b = validData.b;
                var diff = b - a;

                var diffSchema = Joi.number().min(1).max(10);
                return Joi.validate(diff, diffSchema, {
                    stripUnknown: true,
                    convert: true
                });
            };

            var sanitizedInput = validate(sourceARawInput, schema, validationFn);

            assert.deepEqual(sanitizedInput, {
                a: 1,
                b: 10
            });
        });

        it('can perform dynamic Joi validation and add to validated data', function() {
            var sourceARawInput = {
                a: '1',
                b: '10'
            };

            var schema = Joi.object().keys({
                a: Joi.number().integer(),
                b: Joi.number().integer()
            });

            var validationFn = function(rawInput, validData) {
                var a = validData.a;
                var b = validData.b;
                var diff = b - a;

                var diffObjSchema = Joi.object().keys({
                    diff: Joi.number().min(1).max(10)
                });
                var objToValidate = {
                    diff: diff
                };
                return Joi.validate(objToValidate, diffObjSchema, {
                    stripUnknown: true,
                    convert: true
                });
            };

            var sanitizedInput = validate(sourceARawInput, schema, validationFn);

            assert.deepEqual(sanitizedInput, {
                a: 1,
                b: 10,
                diff: 9
            });
        });
    });

    it('can validate input using multiple schemas and/or functions', function() {
        var rawInput = {
            foo: 1,
            bar: 2,
            baz: '3'
        };

        var schema1 = Joi.object().keys({ foo: Joi.number().integer().required() });
        var validationFn = function(rawInput) {
            return {
                valid: rawInput.bar === 2,
                value: {
                    bar: rawInput.bar.toString()
                }
            }
        };
        var schema2 = Joi.object().keys({ baz: Joi.string() });

        var sanitizedInput = validate(rawInput, schema1, validationFn, schema2);

        assert.deepEqual(sanitizedInput, {
            foo: 1,
            bar: '2',
            baz: '3'
        });
    });

    it('can validate input using array of data plus schema(s)', function() {
        var sourceARawInput = {
            foo: 1,
            bar: 2
        };
        var sourceBRawInput = {
            baz: '3',
            ignoreMe: true
        };

        var schema1 = Joi.object().keys({ foo: Joi.number().integer().required() });
        var schema2 = Joi.object().keys({ bar: Joi.any().required() });
        var schema3 = Joi.object().keys({ baz: Joi.string() });

        var sanitizedInput = validate(
            [ sourceARawInput, schema1, schema2 ],
            [ sourceBRawInput, schema3 ]
        );

        assert.deepEqual(sanitizedInput, {
            foo: 1,
            bar: 2,
            baz: '3'
        });
    });

    it('can validate input using multiple data sources followed by multiple schemas', function() {
        var sourceARawInput = {
            foo: 1,
            bar: 2
        };
        var sourceBRawInput = {
            baz: '3',
            ignoreMe: true
        };

        var schema1 = Joi.object().keys({ foo: Joi.number().integer().required() });
        var schema2 = Joi.object().keys({ bar: Joi.any().required() });
        var schema3 = Joi.object().keys({ baz: Joi.string() });

        var sanitizedInput = validate(sourceARawInput, sourceBRawInput, schema1, schema2, schema3);

        assert.deepEqual(sanitizedInput, {
            foo: 1,
            bar: 2,
            baz: '3'
        });
    });

    it('can validate input using multiple data sources followed by single schema', function() {
        var sourceARawInput = {
            foo: 1,
            bar: 2
        };
        var sourceBRawInput = {
            baz: '3',
            ignoreMe: true
        };

        var schema = Joi.object().keys({ bar: Joi.any().required() });

        var sanitizedInput = validate(sourceARawInput, sourceBRawInput, schema);

        assert.deepEqual(sanitizedInput, {
            bar: 2
        });
    });

    it('can convert number strings to numbers', function() {
        var rawInput = {
            someId: '12345'
        };

        var schema = Joi.object().keys({ someId: Joi.number().integer().required() });

        var sanitizedInput = validate(rawInput, schema);

        assert.deepEqual(sanitizedInput, {
            someId: 12345
        });
    });

    describe('Validation Errors', function() {

        it('throws ValidationError on invalid input using static schema', function() {
            var rawInput = {
                foo: 'a',
                bar: 2
            };

            var schema1 = Joi.object().keys({ foo: Joi.number().integer().required() });

            var error = assert.throws(function() {
                validate(rawInput, schema1);
            });

            assert.shallowDeepEqual(error, {
                name: 'ValidationError',
                code: 400,
                message: [{
                    message: '"foo" must be a number',
                    path: 'foo'
                }]
            });
        });

        it('throws ValidationError on invalid input using custom validation function', function() {
            var rawInput = {
                bar: 1
            };

            var validationFn = function(validatedData, rawInput) {
                return {
                    valid: rawInput.bar === 2,
                    message: '"bar" must be 2'
                };
            };

            var error = assert.throws(function() {
                validate(rawInput, validationFn);
            });

            assert.shallowDeepEqual(error, {
                name: 'ValidationError',
                code: 400,
                message: [{
                    message: '"bar" must be 2'
                }]
            });
        });
    });
});