// Copyright IBM Corp. 2013,2019. All Rights Reserved.
// Node module: loopback-connector-mysql
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

'use strict';
const assert = require('assert');
const async = require('async');
const platform = require('./helpers/platform');
const should = require('./init');
const Schema = require('loopback-datasource-juggler').Schema;

let db, UserData, StringData, NumberData, DateData, DefaultData, SimpleEmployee;
let mysqlVersion;

describe('migrations', function() {
  before(setup);

  it('should run migration', function(done) {
    db.automigrate(function() {
      done();
    });
  });

  it('allow user specified datatype on PK', function(done) {
    query('describe SimpleEmployee', function(err, result) {
      should.not.exist(err);
      should.exist(result);
      result[0].Key.should.equal('PRI');
      result[0].Type.should.equal('bigint');
      done();
    });
  });

  it('UserData should have correct columns', function(done) {
    getFields('UserData', function(err, fields) {
      if (!fields) return done();
      fields.should.be.eql({
        id: {
          Field: 'id',
          Type: 'int',
          Null: 'NO',
          Key: 'PRI',
          Default: null,
          Extra: 'auto_increment'},
        email: {
          Field: 'email',
          Type: 'varchar(255)',
          Null: 'NO',
          Key: 'MUL',
          Default: null,
          Extra: ''},
        name: {
          Field: 'name',
          Type: 'varchar(512)',
          Null: 'YES',
          Key: '',
          Default: null,
          Extra: ''},
        bio: {
          Field: 'bio',
          Type: 'text',
          Null: 'YES',
          Key: '',
          Default: null,
          Extra: ''},
        birthDate: {
          Field: 'birthDate',
          Type: 'datetime',
          Null: 'YES',
          Key: '',
          Default: null,
          Extra: ''},
        pendingPeriod: {
          Field: 'pendingPeriod',
          Type: 'int',
          Null: 'YES',
          Key: '',
          Default: null,
          Extra: ''},
        createdByAdmin: {
          Field: 'createdByAdmin',
          Type: 'tinyint(1)',
          Null: 'YES',
          Key: '',
          Default: null,
          Extra: ''},
      });
      done();
    });
  });

  it('UserData should have correct indexes', function(done) {
    // Note: getIndexes truncates multi-key indexes to the first member.
    // Hence index1 is correct.
    getIndexes('UserData', function(err, fields) {
      if (!fields) return done();
      fields.should.match({
        PRIMARY: {
          Table: /UserData/i,
          Non_unique: 0,
          Key_name: 'PRIMARY',
          Seq_in_index: 1,
          Column_name: 'id',
          Collation: 'A',
          // XXX: this actually has more to do with whether the table existed or not and
          // what kind of data is in it that MySQL has analyzed:
          // https://dev.mysql.com/doc/refman/5.5/en/show-index.html
          // Cardinality: /^5\.[567]/.test(mysqlVersion) ? 0 : null,
          Sub_part: null,
          Packed: null,
          Null: '',
          Index_type: 'BTREE',
          Comment: ''},
        email: {
          Table: /UserData/i,
          Non_unique: 1,
          Key_name: 'email',
          Seq_in_index: 1,
          Column_name: 'email',
          Collation: 'A',
          // XXX: this actually has more to do with whether the table existed or not and
          // what kind of data is in it that MySQL has analyzed:
          // https://dev.mysql.com/doc/refman/5.5/en/show-index.html
          // Cardinality: /^5\.[567]/.test(mysqlVersion) ? 0 : null,
          Sub_part: null,
          Packed: null,
          Null: '',
          Index_type: 'BTREE',
          Comment: ''},
        index0: {
          Table: /UserData/i,
          Non_unique: 1,
          Key_name: 'index0',
          Seq_in_index: 1,
          Column_name: 'email',
          Collation: 'A',
          // XXX: this actually has more to do with whether the table existed or not and
          // what kind of data is in it that MySQL has analyzed:
          // https://dev.mysql.com/doc/refman/5.5/en/show-index.html
          // Cardinality: /^5\.[567]/.test(mysqlVersion) ? 0 : null,
          Sub_part: null,
          Packed: null,
          Null: '',
          Index_type: 'BTREE',
          Comment: ''},
      });
      done();
    });
  });

  it('StringData should have correct columns', function(done) {
    getFields('StringData', function(err, fields) {
      fields.should.be.eql({
        idString: {Field: 'idString',
          Type: 'varchar(255)',
          Null: 'NO',
          Key: 'PRI',
          Default: null,
          Extra: ''},
        smallString: {Field: 'smallString',
          Type: 'char(127)',
          Null: 'NO',
          Key: 'MUL',
          Default: null,
          Extra: ''},
        mediumString: {Field: 'mediumString',
          Type: 'varchar(255)',
          Null: 'NO',
          Key: '',
          Default: null,
          Extra: ''},
        tinyText: {Field: 'tinyText',
          Type: 'tinytext',
          Null: 'YES',
          Key: '',
          Default: null,
          Extra: ''},
        giantJSON: {Field: 'giantJSON',
          Type: 'longtext',
          Null: 'YES',
          Key: '',
          Default: null,
          Extra: ''},
        text: {Field: 'text',
          Type: 'varchar(1024)',
          Null: 'YES',
          Key: '',
          Default: null,
          Extra: ''},
      });
      done();
    });
  });

  it('NumberData should have correct columns', function(done) {
    getFields('NumberData', function(err, fields) {
      fields.should.be.eql({
        id: {Field: 'id',
          Type: 'int',
          Null: 'NO',
          Key: 'PRI',
          Default: null,
          Extra: 'auto_increment'},
        number: {Field: 'number',
          Type: 'decimal(10,3)',
          Null: 'NO',
          Key: 'MUL',
          Default: null,
          Extra: ''},
        tinyInt: {Field: 'tinyInt',
          Type: 'tinyint',
          Null: 'YES',
          Key: '',
          Default: null,
          Extra: ''},
        mediumInt: {Field: 'mediumInt',
          Type: 'mediumint unsigned',
          Null: 'NO',
          Key: '',
          Default: null,
          Extra: ''},
        floater: {Field: 'floater',
          Type: 'double(14,6)',
          Null: 'YES',
          Key: '',
          Default: null,
          Extra: ''},
      });
      done();
    });
  });

  it('DateData should have correct columns', function(done) {
    getFields('DateData', function(err, fields) {
      fields.should.be.eql({
        id: {Field: 'id',
          Type: 'int',
          Null: 'NO',
          Key: 'PRI',
          Default: null,
          Extra: 'auto_increment'},
        dateTime: {Field: 'dateTime',
          Type: 'datetime',
          Null: 'YES',
          Key: '',
          Default: null,
          Extra: ''},
        timestamp: {Field: 'timestamp',
          Type: 'timestamp',
          Null: 'YES',
          Key: '',
          Default: null,
          Extra: ''},
      });
      done();
    });
  });

  it('should autoupdate', function(done) {
    // With an install of MYSQL5.7 on windows, these queries `randomly` fail and raise errors
    // especially with decimals, number and Date format.
    if (platform.isWindows) {
      return done();
    }
    const userExists = function(cb) {
      query('SELECT * FROM UserData', function(err, res) {
        cb(!err && res[0].email == 'test@example.com');
      });
    };

    UserData.create({email: 'test@example.com'}, function(err, user) {
      assert.ok(!err, 'Could not create user: ' + err);
      userExists(function(yep) {
        assert.ok(yep, 'User does not exist');
      });
      UserData.defineProperty('email', {type: String});
      UserData.defineProperty('name', {type: String,
        dataType: 'char', limit: 50});
      UserData.defineProperty('newProperty', {type: Number, unsigned: true,
        dataType: 'bigInt'});
      // UserData.defineProperty('pendingPeriod', false);
      // This will not work as expected.
      db.autoupdate(function(err) {
        getFields('UserData', function(err, fields) {
          // change nullable for email
          assert.equal(fields.email.Null, 'YES', 'Email does not allow null');
          // change type of name
          assert.equal(fields.name.Type, 'char(50)', 'Name is not char(50)');
          // add new column
          assert.ok(fields.newProperty, 'New column was not added');
          if (fields.newProperty) {
            assert.equal(fields.newProperty.Type, 'bigint unsigned',
              'New column type is not bigint unsigned');
          }
          // drop column - will not happen.
          // assert.ok(!fields.pendingPeriod,
          // 'Did not drop column pendingPeriod');
          // user still exists
          userExists(function(yep) {
            assert.ok(yep, 'User does not exist');
            done();
          });
        });
      });
    });
  });

  it('should check actuality of dataSource', function(done) {
    // With an install of MYSQL5.7 on windows, these queries `randomly` fail and raise errors
    // with date, number and decimal format
    if (platform.isWindows) {
      return done();
    }
    // 'drop column'
    UserData.dataSource.isActual(function(err, ok) {
      assert.ok(ok, 'dataSource is not actual (should be)');
      UserData.defineProperty('essay', {type: Schema.Text});
      // UserData.defineProperty('email', false); Can't undefine currently.
      UserData.dataSource.isActual(function(err, ok) {
        assert.ok(!ok, 'dataSource is actual (shouldn\t be)');
        done();
      });
    });
  });

  // In MySQL 5.6/5.7 Out of range values are rejected.
  // Reference: http://dev.mysql.com/doc/refman/5.7/en/integer-types.html
  it('allows numbers with decimals', function(done) {
    NumberData.create(
      {number: 1.1234567, tinyInt: 127, mediumInt: 16777215, floater: 12345678.123456},
      function(err, obj) {
        if (err) return (err);
        NumberData.findById(obj.id, function(err, found) {
          assert.equal(found.number, 1.123);
          assert.equal(found.tinyInt, 127);
          assert.equal(found.mediumInt, 16777215);
          assert.equal(found.floater, 12345678.123456);
          done();
        });
      },
    );
  });

  // Reference: http://dev.mysql.com/doc/refman/5.7/en/out-of-range-and-overflow.html
  it('rejects out-of-range and overflow values', function(done) {
    async.series([
      function(next) {
        NumberData.create({number: 1.1234567, tinyInt: 128, mediumInt: 16777215}, function(err, obj) {
          assert(err);
          assert.equal(err.code, 'ER_WARN_DATA_OUT_OF_RANGE');
          next();
        });
      }, function(next) {
        NumberData.create({number: 1.1234567, mediumInt: 16777215 + 1}, function(err, obj) {
          assert(err);
          assert.equal(err.code, 'ER_WARN_DATA_OUT_OF_RANGE');
          next();
        });
      }, function(next) {
        // Minimum value for unsigned mediumInt is 0
        NumberData.create({number: 1.1234567, mediumInt: -8388608}, function(err, obj) {
          assert(err);
          assert.equal(err.code, 'ER_WARN_DATA_OUT_OF_RANGE');
          next();
        });
      }, function(next) {
        NumberData.create({number: 1.1234567, tinyInt: -129, mediumInt: 0}, function(err, obj) {
          assert(err);
          assert.equal(err.code, 'ER_WARN_DATA_OUT_OF_RANGE');
          next();
        });
      },
    ], done);
  });

  it('should take on database default CURRENT_TIMESTAMP, boolean 0 and pending string for columns', function(done) {
    DefaultData.create({}, function(err, obj) {
      assert.ok(!err);
      assert.ok(obj);
      const now = new Date();
      DefaultData.findById(obj.id, function(err, found) {
        now.setSeconds(0);
        found.dateTime.setSeconds(0);
        found.timestamp.setSeconds(0);

        assert.equal(found.dateTime.toGMTString(), now.toGMTString());
        assert.equal(found.timestamp.toGMTString(), now.toGMTString());
        assert.equal(found.isAdmin, '0');
        assert.equal(found.number, 256);
        assert.equal(found.data, null);
        assert.equal(found.text, null);
        assert.equal(found.status, 'pending');
        done();
      });
    });
  });

  it('DefaultData should have correct columns', function(done) {
    getFields('DefaultData', function(err, fields) {
      fields.should.be.eql({
        id: {Field: 'id',
          Type: 'int',
          Null: 'NO',
          Key: 'PRI',
          Default: null,
          Extra: 'auto_increment'},
        dateTime: {Field: 'dateTime',
          Type: 'datetime',
          Null: 'YES',
          Key: '',
          Default: 'CURRENT_TIMESTAMP',
          Extra: 'DEFAULT_GENERATED'},
        timestamp: {Field: 'timestamp',
          Type: 'timestamp',
          Null: 'YES',
          Key: '',
          Default: 'CURRENT_TIMESTAMP',
          Extra: 'DEFAULT_GENERATED'},
        isAdmin: {Field: 'isAdmin',
          Type: 'tinyint(1)',
          Null: 'YES',
          Key: '',
          Default: '0',
          Extra: ''},
        number: {Field: 'number',
          Type: 'int unsigned',
          Null: 'NO',
          Key: 'MUL',
          Default: '256',
          Extra: ''},
        data: {Field: 'data',
          Type: 'longtext',
          Null: 'YES',
          Key: '',
          Default: null,
          Extra: ''},
        text: {Field: 'text',
          Type: 'varchar(1024)',
          Null: 'YES',
          Key: '',
          Default: null,
          Extra: ''},
        status: {Field: 'status',
          Type: 'varchar(512)',
          Null: 'YES',
          Key: '',
          Default: 'pending',
          Extra: ''},
      });
      done();
    });
  });

  it('should allow both kinds of date columns', function(done) {
    DateData.create({
      dateTime: new Date('Aug 9 1996 07:47:33 GMT'),
      timestamp: new Date('Sep 22 2007 17:12:22 GMT'),
    }, function(err, obj) {
      assert.ok(!err);
      assert.ok(obj);
      DateData.findById(obj.id, function(err, found) {
        assert.equal(found.dateTime.toGMTString(),
          'Fri, 09 Aug 1996 07:47:33 GMT');
        assert.equal(found.timestamp.toGMTString(),
          'Sat, 22 Sep 2007 17:12:22 GMT');
        done();
      });
    });
  });

  // InMySQL5.7, DATETIME supported range is '1000-01-01 00:00:00' to '9999-12-31 23:59:59'.
  // TIMESTAMP has a range of '1970-01-01 00:00:01' UTC to '2038-01-19 03:14:07' UTC
  // Reference: http://dev.mysql.com/doc/refman/5.7/en/datetime.html
  // Out of range values are set to null in windows but rejected elsewhere
  // the next example is designed for windows while the following 2 are for other platforms
  it('should map zero dateTime into null', function(done) {
    if (!platform.isWindows) {
      return done();
    }

    query('INSERT INTO `DateData` ' +
      '(`dateTime`, `timestamp`) ' +
      'VALUES("0000-00-00 00:00:00", "0000-00-00 00:00:00") ',
    function(err, ret) {
      should.not.exists(err);
      DateData.findById(ret.insertId, function(err, dateData) {
        should(dateData.dateTime)
          .be.null();
        should(dateData.timestamp)
          .be.null();
        done();
      });
    });
  });

  it('rejects out of range datetime', function(done) {
    if (platform.isWindows) {
      return done();
    }

    query('INSERT INTO `DateData` ' +
      '(`dateTime`, `timestamp`) ' +
      'VALUES("0000-00-00 00:00:00", "0000-00-00 00:00:00") ', function(err) {
      const errMsg = 'Incorrect datetime value: ' +
          '\'0000-00-00 00:00:00\' for column \'dateTime\' at row 1';
      assert(err);
      assert.equal(err.message, errMsg);
      done();
    });
  });

  it('rejects out of range timestamp', function(done) {
    if (platform.isWindows) {
      return done();
    }

    query('INSERT INTO `DateData` ' +
      '(`dateTime`, `timestamp`) ' +
      'VALUES("1000-01-01 00:00:00", "0000-00-00 00:00:00") ', function(err) {
      const errMsg = 'Incorrect datetime value: ' +
            '\'0000-00-00 00:00:00\' for column \'timestamp\' at row 1';
      assert(err);
      assert.equal(err.message, errMsg);
      done();
    });
  });

  it('should report errors for automigrate', function() {
    db.automigrate('XYZ', function(err) {
      assert(err);
    });
  });

  it('should report errors for autoupdate', function() {
    db.autoupdate('XYZ', function(err) {
      assert(err);
    });
  });

  it('should disconnect when done', function(done) {
    db.disconnect();
    done();
  });
});

