'use strict';

var Ajv = require('./ajv');
require('./chai').should();


var coercionRules = {
  'string': {
    'number': [
      { from: 1, to: '1' },
      { from: 1.5, to: '1.5' },
      { from: 2e100, to: '2e+100' }
    ],
    'boolean': [
      { from: false, to: 'false' },
      { from: true, to: 'true' }
    ],
    'null': [
      { from: null, to: '' }
    ],
    'object': [
      { from: {}, to: undefined }
    ],
    'array': [
      { from: [], to: undefined },
      { from: [1], to: undefined }
    ]
  },
  'number': {
    'string': [
      { from: '1', to: 1 },
      { from: '1.5', to: 1.5 },
      { from: '2e10', to: 2e10 },
      { from: '1a', to: undefined },
      { from: 'abc', to: undefined },
      { from: '', to: undefined }
    ],
    'boolean': [
      { from: false, to: 0 },
      { from: true, to: 1 }
    ],
    'null': [
      { from: null, to: 0 }
    ],
    'object': [
      { from: {}, to: undefined }
    ],
    'array': [
      { from: [], to: undefined },
      { from: [true], to: undefined }
    ]
  },
  'integer': {
    'string': [
      { from: '1', to: 1 },
      { from: '1.5', to: undefined },
      { from: '2e10', to: 2e10 },
      { from: '1a', to: undefined },
      { from: 'abc', to: undefined },
      { from: '', to: undefined }
    ],
    'boolean': [
      { from: false, to: 0 },
      { from: true, to: 1 }
    ],
    'null': [
      { from: null, to: 0 }
    ],
    'object': [
      { from: {}, to: undefined }
    ],
    'array': [
      { from: [], to: undefined },
      { from: ['1'], to: undefined }
    ]
  },
  'boolean': {
    'string': [
      { from: 'false', to: false },
      { from: 'true', to: true },
      { from: '', to: undefined },
      { from: 'abc', to: undefined }
    ],
    'number': [
      { from: 0, to: false },
      { from: 1, to: true },
      { from: 2, to: undefined },
      { from: 2.5, to: undefined }
    ],
    'null': [
      { from: null, to: false }
    ],
    'object': [
      { from: {}, to: undefined }
    ],
    'array': [
      { from: [], to: undefined },
      { from: [0], to: undefined }
    ]
  },
  'null': {
    'string': [
      { from: '',     to: null },
      { from: 'abc',  to: undefined },
      { from: 'null', to: undefined }
    ],
    'number': [
      { from: 0, to: null },
      { from: 1, to: undefined }
    ],
    'boolean': [
      { from: false, to: null },
      { from: true,  to: undefined }
    ],
    'object': [
      { from: {}, to: undefined }
    ],
    'array': [
      { from: [], to: undefined },
      { from: [null], to: undefined }
    ]
  },
  'array': {
    'all': [
      { type: 'string',  from: 'abc', to: undefined },
      { type: 'number',  from: 1,     to: undefined },
      { type: 'boolean', from: true,  to: undefined },
      { type: 'null',    from: null,  to: undefined },
      { type: 'object',  from: {},    to: undefined }
    ]
  },
  'object': {
    'all': [
      { type: 'string',  from: 'abc', to: undefined },
      { type: 'number',  from: 1,     to: undefined },
      { type: 'boolean', from: true,  to: undefined },
      { type: 'null',    from: null,  to: undefined },
      { type: 'array',   from: [],    to: undefined }
    ]
  }
};

var coercionArrayRules = JSON.parse(JSON.stringify(coercionRules));
coercionArrayRules.string.array = [
  { from: ['abc'], to: 'abc' },
  { from: [123], to: '123' },
  { from: [true], to: 'true'},
  { from: [null], to: ''},
  { from: [{}], to: undefined },
  { from: ['abc', 'def'], to: undefined },
  { from: [], to: undefined }
];
coercionArrayRules.number.array = [
  { from: [1.5], to: 1.5 },
  { from: ['1.5'], to: 1.5 },
  { from: [true], to: 1 },
  { from: [null], to: 0 },
  { from: ['abc'], to: undefined },
  { from: [{}], to: undefined },
];
coercionArrayRules.integer.array =  [
  { from: [1], to: 1 },
  { from: ['1'], to: 1 },
  { from: [true], to: 1 },
  { from: [null], to: 0 },
  { from: [1.5], to: undefined },
  { from: ['abc'], to: undefined },
  { from: [{}], to: undefined },
];
coercionArrayRules.boolean.array =  [
  { from: [true], to: true },
  { from: ['true'], to: true },
  { from: [1], to: true },
  { from: [null], to: false },
  { from: ['abc'], to: undefined },
  { from: [2], to: undefined },
  { from: [{}], to: undefined },
];
coercionArrayRules.null.array =  [
  { from: [null], to: null },
  { from: [''], to: null },
  { from: [0], to: null },
  { from: [false], to: null },
  { from: ['abc'], to: undefined },
  { from: [1], to: undefined },
  { from: [true], to: undefined },
  { from: [{}], to: undefined },
];
coercionArrayRules.object.array =  [
  { from: [{}], to: undefined }
];

