'use strict';

const chai = require('chai');

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

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

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

  beforeEach(async function () {
    this.User = this.sequelize.define('User', {
      id: { type: DataTypes.INTEGER, primaryKey: true },
      aNumber: { type: DataTypes.INTEGER },
      bNumber: { type: DataTypes.INTEGER },
      cNumber: { type: DataTypes.INTEGER, field: 'c_number' },
    });

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

    await this.User.bulkCreate([
      {
        id: 1,
        aNumber: 0,
        bNumber: 0,
      },
      {
        id: 2,
        aNumber: 0,
        bNumber: 0,
      },
      {
        id: 3,
        aNumber: 0,
        bNumber: 0,
      },
      {
        id: 4,
        aNumber: 0,
        bNumber: 0,
        cNumber: 0,
      },
    ]);
  });

  for (const method of ['increment', 'decrement']) {
    describe(method, () => {
      before(function () {
        this.assert = (increment, decrement) => {
          return method === 'increment' ? increment : decrement;
        };
      });

      it('supports where conditions', async function () {
        await this.User.findByPk(1);
        await this.User[method](['aNumber'], { by: 2, where: { id: 1 } });
        const user3 = await this.User.findByPk(2);
        expect(user3.aNumber).to.equal(this.assert(0, 0));
      });

      it('uses correct column names for where conditions', async function () {
        await this.User[method](['aNumber'], { by: 2, where: { cNumber: 0 } });
        const user4 = await this.User.findByPk(4);
        expect(user4.aNumber).to.equal(this.assert(2, -2));
      });

      it('should still work right with other concurrent increments', async function () {
        const aUsers = await this.User.findAll();

        await Promise.all([
          this.User[method](['aNumber'], { by: 2, where: {} }),
          this.User[method](['aNumber'], { by: 2, where: {} }),
          this.User[method](['aNumber'], { by: 2, where: {} }),
        ]);

        const bUsers = await this.User.findAll();
        for (const [i, bUser] of bUsers.entries()) {
          expect(bUser.aNumber).to.equal(this.assert(aUsers[i].aNumber + 6, aUsers[i].aNumber - 6));
        }
      });

      it('with array', async function () {
        const aUsers = await this.User.findAll();
        await this.User[method](['aNumber'], { by: 2, where: {} });
        const bUsers = await this.User.findAll();
        for (const [i, bUser] of bUsers.entries()) {
          expect(bUser.aNumber).to.equal(this.assert(aUsers[i].aNumber + 2, aUsers[i].aNumber - 2));
        }
      });

      it('with single field', async function () {
        const aUsers = await this.User.findAll();
        await this.User[method]('aNumber', { by: 2, where: {} });
        const bUsers = await this.User.findAll();
        for (const [i, bUser] of bUsers.entries()) {
          expect(bUser.aNumber).to.equal(this.assert(aUsers[i].aNumber + 2, aUsers[i].aNumber - 2));
        }
      });

      it('with single field and no value', async function () {
        const aUsers = await this.User.findAll();
        await this.User[method]('aNumber', { where: {} });
        const bUsers = await this.User.findAll();
        for (const [i, bUser] of bUsers.entries()) {
          expect(bUser.aNumber).to.equal(this.assert(aUsers[i].aNumber + 1, aUsers[i].aNumber - 1));
        }
      });

      it('with key value pair', async function () {
        const aUsers = await this.User.findAll();
        await this.User[method]({ aNumber: 1, bNumber: 2 }, { where: {} });
        const bUsers = await this.User.findAll();
        for (const [i, bUser] of bUsers.entries()) {
          expect(bUser.aNumber).to.equal(this.assert(aUsers[i].aNumber + 1, aUsers[i].aNumber - 1));
          expect(bUser.bNumber).to.equal(this.assert(aUsers[i].bNumber + 2, aUsers[i].bNumber - 2));
        }
      });

      it('should still work right with other concurrent updates', async function () {
        const aUsers = await this.User.findAll();
        await this.User.update({ aNumber: 2 }, { where: {} });
        await this.User[method](['aNumber'], { by: 2, where: {} });
        const bUsers = await this.User.findAll();
        for (const [i, bUser] of bUsers.entries()) {
          // for decrement 2 - 2 = 0
          expect(bUser.aNumber).to.equal(this.assert(aUsers[i].aNumber + 4, aUsers[i].aNumber));
        }
      });

      it('with timestamps set to true', async function () {
        const User = this.sequelize.define(
          'IncrementUser',
          {
            aNumber: DataTypes.INTEGER,
          },
          { timestamps: true },
        );

        await User.sync({ force: true });
        const user = await User.create({ aNumber: 1 });
        const oldDate = user.updatedAt;

        this.clock.tick(1000);
        await User[method]('aNumber', { by: 1, where: {} });

        await expect(User.findByPk(1)).to.eventually.have.property('updatedAt').afterTime(oldDate);
      });

      it('with timestamps set to true and options.silent set to true', async function () {
        const User = this.sequelize.define(
          'IncrementUser',
          {
            aNumber: DataTypes.INTEGER,
          },
          { timestamps: true },
        );

        await User.sync({ force: true });
        const user = await User.create({ aNumber: 1 });
        const oldDate = user.updatedAt;
        expect(oldDate).to.be.instanceOf(Date, 'Date from User.create is not a Date');
        this.clock.tick(1000);
        await User[method]('aNumber', { by: 1, silent: true, where: {} });

        const updatedUser = await User.findByPk(1);

        await expect(updatedUser.updatedAt).to.equalTime(oldDate);
      });

      it('should work with scopes', async function () {
        const User = this.sequelize.define(
          'User',
          {
            aNumber: DataTypes.INTEGER,
            name: DataTypes.STRING,
          },
          {
            scopes: {
              jeff: {
                where: {
                  name: 'Jeff',
                },
              },
            },
          },
        );

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

        await User.bulkCreate([
          {
            aNumber: 1,
            name: 'Jeff',
          },
          {
            aNumber: 3,
            name: 'Not Jeff',
          },
        ]);

        await User.withScope('jeff')[method]('aNumber', {});
        const jeff = await User.withScope('jeff').findOne();
        expect(jeff.aNumber).to.equal(this.assert(2, 0));

        const notJeff = await User.findOne({
          where: {
            name: 'Not Jeff',
          },
        });

        expect(notJeff.aNumber).to.equal(this.assert(3, 3));
      });

      it('should not care for attributes in the instance scope', async function () {
        this.User.addScope('test', {
          attributes: ['foo', 'bar'],
        });
        const createdUser = await this.User.withScope('test').create({ id: 5, aNumber: 5 });
        await createdUser[method]('aNumber', { by: 2 });
        const user = await this.User.findByPk(5);
        expect(user.aNumber).to.equal(this.assert(7, 3));
      });
      it('should not care for exclude-attributes in the instance scope', async function () {
        this.User.addScope('test', {
          attributes: { exclude: ['foo', 'bar'] },
        });
        const createdUser = await this.User.withScope('test').create({ id: 5, aNumber: 5 });
        await createdUser[method]('aNumber', { by: 2 });
        const user = await this.User.findByPk(5);
        expect(user.aNumber).to.equal(this.assert(7, 3));
      });
      it('should not care for include-attributes in the instance scope', async function () {
        this.User.addScope('test', {
          attributes: { include: ['foo', 'bar'] },
        });
        const createdUser = await this.User.withScope('test').create({ id: 5, aNumber: 5 });
        await createdUser[method]('aNumber', { by: 2 });
        const user = await this.User.findByPk(5);
        expect(user.aNumber).to.equal(this.assert(7, 3));
      });
    });
  }
});