function setup(done) {
  require('./init.js');

  db = global.getSchema();

  UserData = db.define('UserData', {
    email: {type: String, null: false, index: true},
    name: String,
    bio: Schema.Text,
    birthDate: Date,
    pendingPeriod: Number,
    createdByAdmin: Boolean,
  }, {indexes: {
    index0: {
      columns: 'email, createdByAdmin',
    },
  },
  });

  StringData = db.define('StringData', {
    idString: {type: String, id: true},
    smallString: {type: String, null: false, index: true,
      dataType: 'char', limit: 127},
    mediumString: {type: String, null: false, dataType: 'varchar', limit: 255},
    tinyText: {type: String, dataType: 'tinyText'},
    giantJSON: {type: Schema.JSON, dataType: 'longText'},
    text: {type: Schema.Text, dataType: 'varchar', limit: 1024},
  });

  NumberData = db.define('NumberData', {
    number: {type: Number, null: false, index: true, unsigned: true,
      dataType: 'decimal', precision: 10, scale: 3},
    tinyInt: {type: Number, dataType: 'tinyInt', display: 2},
    mediumInt: {type: Number, dataType: 'mediumInt', unsigned: true,
      required: true},
    floater: {type: Number, dataType: 'double', precision: 14, scale: 6},
  });

  DefaultData = db.define('DefaultData', {
    dateTime: {type: Date, dataType: 'datetime', mysql: {default: 'now'}},
    timestamp: {type: Date, dataType: 'timestamp', mysql: {default: 'CURRENT_TIMESTAMP'}},
    isAdmin: {type: Boolean, mysql: {default: '0'}},
    number: {type: Number, null: false, index: true, unsigned: true,
      dataType: 'int', mysql: {default: 256}},
    data: {type: Schema.JSON, dataType: 'longText', mysql: {default: 'Not Supported'}},
    text: {type: Schema.Text, dataType: 'varchar', limit: 1024, mysql: {default: 'Not Supported'}},
    status: {type: String, dataType: 'varchar', mysql: {default: 'pending'}},
  });

  DateData = db.define('DateData', {
    dateTime: {type: Date, dataType: 'datetime'},
    timestamp: {type: Date, dataType: 'timestamp'},
  });

  SimpleEmployee = db.define('SimpleEmployee', {
    eId: {type: Number, generated: true, id: true, mysql: {dataType: 'bigint', dataLength: 20}},
    name: {type: String},
  });

  query('SELECT VERSION()', function(err, res) {
    mysqlVersion = res && res[0] && res[0]['VERSION()'];
    blankDatabase(db, done);
  });
}