coercionArrayRules.array = {
  'string':  [
    {from: 'abc', to: ['abc']}
  ],
  'number':  [
    {from: 1, to: [1]}
  ],
  'boolean': [
    {from: true, to: [true]}
  ],
  'null':    [
    {from: null, to: [null]}
  ],
  'object':  [
    {from: {}, to: undefined}
  ]
};

describe('Type coercion', function () {
  var ajv, fullAjv, instances;

  beforeEach(function() {
    ajv = new Ajv({ coerceTypes: true, verbose: true });
    fullAjv = new Ajv({ coerceTypes: true, verbose: true, allErrors: true });
    instances = [ ajv, fullAjv ];
  });


  it('should coerce scalar values', function() {
    testRules(coercionRules, function (test, schema, canCoerce/*, toType, fromType*/) {
      instances.forEach(function (_ajv) {
        var valid = _ajv.validate(schema, test.from);
        //if (valid !== canCoerce) console.log('true', toType, fromType, test, ajv.errors);
        valid. should.equal(canCoerce);
      });
    });
  });

  it('should coerce scalar values (coerceTypes = array)', function() {
    ajv = new Ajv({ coerceTypes: 'array', verbose: true });
    fullAjv = new Ajv({ coerceTypes: 'array', verbose: true, allErrors: true });
    instances = [ ajv, fullAjv ];

    testRules(coercionArrayRules, function (test, schema, canCoerce, toType, fromType) {
      instances.forEach(function (_ajv) {
        var valid = _ajv.validate(schema, test.from);
        if (valid !== canCoerce) console.log(toType, '.', fromType, test, schema, ajv.errors);
        valid. should.equal(canCoerce);
      });
    });
  });

  it('should coerce values in objects/arrays and update properties/items', function() {
    testRules(coercionRules, function (test, schema, canCoerce/*, toType, fromType*/) {
      var schemaObject = {
        type: 'object',
        properties: {
          foo: schema
        }
      };

      var schemaArray = {
        type: 'array',
        items: schema
      };

      var schemaArrObj = {
        type: 'array',
        items: schemaObject
      };


      instances.forEach(function (_ajv) {
        testCoercion(_ajv, schemaArray,  [ test.from ], [ test.to ]);
        testCoercion(_ajv, schemaObject, { foo: test.from }, { foo: test.to });
        testCoercion(_ajv, schemaArrObj, [ { foo: test.from } ], [ { foo: test.to } ]);
      });

      function testCoercion(_ajv, _schema, fromData, toData) {
        var valid = _ajv.validate(_schema, fromData);
        //if (valid !== canCoerce) console.log(schema, fromData, toData);
        valid. should.equal(canCoerce);
        if (valid) fromData.should.eql(toData);
      }
    });
  });


  it('should coerce to multiple types in order with number type', function() {
    var schema = {
      type: 'object',
      properties: {
        foo: {
          type: [ 'number', 'boolean', 'null' ]
        }
      }
    };

    instances.forEach(function (_ajv) {
      var data;

      _ajv.validate(schema, data = { foo: '1' }) .should.equal(true);
      data .should.eql({ foo: 1 });

      _ajv.validate(schema, data = { foo: '1.5' }) .should.equal(true);
      data .should.eql({ foo: 1.5 });

      _ajv.validate(schema, data = { foo: 'false' }) .should.equal(true);
      data .should.eql({ foo: false });

      _ajv.validate(schema, data = { foo: 1 }) .should.equal(true);
      data .should.eql({ foo: 1 }); // no coercion

      _ajv.validate(schema, data = { foo: true }) .should.equal(true);
      data .should.eql({ foo: true }); // no coercion

      _ajv.validate(schema, data = { foo: null }) .should.equal(true);
      data .should.eql({ foo: null }); // no coercion

      _ajv.validate(schema, data = { foo: 'abc' }) .should.equal(false);
      data .should.eql({ foo: 'abc' }); // can't coerce

      _ajv.validate(schema, data = { foo: {} }) .should.equal(false);
      data .should.eql({ foo: {} }); // can't coerce

      _ajv.validate(schema, data = { foo: [] }) .should.equal(false);
      data .should.eql({ foo: [] }); // can't coerce
    });
  });

  it('should coerce to multiple types in order with integer type', function() {
    var schema = {
      type: 'object',
      properties: {
        foo: {
          type: [ 'integer', 'boolean', 'null' ]
        }
      }
    };

    instances.forEach(function (_ajv) {
      var data;

      _ajv.validate(schema, data = { foo: '1' }) .should.equal(true);
      data .should.eql({ foo: 1 });

      _ajv.validate(schema, data = { foo: 'false' }) .should.equal(true);
      data .should.eql({ foo: false });

      _ajv.validate(schema, data = { foo: 1 }) .should.equal(true);
      data .should.eql({ foo: 1 }); // no coercion

      _ajv.validate(schema, data = { foo: true }) .should.equal(true);
      data .should.eql({ foo: true }); // no coercion

      _ajv.validate(schema, data = { foo: null }) .should.equal(true);
      data .should.eql({ foo: null }); // no coercion

      _ajv.validate(schema, data = { foo: 'abc' }) .should.equal(false);
      data .should.eql({ foo: 'abc' }); // can't coerce

      _ajv.validate(schema, data = { foo: {} }) .should.equal(false);
      data .should.eql({ foo: {} }); // can't coerce

      _ajv.validate(schema, data = { foo: [] }) .should.equal(false);
      data .should.eql({ foo: [] }); // can't coerce
    });
  });


  it('should fail to coerce non-number if multiple properties/items are coerced (issue #152)', function() {
    var schema = {
      type: 'object',
      properties: {
        foo: { type: 'number' },
        bar: { type: 'number' }
      }
    };

    var schema2 = {
      type: 'array',
      items: { type: 'number' }
    };

    instances.forEach(function (_ajv)  {
      var data = { foo: '123', bar: 'bar' };
      _ajv.validate(schema, data) .should.equal(false);
      data .should.eql({ foo: 123, bar: 'bar' });

      var data2 = [ '123', 'bar' ];
      _ajv.validate(schema2, data2) .should.equal(false);
      data2 .should.eql([ 123, 'bar' ]);
    });
  });


  it('should update data if the schema is in ref that is not inlined', function () {
    instances.push(new Ajv({ coerceTypes: true, inlineRefs: false }));

    var schema = {
      type: 'object',
      definitions: {
        foo: { type: 'number' }
      },
      properties: {
        foo: { $ref: '#/definitions/foo' }
      }
    };

    var schema2 = {
      type: 'object',
      definitions: {
        foo: {
          // allOf is needed to make sure that "foo" is compiled to a separate function
          // and not simply passed through (as it would be if it were only $ref)
          allOf: [{ $ref: '#/definitions/bar' }]
        },
        bar: { type: 'number' }
      },
      properties: {
        foo: { $ref: '#/definitions/foo' }
      }
    };

    var schemaRecursive = {
      type: [ 'object', 'number' ],
      properties: {
        foo: { $ref: '#' }
      }
    };

    var schemaRecursive2 = {
      $id: 'http://e.com/schema.json#',
      definitions: {
        foo: {
          $id: 'http://e.com/foo.json#',
          type: [ 'object', 'number' ],
          properties: {
            foo: { $ref: '#' }
          }
        }
      },
      properties: {
        foo: { $ref: 'http://e.com/foo.json#' }
      }
    };

    instances.forEach(function (_ajv) {
      testCoercion(schema, { foo: '1' }, { foo: 1 });
      testCoercion(schema2, { foo: '1' }, { foo: 1 });
      testCoercion(schemaRecursive, { foo: { foo: '1' } }, { foo: { foo: 1 } });
      testCoercion(schemaRecursive2, { foo: { foo: { foo: '1' } } },
                                     { foo: { foo: { foo: 1 } } });

      function testCoercion(_schema, fromData, toData) {
        var valid = _ajv.validate(_schema, fromData);
        // if (!valid) console.log(schema, fromData, toData);
        valid. should.equal(true);
        fromData .should.eql(toData);
      }
    });
  });


  it('should generate one error for type with coerceTypes option (issue #469)', function() {
    var schema = {
      "type": "number",
      "minimum": 10
    };

    instances.forEach(function (_ajv) {
      var validate = _ajv.compile(schema);
      validate(9). should.equal(false);
      validate.errors.length .should.equal(1);

      validate(11). should.equal(true);

      validate('foo'). should.equal(false);
      validate.errors.length .should.equal(1);
    });
  });


  it('should check "uniqueItems" after coercion', function() {
    var schema = {
      items: {type: 'number'},
      uniqueItems: true
    };

    instances.forEach(function (_ajv) {
      var validate = _ajv.compile(schema);
      validate([1, '2', 3]). should.equal(true);

      validate([1, '2', 2]). should.equal(false);
      validate.errors.length .should.equal(1);
      validate.errors[0].keyword .should.equal('uniqueItems');
    });
  });


  it('should check "contains" after coercion', function() {
    var schema = {
      items: {type: 'number'},
      contains: {const: 2}
    };

    instances.forEach(function (_ajv) {
      var validate = _ajv.compile(schema);
      validate([1, '2', 3]). should.equal(true);

      validate([1, '3', 4]). should.equal(false);
      validate.errors.pop().keyword .should.equal('contains');
    });
  });


  function testRules(rules, cb) {
    for (var toType in rules) {
      for (var fromType in rules[toType]) {
        var tests = rules[toType][fromType];
        tests.forEach(function (test) {
          var canCoerce = test.to !== undefined;
          var schema = canCoerce
                        ? (Array.isArray(test.to)
                          ? { "type": toType, "items": { "type": fromType, "enum": [ test.to[0] ] } }
                          : { "type": toType, "enum": [ test.to ] })
                        : { type: toType };
          cb(test, schema, canCoerce, toType, fromType);
        });
      }
    }
  }
});
