'use strict';

const chai = require('chai');

const expect = chai.expect;
const Support = require('./support');
const { DataTypes, sql } = require('@sequelize/core');

const dialect = Support.getTestDialect();
const sinon = require('sinon');
const isUUID = require('validator').isUUID;

describe(Support.getTestDialectTeaser('Instance'), () => {
  before(function () {
    this.clock = sinon.useFakeTimers();
  });

  afterEach(function () {
    this.clock.reset();
  });

  after(function () {
    this.clock.restore();
  });

  beforeEach(async function () {
    this.User = this.sequelize.define('User', {
      username: { type: DataTypes.STRING },
      uuidv1: { type: DataTypes.UUID, defaultValue: sql.uuidV1 },
      uuidv4: { type: DataTypes.UUID, defaultValue: sql.uuidV4 },
      touchedAt: { type: DataTypes.DATE, defaultValue: DataTypes.NOW },
      aNumber: { type: DataTypes.INTEGER },
      bNumber: { type: DataTypes.INTEGER },
      aDate: { type: DataTypes.DATE },

      validateTest: {
        type: DataTypes.INTEGER,
        allowNull: true,
        validate: { isInt: true },
      },
      validateCustom: {
        type: DataTypes.STRING,
        allowNull: true,
        validate: { len: { msg: 'Length failed.', args: [1, 20] } },
      },

      dateAllowNullTrue: {
        type: DataTypes.DATE,
        allowNull: true,
      },

      isSuperUser: {
        type: DataTypes.BOOLEAN,
        defaultValue: false,
      },
    });

    await this.User.sync({ force: true });
  });

  describe('Escaping', () => {
    it('is done properly for special characters', async function () {
      // Ideally we should test more: "\0\n\r\b\t\\\'\"\x1a"
      // But this causes sqlite to fail and exits the entire test suite immediately
      const bio = `${dialect}'"\n`; // Need to add the dialect here so in case of failure I know what DB it failed for

      const u1 = await this.User.create({ username: bio });
      const u2 = await this.User.findByPk(u1.id);
      expect(u2.username).to.equal(bio);
    });
  });

  describe('isNewRecord', () => {
    it('returns true for non-saved objects', function () {
      const user = this.User.build({ username: 'user' });
      expect(user.id).to.be.null;
      expect(user.isNewRecord).to.be.ok;
    });

    it('returns false for saved objects', async function () {
      const user = await this.User.build({ username: 'user' }).save();
      expect(user.isNewRecord).to.not.be.ok;
    });

    it('returns false for created objects', async function () {
      const user = await this.User.create({ username: 'user' });
      expect(user.isNewRecord).to.not.be.ok;
    });

    it('returns false for upserted objects', async function () {
      if (!Support.sequelize.dialect.supports.upserts) {
        return;
      }

      // adding id here so MSSQL doesn't fail. It needs a primary key to upsert
      const [user] = await this.User.upsert({ id: 2, username: 'user' });
      expect(user.isNewRecord).to.not.be.ok;
    });

    it('returns false for objects found by find method', async function () {
      await this.User.create({ username: 'user' });
      const user = await this.User.create({ username: 'user' });
      const user0 = await this.User.findByPk(user.id);
      expect(user0.isNewRecord).to.not.be.ok;
    });

    it('returns false for objects found by findAll method', async function () {
      const users = [];

      for (let i = 0; i < 10; i++) {
        users[i] = { username: 'user' };
      }

      await this.User.bulkCreate(users);
      const users0 = await this.User.findAll();
      for (const u of users0) {
        expect(u.isNewRecord).to.not.be.ok;
      }
    });
  });

  describe('default values', () => {
    describe('uuid', () => {
      it('should store a string in uuidv1 and uuidv4', function () {
        const user = this.User.build({ username: 'a user' });
        expect(user.uuidv1).to.be.a('string');
        expect(user.uuidv4).to.be.a('string');
      });

      it('should store a string of length 36 in uuidv1 and uuidv4', function () {
        const user = this.User.build({ username: 'a user' });
        expect(user.uuidv1).to.have.length(36);
        expect(user.uuidv4).to.have.length(36);
      });

      it('should store a valid uuid in uuidv1 and uuidv4 that conforms to the UUID v1 and v4 specifications', function () {
        const user = this.User.build({ username: 'a user' });
        expect(isUUID(user.uuidv1)).to.be.true;
        expect(isUUID(user.uuidv4, 4)).to.be.true;
      });

      it('should store a valid uuid if the multiple primary key fields used', function () {
        const Person = this.sequelize.define('Person', {
          id1: {
            type: DataTypes.UUID,
            defaultValue: sql.uuidV1,
            primaryKey: true,
          },
          id2: {
            type: DataTypes.UUID,
            defaultValue: sql.uuidV4,
            primaryKey: true,
          },
        });

        const person = Person.build({});
        expect(person.id1).to.be.ok;
        expect(person.id1).to.have.length(36);

        expect(person.id2).to.be.ok;
        expect(person.id2).to.have.length(36);
      });
    });
    describe('current date', () => {
      it('should store a date in touchedAt', function () {
        const user = this.User.build({ username: 'a user' });
        expect(user.touchedAt).to.be.instanceof(Date);
      });

      it('should store the current date in touchedAt', function () {
        this.clock.tick(5000);
        const user = this.User.build({ username: 'a user' });
        this.clock.restore();
        expect(Number(user.touchedAt)).to.equal(5000);
      });
    });

    describe('allowNull date', () => {
      it('should be just "null" and not Date with Invalid Date', async function () {
        await this.User.build({ username: 'a user' }).save();
        const user = await this.User.findOne({ where: { username: 'a user' } });
        expect(user.dateAllowNullTrue).to.be.null;
      });

      it('should be the same valid date when saving the date', async function () {
        const date = new Date('2018-01-01T12:12:12.000Z');
        await this.User.build({ username: 'a user', dateAllowNullTrue: date }).save();
        const user = await this.User.findOne({ where: { username: 'a user' } });
        expect(user.dateAllowNullTrue.toISOString()).to.equal(date.toISOString());
      });
    });

    describe('super user boolean', () => {
      it('should default to false', async function () {
        await this.User.build({
          username: 'a user',
        }).save();

        const user = await this.User.findOne({
          where: {
            username: 'a user',
          },
        });

        expect(user.isSuperUser).to.be.false;
      });

      it('should override default when given truthy boolean', async function () {
        await this.User.build({
          username: 'a user',
          isSuperUser: true,
        }).save();

        const user = await this.User.findOne({
          where: {
            username: 'a user',
          },
        });

        expect(user.isSuperUser).to.be.true;
      });

      it('should throw error when given value of incorrect type', async function () {
        let callCount = 0;

        try {
          await this.User.build({
            username: 'a user',
            isSuperUser: 'INCORRECT_VALUE_TYPE',
          }).save();

          callCount += 1;
        } catch (error) {
          expect(callCount).to.equal(0);
          expect(error).to.exist;
          expect(error.message).to.exist;
        }
      });
    });
  });

  describe('complete', () => {
    it('gets triggered if an error occurs', async function () {
      try {
        await this.User.findOne({ where: ['asdasdasd'] });
      } catch (error) {
        expect(error).to.exist;
        expect(error.message).to.exist;
      }
    });

    it('gets triggered if everything was ok', async function () {
      const result = await this.User.count();
      expect(result).to.exist;
    });
  });

  describe('findAll', () => {
    beforeEach(async function () {
      this.ParanoidUser = this.sequelize.define(
        'ParanoidUser',
        {
          username: { type: DataTypes.STRING },
        },
        { paranoid: true },
      );

      this.ParanoidUser.hasOne(this.ParanoidUser, {
        as: 'paranoidParent',
        inverse: { as: 'paranoidChild' },
      });
      await this.ParanoidUser.sync({ force: true });
    });

    it('sql should have paranoid condition', async function () {
      await this.ParanoidUser.create({ username: 'cuss' });
      const users0 = await this.ParanoidUser.findAll();
      expect(users0).to.have.length(1);
      await users0[0].destroy();
      const users = await this.ParanoidUser.findAll();
      expect(users).to.have.length(0);
    });

    it('sequelize.and as where should include paranoid condition', async function () {
      await this.ParanoidUser.create({ username: 'cuss' });

      const users0 = await this.ParanoidUser.findAll({
        where: this.sequelize.and({
          username: 'cuss',
        }),
      });

      expect(users0).to.have.length(1);
      await users0[0].destroy();

      const users = await this.ParanoidUser.findAll({
        where: this.sequelize.and({
          username: 'cuss',
        }),
      });

      expect(users).to.have.length(0);
    });

    it('sequelize.or as where should include paranoid condition', async function () {
      await this.ParanoidUser.create({ username: 'cuss' });

      const users0 = await this.ParanoidUser.findAll({
        where: this.sequelize.or({
          username: 'cuss',
        }),
      });

      expect(users0).to.have.length(1);
      await users0[0].destroy();

      const users = await this.ParanoidUser.findAll({
        where: this.sequelize.or({
          username: 'cuss',
        }),
      });

      expect(users).to.have.length(0);
    });

    it('escapes a single single quotes properly in where clauses', async function () {
      await this.User.create({ username: "user'name" });

      const users = await this.User.findAll({
        where: { username: "user'name" },
      });

      expect(users.length).to.equal(1);
      expect(users[0].username).to.equal("user'name");
    });

    it('escapes two single quotes properly in where clauses', async function () {
      await this.User.create({ username: "user''name" });

      const users = await this.User.findAll({
        where: { username: "user''name" },
      });

      expect(users.length).to.equal(1);
      expect(users[0].username).to.equal("user''name");
    });

    it('returns the timestamps if no attributes have been specified', async function () {
      await this.User.create({ username: 'fnord' });
      const users = await this.User.findAll();
      expect(users[0].createdAt).to.exist;
    });

    it('does not return the timestamps if the username attribute has been specified', async function () {
      await this.User.create({ username: 'fnord' });
      const users = await this.User.findAll({ attributes: ['username'] });
      expect(users[0].createdAt).not.to.exist;
      expect(users[0].username).to.exist;
    });

    it('creates the deletedAt property, when defining paranoid as true', async function () {
      await this.ParanoidUser.create({ username: 'fnord' });
      const users = await this.ParanoidUser.findAll();
      expect(users[0].deletedAt).to.be.null;
    });

    it('destroys a record with a primary key of something other than id', async function () {
      const UserDestroy = this.sequelize.define('UserDestroy', {
        newId: {
          type: DataTypes.STRING,
          primaryKey: true,
        },
        email: DataTypes.STRING,
      });

      await UserDestroy.sync();
      await UserDestroy.create({ newId: '123ABC', email: 'hello' });
      const user = await UserDestroy.findOne({ where: { email: 'hello' } });

      await user.destroy();
    });

    it('sets deletedAt property to a specific date when deleting an instance', async function () {
      await this.ParanoidUser.create({ username: 'fnord' });
      const users = await this.ParanoidUser.findAll();
      await users[0].destroy();
      expect(users[0].deletedAt.getMonth).to.exist;

      const user = await users[0].reload({ paranoid: false });
      expect(user.deletedAt.getMonth).to.exist;
    });

    it('keeps the deletedAt-attribute with value null, when running update', async function () {
      await this.ParanoidUser.create({ username: 'fnord' });
      const users = await this.ParanoidUser.findAll();
      const user = await users[0].update({ username: 'newFnord' });
      expect(user.deletedAt).not.to.exist;
    });

    it('keeps the deletedAt-attribute with value null, when updating associations', async function () {
      await this.ParanoidUser.create({ username: 'fnord' });
      const users = await this.ParanoidUser.findAll();
      const linkedUser = await this.ParanoidUser.create({ username: 'linkedFnord' });
      const user = await users[0].setParanoidParent(linkedUser);
      expect(user.deletedAt).not.to.exist;
    });

    it('can reuse query option objects', async function () {
      await this.User.create({ username: 'fnord' });
      const query = { where: { username: 'fnord' } };
      const users = await this.User.findAll(query);
      expect(users[0].username).to.equal('fnord');
      const users0 = await this.User.findAll(query);
      expect(users0[0].username).to.equal('fnord');
    });
  });

  describe('findOne', () => {
    it('can reuse query option objects', async function () {
      await this.User.create({ username: 'fnord' });
      const query = { where: { username: 'fnord' } };
      const user = await this.User.findOne(query);
      expect(user.username).to.equal('fnord');
      const user0 = await this.User.findOne(query);
      expect(user0.username).to.equal('fnord');
    });
    it('returns null for null, undefined, and unset boolean values', async function () {
      const Setting = this.sequelize.define(
        'SettingHelper',
        {
          setting_key: DataTypes.STRING,
          bool_value: { type: DataTypes.BOOLEAN, allowNull: true },
          bool_value2: { type: DataTypes.BOOLEAN, allowNull: true },
          bool_value3: { type: DataTypes.BOOLEAN, allowNull: true },
        },
        { timestamps: false, logging: false },
      );

      await Setting.sync({ force: true });
      await Setting.create({ setting_key: 'test', bool_value: null, bool_value2: undefined });
      const setting = await Setting.findOne({ where: { setting_key: 'test' } });
      expect(setting.bool_value).to.equal(null);
      expect(setting.bool_value2).to.equal(null);
      expect(setting.bool_value3).to.equal(null);
    });
  });

  describe('equals', () => {
    it('can compare records with Date field', async function () {
      const user1 = await this.User.create({ username: 'fnord' });
      const user2 = await this.User.findOne({ where: { username: 'fnord' } });
      expect(user1.equals(user2)).to.be.true;
    });

    it('does not compare the existence of associations', async function () {
      this.UserAssociationEqual = this.sequelize.define(
        'UserAssociationEquals',
        {
          username: DataTypes.STRING,
          age: DataTypes.INTEGER,
        },
        { timestamps: false },
      );

      this.ProjectAssociationEqual = this.sequelize.define(
        'ProjectAssocationEquals',
        {
          title: DataTypes.STRING,
          overdue_days: DataTypes.INTEGER,
        },
        { timestamps: false },
      );

      this.UserAssociationEqual.hasMany(this.ProjectAssociationEqual, {
        as: 'Projects',
        foreignKey: 'userId',
      });
      this.ProjectAssociationEqual.belongsTo(this.UserAssociationEqual, {
        as: 'Users',
        foreignKey: 'userId',
      });

      await this.UserAssociationEqual.sync({ force: true });
      await this.ProjectAssociationEqual.sync({ force: true });
      const user1 = await this.UserAssociationEqual.create({ username: 'jimhalpert' });
      const project1 = await this.ProjectAssociationEqual.create({ title: 'A Cool Project' });
      await user1.setProjects([project1]);
      const user2 = await this.UserAssociationEqual.findOne({
        where: { username: 'jimhalpert' },
        include: [{ model: this.ProjectAssociationEqual, as: 'Projects' }],
      });
      const user3 = await this.UserAssociationEqual.create({ username: 'pambeesly' });
      expect(user1.get('Projects')).to.not.exist;
      expect(user2.get('Projects')).to.exist;
      expect(user1.equals(user2)).to.be.true;
      expect(user2.equals(user1)).to.be.true;
      expect(user1.equals(user3)).to.not.be.true;
      expect(user3.equals(user1)).to.not.be.true;
    });
  });

  describe('values', () => {
    it('returns all values', async function () {
      const User = this.sequelize.define(
        'UserHelper',
        {
          username: DataTypes.STRING,
        },
        { timestamps: false, logging: false },
      );

      await User.sync();
      const user = User.build({ username: 'foo' });
      expect(user.get({ plain: true })).to.deep.equal({ username: 'foo', id: null });
    });
  });

  describe('isSoftDeleted', () => {
    beforeEach(async function () {
      this.ParanoidUser = this.sequelize.define(
        'ParanoidUser',
        {
          username: { type: DataTypes.STRING },
        },
        { paranoid: true },
      );

      await this.ParanoidUser.sync({ force: true });
    });

    it('should return false when model is just created', async function () {
      const user = await this.ParanoidUser.create({ username: 'foo' });
      expect(user.isSoftDeleted()).to.be.false;
    });

    it('returns false if user is not soft deleted', async function () {
      await this.ParanoidUser.create({ username: 'fnord' });
      const users = await this.ParanoidUser.findAll();
      expect(users[0].isSoftDeleted()).to.be.false;
    });

    it('returns true if user is soft deleted', async function () {
      await this.ParanoidUser.create({ username: 'fnord' });
      const users = await this.ParanoidUser.findAll();
      await users[0].destroy();
      expect(users[0].isSoftDeleted()).to.be.true;

      const user = await users[0].reload({ paranoid: false });
      expect(user.isSoftDeleted()).to.be.true;
    });

    it('works with custom `deletedAt` field name', async function () {
      this.ParanoidUserWithCustomDeletedAt = this.sequelize.define(
        'ParanoidUserWithCustomDeletedAt',
        {
          username: { type: DataTypes.STRING },
        },
        {
          deletedAt: 'deletedAtThisTime',
          paranoid: true,
        },
      );

      this.ParanoidUserWithCustomDeletedAt.hasOne(this.ParanoidUser);

      await this.ParanoidUserWithCustomDeletedAt.sync({ force: true });
      await this.ParanoidUserWithCustomDeletedAt.create({ username: 'fnord' });
      const users = await this.ParanoidUserWithCustomDeletedAt.findAll();
      expect(users[0].isSoftDeleted()).to.be.false;

      await users[0].destroy();
      expect(users[0].isSoftDeleted()).to.be.true;

      const user = await users[0].reload({ paranoid: false });
      expect(user.isSoftDeleted()).to.be.true;
    });
  });

  describe('restore', () => {
    it('returns an error if the model is not paranoid', async function () {
      const user = await this.User.create({ username: 'Peter', secretValue: '42' });
      await expect(user.restore()).to.be.rejectedWith(Error, 'Model is not paranoid');
    });

    it('restores a previously deleted model', async function () {
      const ParanoidUser = this.sequelize.define(
        'ParanoidUser',
        {
          username: DataTypes.STRING,
          secretValue: DataTypes.STRING,
          data: DataTypes.STRING,
          intVal: { type: DataTypes.INTEGER, defaultValue: 1 },
        },
        {
          paranoid: true,
        },
      );
      const data = [
        { username: 'Peter', secretValue: '42' },
        { username: 'Paul', secretValue: '43' },
        { username: 'Bob', secretValue: '44' },
      ];

      await ParanoidUser.sync({ force: true });
      await ParanoidUser.bulkCreate(data);
      const user0 = await ParanoidUser.findOne({ where: { secretValue: '42' } });
      await user0.destroy();
      await user0.restore();
      const user = await ParanoidUser.findOne({ where: { secretValue: '42' } });
      expect(user).to.be.ok;
      expect(user.username).to.equal('Peter');
    });

    it('supports custom deletedAt field', async function () {
      const ParanoidUser = this.sequelize.define(
        'ParanoidUser',
        {
          username: DataTypes.STRING,
          destroyTime: DataTypes.DATE,
        },
        { paranoid: true, deletedAt: 'destroyTime' },
      );

      await ParanoidUser.sync({ force: true });

      const user2 = await ParanoidUser.create({
        username: 'username',
      });

      const user1 = await user2.destroy();
      expect(user1.destroyTime).to.be.ok;
      expect(user1.deletedAt).to.not.be.ok;
      const user0 = await user1.restore();
      expect(user0.destroyTime).to.not.be.ok;
      const user = await ParanoidUser.findOne({ where: { username: 'username' } });
      expect(user).to.be.ok;
      expect(user.destroyTime).to.not.be.ok;
      expect(user.deletedAt).to.not.be.ok;
    });

    it('supports custom deletedAt field name', async function () {
      const ParanoidUser = this.sequelize.define(
        'ParanoidUser',
        {
          username: DataTypes.STRING,
          deletedAt: { type: DataTypes.DATE, field: 'deleted_at' },
        },
        { paranoid: true },
      );

      await ParanoidUser.sync({ force: true });

      const user2 = await ParanoidUser.create({
        username: 'username',
      });

      const user1 = await user2.destroy();
      expect(user1.dataValues.deletedAt).to.be.ok;
      expect(user1.dataValues.deleted_at).to.not.be.ok;
      const user0 = await user1.restore();
      expect(user0.dataValues.deletedAt).to.not.be.ok;
      expect(user0.dataValues.deleted_at).to.not.be.ok;
      const user = await ParanoidUser.findOne({ where: { username: 'username' } });
      expect(user).to.be.ok;
      expect(user.deletedAt).to.not.be.ok;
      expect(user.deleted_at).to.not.be.ok;
    });

    it('supports custom deletedAt field and database column', async function () {
      const ParanoidUser = this.sequelize.define(
        'ParanoidUser',
        {
          username: DataTypes.STRING,
          destroyTime: { type: DataTypes.DATE, field: 'destroy_time' },
        },
        { paranoid: true, deletedAt: 'destroyTime' },
      );

      await ParanoidUser.sync({ force: true });

      const user2 = await ParanoidUser.create({
        username: 'username',
      });

      const user1 = await user2.destroy();
      expect(user1.dataValues.destroyTime).to.be.ok;
      expect(user1.dataValues.deletedAt).to.not.be.ok;
      expect(user1.dataValues.destroy_time).to.not.be.ok;
      const user0 = await user1.restore();
      expect(user0.dataValues.destroyTime).to.not.be.ok;
      expect(user0.dataValues.destroy_time).to.not.be.ok;
      const user = await ParanoidUser.findOne({ where: { username: 'username' } });
      expect(user).to.be.ok;
      expect(user.destroyTime).to.not.be.ok;
      expect(user.destroy_time).to.not.be.ok;
    });

    it('supports custom default value', async function () {
      const ParanoidUser = this.sequelize.define(
        'ParanoidUser',
        {
          username: DataTypes.STRING,
          deletedAt: { type: DataTypes.DATE, defaultValue: new Date(0) },
        },
        { paranoid: true },
      );

      await ParanoidUser.sync({ force: true });

      const user2 = await ParanoidUser.create({
        username: 'username',
      });

      const user1 = await user2.destroy();
      const user0 = await user1.restore();
      expect(user0.dataValues.deletedAt.toISOString()).to.equal(new Date(0).toISOString());
      const user = await ParanoidUser.findOne({ where: { username: 'username' } });
      expect(user).to.be.ok;
      expect(user.deletedAt.toISOString()).to.equal(new Date(0).toISOString());
    });
  });
});