function query(sql, cb) {
  db.adapter.execute(sql, cb);
}

function blankDatabase(db, cb) {
  const dbn = db.settings.database;
  const cs = db.settings.charset;
  const co = db.settings.collation;
  query('DROP DATABASE IF EXISTS ' + dbn, function(err) {
    let q = 'CREATE DATABASE ' + dbn;
    if (cs) {
      q += ' CHARACTER SET ' + cs;
    }
    if (co) {
      q += ' COLLATE ' + co;
    }
    query(q, function(err) {
      query('USE ' + dbn, cb);
    });
  });
}

function getFields(model, cb) {
  query('SHOW FIELDS FROM ' + model, function(err, res) {
    if (err) {
      cb(err);
    } else {
      const fields = {};
      res.forEach(function(field) {
        fields[field.Field] = field;
      });
      // The returned data are in arrays of type `RowDataPacket`,
      // which are not objects.
      cb(err, JSON.parse(JSON.stringify(fields)));
    }
  });
}

function getIndexes(model, cb) {
  query('SHOW INDEXES FROM ' + model, function(err, res) {
    if (err) {
      console.log(err);
      cb(err);
    } else {
      const indexes = {};
      // Note: this will only show the first key of compound keys
      res.forEach(function(index) {
        if (parseInt(index.Seq_in_index, 10) == 1) {
          indexes[index.Key_name] = index;
        }
      });
      cb(err, indexes);
    }
  });
}