'use strict';

const chai = require('chai');
const sinon = require('sinon');

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

const dialect = Support.getTestDialect();

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

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

  describe('attributes', () => {
    describe('field', () => {
      beforeEach(async function () {
        const queryInterface = this.sequelize.queryInterface;

        this.User = this.sequelize.define(
          'user',
          {
            id: {
              type: DataTypes.INTEGER,
              allowNull: false,
              primaryKey: true,
              autoIncrement: true,
              field: 'userId',
            },
            name: {
              type: DataTypes.STRING,
              field: 'full_name',
            },
            taskCount: {
              type: DataTypes.INTEGER,
              field: 'task_count',
              defaultValue: 0,
              allowNull: false,
            },
          },
          {
            tableName: 'users',
            timestamps: false,
          },
        );

        this.Task = this.sequelize.define(
          'task',
          {
            id: {
              type: DataTypes.INTEGER,
              allowNull: false,
              primaryKey: true,
              autoIncrement: true,
              field: 'taskId',
            },
            title: {
              type: DataTypes.STRING,
              field: 'name',
            },
          },
          {
            tableName: 'tasks',
            timestamps: false,
          },
        );

        this.Comment = this.sequelize.define(
          'comment',
          {
            id: {
              type: DataTypes.INTEGER,
              allowNull: false,
              primaryKey: true,
              autoIncrement: true,
              field: 'commentId',
            },
            text: { type: DataTypes.STRING, field: 'comment_text' },
            notes: { type: DataTypes.STRING, field: 'notes' },
            likes: { type: DataTypes.INTEGER, field: 'like_count' },
            createdAt: { field: 'created_at' },
            updatedAt: { field: 'updated_at' },
          },
          {
            tableName: 'comments',
            timestamps: true,
          },
        );

        this.User.hasMany(this.Task, {
          foreignKey: 'user_id',
        });
        this.Task.belongsTo(this.User, {
          foreignKey: 'user_id',
        });
        this.Task.hasMany(this.Comment, {
          foreignKey: 'task_id',
        });
        this.Comment.belongsTo(this.Task, {
          foreignKey: 'task_id',
        });

        this.User.belongsToMany(this.Comment, {
          foreignKey: 'userId',
          otherKey: 'commentId',
          through: 'userComments',
        });

        await Promise.all([
          queryInterface.createTable('users', {
            userId: {
              type: DataTypes.INTEGER,
              allowNull: false,
              primaryKey: true,
              autoIncrement: true,
            },
            full_name: {
              type: DataTypes.STRING,
            },
            task_count: {
              type: DataTypes.INTEGER,
              allowNull: false,
              defaultValue: 0,
            },
          }),
          queryInterface.createTable('tasks', {
            taskId: {
              type: DataTypes.INTEGER,
              allowNull: false,
              primaryKey: true,
              autoIncrement: true,
            },
            user_id: {
              type: DataTypes.INTEGER,
            },
            name: {
              type: DataTypes.STRING,
            },
          }),
          queryInterface.createTable('comments', {
            commentId: {
              type: DataTypes.INTEGER,
              allowNull: false,
              primaryKey: true,
              autoIncrement: true,
            },
            task_id: {
              type: DataTypes.INTEGER,
            },
            comment_text: {
              type: DataTypes.STRING,
            },
            notes: {
              type: DataTypes.STRING,
            },
            like_count: {
              type: DataTypes.INTEGER,
            },
            created_at: {
              type: DataTypes.DATE,
              allowNull: false,
            },
            updated_at: {
              type: DataTypes.DATE,
            },
          }),
          queryInterface.createTable('userComments', {
            commentId: {
              type: DataTypes.INTEGER,
            },
            userId: {
              type: DataTypes.INTEGER,
            },
          }),
        ]);
      });

      describe('primaryKey', () => {
        describe('in combination with allowNull', () => {
          beforeEach(async function () {
            this.ModelUnderTest = this.sequelize.define('ModelUnderTest', {
              identifier: {
                primaryKey: true,
                type: DataTypes.STRING,
                allowNull: false,
              },
            });

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

          it('sets the column to not allow null', async function () {
            const fields = await this.ModelUnderTest.describe();

            expect(fields.identifier).to.include({ allowNull: false });
          });
        });

        it('should support instance.destroy()', async function () {
          const user = await this.User.create();
          await user.destroy();
        });

        it('should support Model.destroy()', async function () {
          const user = await this.User.create();

          await this.User.destroy({
            where: {
              id: user.get('id'),
            },
          });
        });
      });

      describe('field and attribute name is the same', () => {
        beforeEach(async function () {
          await this.Comment.bulkCreate([{ notes: 'Number one' }, { notes: 'Number two' }]);
        });

        it('bulkCreate should work', async function () {
          const comments = await this.Comment.findAll();
          expect(comments[0].notes).to.equal('Number one');
          expect(comments[1].notes).to.equal('Number two');
        });

        it('find with where should work', async function () {
          const comments = await this.Comment.findAll({ where: { notes: 'Number one' } });
          expect(comments).to.have.length(1);
          expect(comments[0].notes).to.equal('Number one');
        });

        it('reload should work', async function () {
          const comment = await this.Comment.findByPk(1);
          await comment.reload();
        });

        it('save should work', async function () {
          const comment1 = await this.Comment.create({ notes: 'my note' });
          comment1.notes = 'new note';
          const comment0 = await comment1.save();
          const comment = await comment0.reload();
          expect(comment.notes).to.equal('new note');
        });
      });

      it('increment should work', async function () {
        await this.Comment.truncate();
        const comment1 = await this.Comment.create({ note: 'oh boy, here I go again', likes: 23 });
        const comment0 = await comment1.increment('likes');
        const comment = await comment0.reload();
        expect(comment.likes).to.equal(24);
      });

      it('decrement should work', async function () {
        await this.Comment.truncate();
        const comment1 = await this.Comment.create({ note: 'oh boy, here I go again', likes: 23 });
        const comment0 = await comment1.decrement('likes');
        const comment = await comment0.reload();
        expect(comment.likes).to.equal(22);
      });

      it('sum should work', async function () {
        await this.Comment.truncate();
        await this.Comment.create({ note: 'oh boy, here I go again', likes: 23 });
        const likes = await this.Comment.sum('likes');
        expect(likes).to.equal(23);
      });

      it('should create, fetch and update with alternative field names from a simple model', async function () {
        await this.User.create({
          name: 'Foobar',
        });

        const user0 = await this.User.findOne({
          limit: 1,
        });

        expect(user0.get('name')).to.equal('Foobar');

        await user0.update({
          name: 'Barfoo',
        });

        const user = await this.User.findOne({
          limit: 1,
        });

        expect(user.get('name')).to.equal('Barfoo');
      });

      it('should bulk update', async function () {
        const Entity = this.sequelize.define('Entity', {
          strField: { type: DataTypes.STRING, field: 'str_field' },
        });

        await this.sequelize.sync({ force: true });
        await Entity.create({ strField: 'foo' });

        await Entity.update({ strField: 'bar' }, { where: { strField: 'foo' } });

        const entity = await Entity.findOne({
          where: {
            strField: 'bar',
          },
        });

        expect(entity).to.be.ok;
        expect(entity.get('strField')).to.equal('bar');
      });

      it('should not contain the field properties after create', async function () {
        const Model = this.sequelize.define(
          'test',
          {
            id: {
              type: DataTypes.INTEGER,
              field: 'test_id',
              autoIncrement: true,
              primaryKey: true,
              validate: {
                min: 1,
              },
            },
            title: {
              allowNull: false,
              type: DataTypes.STRING(255),
              field: 'test_title',
            },
          },
          {
            timestamps: true,
            underscored: true,
            freezeTableName: true,
          },
        );

        await Model.sync({ force: true });
        const data = await Model.create({ title: 'test' });
        expect(data.get('test_title')).to.be.an('undefined');
        expect(data.get('test_id')).to.be.an('undefined');
      });

      it('should make the aliased auto incremented primary key available after create', async function () {
        const user = await this.User.create({
          name: 'Barfoo',
        });

        expect(user.get('id')).to.be.ok;
      });

      it('should work with where on includes for find', async function () {
        const user = await this.User.create({
          name: 'Barfoo',
        });

        const task0 = await user.createTask({
          title: 'DatDo',
        });

        await task0.createComment({
          text: 'Comment',
        });

        const task = await this.Task.findOne({
          include: [{ model: this.Comment }, { model: this.User }],
          where: { title: 'DatDo' },
        });

        expect(task.get('title')).to.equal('DatDo');
        expect(task.get('comments')[0].get('text')).to.equal('Comment');
        expect(task.get('user')).to.be.ok;
      });

      it('should work with where on includes for findAll', async function () {
        const user = await this.User.create({
          name: 'Foobar',
        });

        const task = await user.createTask({
          title: 'DoDat',
        });

        await task.createComment({
          text: 'Comment',
        });

        const users = await this.User.findAll({
          include: [
            {
              model: this.Task,
              where: { title: 'DoDat' },
              include: [{ model: this.Comment }],
            },
          ],
        });

        for (const user of users) {
          expect(user.get('name')).to.be.ok;
          expect(user.get('tasks')[0].get('title')).to.equal('DoDat');
          expect(user.get('tasks')[0].get('comments')).to.be.ok;
        }
      });

      it('should work with increment', async function () {
        const user = await this.User.create();
        await user.increment('taskCount');
      });

      it('should work with a simple where', async function () {
        await this.User.create({
          name: 'Foobar',
        });

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

        expect(user).to.be.ok;
      });

      it('should work with a where or', async function () {
        await this.User.create({
          name: 'Foobar',
        });

        const user = await this.User.findOne({
          where: this.sequelize.or(
            {
              name: 'Foobar',
            },
            {
              name: 'Lollerskates',
            },
          ),
        });

        expect(user).to.be.ok;
      });

      it('should work with bulkCreate and findAll', async function () {
        await this.User.bulkCreate([
          {
            name: 'Abc',
          },
          {
            name: 'Bcd',
          },
          {
            name: 'Cde',
          },
        ]);

        const users = await this.User.findAll();
        for (const user of users) {
          expect(['Abc', 'Bcd', 'Cde'].includes(user.get('name'))).to.be.true;
        }
      });

      it('should support renaming of sequelize method fields', async function () {
        const Test = this.sequelize.define('test', {
          someProperty: DataTypes.VIRTUAL, // Since we specify the AS part as a part of the literal string, not with sequelize syntax, we have to tell sequelize about the field
        });

        await this.sequelize.sync({ force: true });
        await Test.create({});
        let findAttributes;
        if (dialect === 'mssql') {
          findAttributes = [
            Sequelize.literal(
              'CAST(CASE WHEN EXISTS(SELECT 1) THEN 1 ELSE 0 END AS BIT) AS "someProperty"',
            ),
            [
              Sequelize.literal('CAST(CASE WHEN EXISTS(SELECT 1) THEN 1 ELSE 0 END AS BIT)'),
              'someProperty2',
            ],
          ];
        } else if (['db2', 'ibmi'].includes(dialect)) {
          findAttributes = [
            Sequelize.literal('1 AS "someProperty"'),
            [Sequelize.literal('1'), 'someProperty2'],
          ];
        } else {
          findAttributes = [
            Sequelize.literal('EXISTS(SELECT 1) AS "someProperty"'),
            [Sequelize.literal('EXISTS(SELECT 1)'), 'someProperty2'],
          ];
        }

        const tests = await Test.findAll({
          attributes: findAttributes,
        });

        expect(tests[0].get('someProperty')).to.be.ok;
        expect(tests[0].get('someProperty2')).to.be.ok;
      });

      it('should sync foreign keys with custom field names', async function () {
        await this.sequelize.sync({ force: true });
        const attrs = this.Task.tableAttributes;
        expect(attrs.user_id.references.table).to.deep.equal({
          tableName: 'users',
          schema: this.sequelize.dialect.getDefaultSchema(),
          delimiter: '.',
        });
        expect(attrs.user_id.references.key).to.equal('userId');
      });

      it('should find the value of an attribute with a custom field name', async function () {
        await this.User.create({ name: 'test user' });
        const user = await this.User.findOne({ where: { name: 'test user' } });
        expect(user.name).to.equal('test user');
      });

      it('field names that are the same as property names should create, update, and read correctly', async function () {
        await this.Comment.create({
          notes: 'Foobar',
        });

        const comment0 = await this.Comment.findOne({
          limit: 1,
        });

        expect(comment0.get('notes')).to.equal('Foobar');

        await comment0.update({
          notes: 'Barfoo',
        });

        const comment = await this.Comment.findOne({
          limit: 1,
        });

        expect(comment.get('notes')).to.equal('Barfoo');
      });

      it('should work with a belongsTo association getter', async function () {
        const userId = Math.floor(Math.random() * 100_000);

        const [user, task] = await Promise.all([
          this.User.create({
            id: userId,
          }),
          this.Task.create({
            user_id: userId,
          }),
        ]);

        const [userA, userB] = await Promise.all([user, task.getUser()]);
        expect(userA.get('id')).to.equal(userB.get('id'));
        expect(userA.get('id')).to.equal(userId);
        expect(userB.get('id')).to.equal(userId);
      });

      it('should work with paranoid instance.destroy()', async function () {
        const User = this.sequelize.define(
          'User',
          {
            deletedAt: {
              type: DataTypes.DATE,
              field: 'deleted_at',
            },
          },
          {
            timestamps: true,
            paranoid: true,
          },
        );

        await User.sync({ force: true });
        const user = await User.create();
        await user.destroy();
        this.clock.tick(1000);
        const users = await User.findAll();
        expect(users.length).to.equal(0);
      });

      it('should work with paranoid Model.destroy()', async function () {
        const User = this.sequelize.define(
          'User',
          {
            deletedAt: {
              type: DataTypes.DATE,
              field: 'deleted_at',
            },
          },
          {
            timestamps: true,
            paranoid: true,
          },
        );

        await User.sync({ force: true });
        const user = await User.create();
        await User.destroy({ where: { id: user.get('id') } });
        const users = await User.findAll();
        expect(users.length).to.equal(0);
      });

      it('should work with `belongsToMany` association `count`', async function () {
        const user = await this.User.create({
          name: 'John',
        });

        const commentCount = await user.countComments();
        await expect(commentCount).to.equal(0);
      });

      it('should work with `hasMany` association `count`', async function () {
        const user = await this.User.create({
          name: 'John',
        });

        const taskCount = await user.countTasks();
        await expect(taskCount).to.equal(0);
      });
    });
  });
});
