'use strict';

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

const expect = chai.expect;
const Support = require('../support');

const { DataTypes, Op, Sequelize } = require('@sequelize/core');
const pMap = require('p-map');

const current = Support.sequelize;
const dialect = current.dialect;
const dialectName = Support.getTestDialect();

describe('Model.findOne', () => {
  beforeEach(async function () {
    this.User = this.sequelize.define('User', {
      username: DataTypes.STRING,
      secretValue: DataTypes.STRING,
      data: DataTypes.STRING,
      intVal: DataTypes.INTEGER,
      theDate: DataTypes.DATE,
      aBool: DataTypes.BOOLEAN,
    });

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

  describe('findOne', () => {
    if (current.dialect.supports.transactions) {
      it('supports transactions', async function () {
        const sequelize = await Support.createSingleTransactionalTestSequelizeInstance(
          this.sequelize,
        );
        const User = sequelize.define('User', { username: DataTypes.STRING });

        await User.sync({ force: true });
        const t = await sequelize.startUnmanagedTransaction();
        await User.create({ username: 'foo' }, { transaction: t });

        const user1 = await User.findOne({
          where: { username: 'foo' },
        });

        const user2 = await User.findOne({
          where: { username: 'foo' },
          transaction: t,
        });

        expect(user1).to.be.null;
        expect(user2).to.not.be.null;
        await t.rollback();
      });

      it('supports concurrent transactions', async function () {
        // Disabled in sqlite3 because it only supports one write transaction at a time
        if (dialectName === 'sqlite3') {
          return;
        }

        this.timeout(90_000);
        const sequelize = await Support.createSingleTransactionalTestSequelizeInstance(
          this.sequelize,
        );
        const User = sequelize.define('User', { username: DataTypes.STRING });
        const testAsync = async function () {
          const t0 = await sequelize.startUnmanagedTransaction();

          await User.create(
            {
              username: 'foo',
            },
            {
              transaction: t0,
            },
          );

          const users0 = await User.findAll({
            where: {
              username: 'foo',
            },
          });

          expect(users0).to.have.length(0);

          const users = await User.findAll({
            where: {
              username: 'foo',
            },
            transaction: t0,
          });

          expect(users).to.have.length(1);
          const t = t0;

          return t.rollback();
        };

        await User.sync({ force: true });
        const tasks = [];
        for (let i = 0; i < 1000; i++) {
          tasks.push(testAsync);
        }

        await pMap(
          tasks,
          entry => {
            return entry();
          },
          {
            // Needs to be one less than ??? else the non transaction query won't ever get a connection
            concurrency: (sequelize.rawOptions.pool?.max || 5) - 1,
          },
        );
      });
    }

    describe('general / basic function', () => {
      beforeEach(async function () {
        const user = await this.User.create({ username: 'barfooz' });
        this.UserPrimary = this.sequelize.define('UserPrimary', {
          specialkey: {
            type: DataTypes.STRING,
            primaryKey: true,
          },
        });

        await this.UserPrimary.sync({ force: true });
        await this.UserPrimary.create({ specialkey: 'a string' });
        this.user = user;
      });

      if (dialectName === 'mysql') {
        // Bit fields interpreted as boolean need conversion from buffer / bool.
        // Sqlite returns the inserted value as is, and postgres really should the built in bool type instead

        it('allows bit fields as booleans', async function () {
          let bitUser = this.sequelize.define(
            'bituser',
            {
              bool: 'BIT(1)',
            },
            {
              timestamps: false,
            },
          );

          // First use a custom data type def to create the bit field
          await bitUser.sync({ force: true });
          // Then change the definition to BOOLEAN
          bitUser = this.sequelize.define(
            'bituser',
            {
              bool: DataTypes.BOOLEAN,
            },
            {
              timestamps: false,
            },
          );

          await bitUser.bulkCreate([{ bool: false }, { bool: true }]);

          const bitUsers = await bitUser.findAll();
          expect(bitUsers[0].bool).not.to.be.ok;
          expect(bitUsers[1].bool).to.be.ok;
        });
      }

      it('treats questionmarks in an array', async function () {
        let test = false;

        await this.UserPrimary.findOne({
          where: { specialkey: 'awesome' },
          logging(sql) {
            test = true;
            expect(sql).to.match(
              /WHERE ["[`|]UserPrimary["\]`|]\.["[`|]specialkey["\]`|] = N?'awesome'/,
            );
          },
        });

        expect(test).to.be.true;
      });

      it("doesn't throw an error when entering in a non integer value for a specified primary field", async function () {
        const user = await this.UserPrimary.findByPk('a string');
        expect(user.specialkey).to.equal('a string');
      });

      it('returns a single dao', async function () {
        const user = await this.User.findByPk(this.user.id);
        expect(Array.isArray(user)).to.not.be.ok;
        expect(user.id).to.equal(this.user.id);
        expect(user.id).to.equal(1);
      });

      it('returns a single dao given a string id', async function () {
        const user = await this.User.findByPk(this.user.id.toString());
        expect(Array.isArray(user)).to.not.be.ok;
        expect(user.id).to.equal(this.user.id);
        expect(user.id).to.equal(1);
      });

      it('should make aliased attributes available', async function () {
        const user = await this.User.findOne({
          where: { id: 1 },
          attributes: ['id', ['username', 'name']],
        });

        expect(user.dataValues.name).to.equal('barfooz');
      });

      it('should fail with meaningful error message on invalid attributes definition', function () {
        expect(
          this.User.findOne({
            where: { id: 1 },
            attributes: ['id', ['username']],
          }),
        ).to.be.rejectedWith(
          "[\"username\"] is not a valid attribute definition. Please use the following format: ['attribute definition', 'alias']",
        );
      });

      it('should not try to convert boolean values if they are not selected', async function () {
        const UserWithBoolean = this.sequelize.define('UserBoolean', {
          active: DataTypes.BOOLEAN,
        });

        await UserWithBoolean.sync({ force: true });
        const user = await UserWithBoolean.create({ active: true });
        const user0 = await UserWithBoolean.findOne({ where: { id: user.id }, attributes: ['id'] });
        expect(user0.active).not.to.exist;
      });

      it('finds a specific user via where option', async function () {
        const user = await this.User.findOne({ where: { username: 'barfooz' } });
        expect(user.username).to.equal('barfooz');
      });

      it("doesn't find a user if conditions are not matching", async function () {
        const user = await this.User.findOne({ where: { username: 'foo' } });
        expect(user).to.be.null;
      });

      it('allows sql logging', async function () {
        let test = false;

        await this.User.findOne({
          where: { username: 'foo' },
          logging(sql) {
            test = true;
            expect(sql).to.exist;
            expect(sql.toUpperCase()).to.include('SELECT');
          },
        });

        expect(test).to.be.true;
      });

      it('ignores passed limit option', async function () {
        const user = await this.User.findOne({ limit: 10 });
        // it returns an object instead of an array
        expect(Array.isArray(user)).to.not.be.ok;
        expect(user.dataValues.hasOwnProperty('username')).to.be.ok;
      });

      it('finds entries via primary keys', async function () {
        const UserPrimary = this.sequelize.define('UserWithPrimaryKey', {
          identifier: { type: DataTypes.STRING, primaryKey: true },
          name: DataTypes.STRING,
        });

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

        const u = await UserPrimary.create({
          identifier: 'an identifier',
          name: 'John',
        });

        expect(u.id).not.to.exist;
        const u2 = await UserPrimary.findByPk('an identifier');
        expect(u2.identifier).to.equal('an identifier');
        expect(u2.name).to.equal('John');
      });

      it('finds entries via a string primary key called id', async function () {
        const UserPrimary = this.sequelize.define('UserWithPrimaryKey', {
          id: { type: DataTypes.STRING, primaryKey: true },
          name: DataTypes.STRING,
        });

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

        await UserPrimary.create({
          id: 'a string based id',
          name: 'Johnno',
        });

        const u2 = await UserPrimary.findByPk('a string based id');
        expect(u2.id).to.equal('a string based id');
        expect(u2.name).to.equal('Johnno');
      });

      if (current.dialect.supports.dataTypes.BIGINT) {
        it('finds entries via a bigint primary key called id', async function () {
          const UserPrimary = this.sequelize.define('UserWithPrimaryKey', {
            id: { type: DataTypes.BIGINT, primaryKey: true },
            name: DataTypes.STRING,
          });

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

          await UserPrimary.create({
            id: 9_007_199_254_740_993n, // Number.MAX_SAFE_INTEGER + 2 (cannot be represented exactly as a number in JS)
            name: 'Johnno',
          });

          const u2 = await UserPrimary.findByPk(9_007_199_254_740_993n);
          expect(u2.name).to.equal('Johnno');

          // Getting the value back as bigint is not supported yet: https://github.com/sequelize/sequelize/issues/14296
          // With most dialects we'll receive a string, but in some cases we have to be a bit creative to prove that we did get hold of the right record:
          if (dialectName === 'db2') {
            // ibm_db 2.7.4+ returns BIGINT values as JS numbers, which leads to a loss of precision:
            // https://github.com/ibmdb/node-ibm_db/issues/816
            // It means that u2.id comes back as 9_007_199_254_740_992 here :(
            // Hopefully this will be fixed soon.
            // For now we can do a separate query where we stringify the value to prove that it did get stored correctly:
            const [[{ stringifiedId }]] = await this.sequelize.query(
              `select "id"::varchar as "stringifiedId" from "${UserPrimary.tableName}" where "id" = 9007199254740993`,
            );
            expect(stringifiedId).to.equal('9007199254740993');
          } else if (dialectName === 'mariadb') {
            // With our current default config, the mariadb driver sends back a Long instance.
            // Updating the mariadb dev dep and passing "supportBigInt: true" would get it back as a bigint,
            // but that's potentially a big change.
            // For now, we'll just stringify the Long and make the comparison:
            expect(u2.id.toString()).to.equal('9007199254740993');
          } else {
            expect(u2.id).to.equal('9007199254740993');
          }
        });
      }

      it('always honors ZERO as primary key', async function () {
        const permutations = [0, '0'];
        let count = 0;

        await this.User.bulkCreate([{ username: 'jack' }, { username: 'jack' }]);

        await Promise.all(
          permutations.map(async perm => {
            const user = await this.User.findByPk(perm, {
              logging(s) {
                expect(s).to.include(0);
                count++;
              },
            });

            expect(user).to.be.null;
          }),
        );

        expect(count).to.equal(permutations.length);
      });

      it('should allow us to find IDs using capital letters', async function () {
        const User = this.sequelize.define(`User${Support.rand()}`, {
          ID: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
          Login: { type: DataTypes.STRING },
        });

        await User.sync({ force: true });
        await User.create({ Login: 'foo' });
        const user = await User.findByPk(1);
        expect(user).to.exist;
        expect(user.ID).to.equal(1);
      });

      if (dialect.supports.dataTypes.CITEXT) {
        it('should allow case-insensitive find on CITEXT type', async function () {
          const User = this.sequelize.define('UserWithCaseInsensitiveName', {
            username: DataTypes.CITEXT,
          });

          await User.sync({ force: true });
          await User.create({ username: 'longUserNAME' });
          const user = await User.findOne({ where: { username: 'LONGusername' } });
          expect(user).to.exist;
          expect(user.username).to.equal('longUserNAME');
        });
      }

      if (dialectName === 'postgres') {
        it('should allow case-sensitive find on TSVECTOR type', async function () {
          const User = this.sequelize.define('UserWithCaseInsensitiveName', {
            username: DataTypes.TSVECTOR,
          });

          await User.sync({ force: true });
          await User.create({ username: 'longUserNAME' });
          const user = await User.findOne({
            where: { username: 'longUserNAME' },
          });
          expect(user).to.exist;
          expect(user.username).to.equal("'longUserNAME'");
        });
      }

      it('should not fail if model is paranoid and where is an empty array', async function () {
        const User = this.sequelize.define(
          'User',
          { username: DataTypes.STRING },
          { paranoid: true },
        );

        await User.sync({ force: true });
        await User.create({ username: 'A fancy name' });
        expect((await User.findOne({ where: [] })).username).to.equal('A fancy name');
      });

      it('should work if model is paranoid and only operator in where clause is a Symbol (#8406)', async function () {
        const User = this.sequelize.define(
          'User',
          { username: DataTypes.STRING },
          { paranoid: true },
        );

        await User.sync({ force: true });
        await User.create({ username: 'foo' });
        expect(
          await User.findOne({
            where: {
              [Op.or]: [{ username: 'bar' }, { username: 'baz' }],
            },
          }),
        ).to.not.be.ok;
      });
    });

    describe('eager loading', () => {
      beforeEach(function () {
        this.Task = this.sequelize.define('Task', { title: DataTypes.STRING });
        this.Worker = this.sequelize.define('Worker', { name: DataTypes.STRING });

        this.init = async function (callback) {
          await this.sequelize.sync({ force: true });
          const worker = await this.Worker.create({ name: 'worker' });
          const task = await this.Task.create({ title: 'homework' });
          this.worker = worker;
          this.task = task;

          return callback();
        };
      });

      describe('belongsTo', () => {
        describe('generic', () => {
          it('throws an error about unexpected input if include contains a non-object', async function () {
            try {
              await this.Worker.findOne({ include: [1] });
            } catch (error) {
              expect(error.message).to
                .equal(`Invalid Include received. Include has to be either a Model, an Association, the name of an association, or a plain object compatible with IncludeOptions.
Got { association: 1 } instead`);
            }
          });

          it('throws an error if included DaoFactory is not associated', async function () {
            try {
              await this.Worker.findOne({ include: [this.Task] });
            } catch (error) {
              expect(error.message).to.equal(
                'Invalid Include received: no associations exist between "Worker" and "Task"',
              );
            }
          });

          it('returns the associated worker via task.worker', async function () {
            this.Task.belongsTo(this.Worker);

            await this.init(async () => {
              await this.task.setWorker(this.worker);

              const task = await this.Task.findOne({
                where: { title: 'homework' },
                include: [this.Worker],
              });

              expect(task).to.exist;
              expect(task.worker).to.exist;
              expect(task.worker.name).to.equal('worker');
            });
          });
        });

        it('returns the private and public ip', async function () {
          const ctx = Object.create(this);
          ctx.Domain = ctx.sequelize.define('Domain', { ip: DataTypes.STRING });
          ctx.Environment = ctx.sequelize.define('Environment', { name: DataTypes.STRING });
          ctx.Environment.belongsTo(ctx.Domain, {
            as: 'PrivateDomain',
            foreignKey: 'privateDomainId',
          });
          ctx.Environment.belongsTo(ctx.Domain, {
            as: 'PublicDomain',
            foreignKey: 'publicDomainId',
          });

          await ctx.Domain.sync({ force: true });
          await ctx.Environment.sync({ force: true });
          const privateIp = await ctx.Domain.create({ ip: '192.168.0.1' });
          const publicIp = await ctx.Domain.create({ ip: '91.65.189.19' });
          const env = await ctx.Environment.create({ name: 'environment' });
          await env.setPrivateDomain(privateIp);
          await env.setPublicDomain(publicIp);

          const environment = await ctx.Environment.findOne({
            where: { name: 'environment' },
            include: [
              { model: ctx.Domain, as: 'PrivateDomain' },
              { model: ctx.Domain, as: 'PublicDomain' },
            ],
          });

          expect(environment).to.exist;
          expect(environment.PrivateDomain).to.exist;
          expect(environment.PrivateDomain.ip).to.equal('192.168.0.1');
          expect(environment.PublicDomain).to.exist;
          expect(environment.PublicDomain.ip).to.equal('91.65.189.19');
        });

        it('eager loads with non-id primary keys', async function () {
          this.User = this.sequelize.define('UserPKeagerbelong', {
            username: {
              type: DataTypes.STRING,
              primaryKey: true,
            },
          });
          this.Group = this.sequelize.define('GroupPKeagerbelong', {
            name: {
              type: DataTypes.STRING,
              primaryKey: true,
            },
          });
          this.User.belongsTo(this.Group);

          await this.sequelize.sync({ force: true });
          await this.Group.create({ name: 'people' });
          await this.User.create({ username: 'someone', groupPKeagerbelongName: 'people' });

          const someUser = await this.User.findOne({
            where: {
              username: 'someone',
            },
            include: [this.Group],
          });

          expect(someUser).to.exist;
          expect(someUser.username).to.equal('someone');
          expect(someUser.groupPKeagerbelong.name).to.equal('people');
        });

        it('getting parent data in many to one relationship', async function () {
          const User = this.sequelize.define('User', {
            id: { type: DataTypes.INTEGER, autoIncrement: true, primaryKey: true },
            username: { type: DataTypes.STRING },
          });

          const Message = this.sequelize.define('Message', {
            id: { type: DataTypes.INTEGER, autoIncrement: true, primaryKey: true },
            user_id: { type: DataTypes.INTEGER },
            message: { type: DataTypes.STRING },
          });

          User.hasMany(Message, { foreignKey: 'user_id' });

          await this.sequelize.sync({ force: true });
          const user = await User.create({ username: 'test_testerson' });
          await Message.create({ user_id: user.id, message: 'hi there!' });
          await Message.create({ user_id: user.id, message: 'a second message' });

          const messages = await Message.findAll({
            where: { user_id: user.id },
            attributes: ['user_id', 'message'],
            include: [{ model: User, attributes: ['username'] }],
          });

          expect(messages.length).to.equal(2);

          expect(messages[0].message).to.equal('hi there!');
          expect(messages[0].user.username).to.equal('test_testerson');

          expect(messages[1].message).to.equal('a second message');
          expect(messages[1].user.username).to.equal('test_testerson');
        });

        it('allows mulitple assocations of the same model with different alias', async function () {
          this.Worker.belongsTo(this.Task, { as: 'ToDo' });
          this.Worker.belongsTo(this.Task, { as: 'DoTo' });

          await this.init(() => {
            return this.Worker.findOne({
              include: [
                { model: this.Task, as: 'ToDo' },
                { model: this.Task, as: 'DoTo' },
              ],
            });
          });
        });
      });

      describe('hasOne', () => {
        beforeEach(async function () {
          this.Worker.hasOne(this.Task);

          await this.init(() => {
            return this.worker.setTask(this.task);
          });
        });

        it('throws an error if included DaoFactory is not associated', async function () {
          try {
            await this.Task.findOne({ include: [this.Worker] });
          } catch (error) {
            expect(error.message).to.equal(
              'Invalid Include received: no associations exist between "Task" and "Worker"',
            );
          }
        });

        it('returns the associated task via worker.task', async function () {
          const worker = await this.Worker.findOne({
            where: { name: 'worker' },
            include: [this.Task],
          });

          expect(worker).to.exist;
          expect(worker.task).to.exist;
          expect(worker.task.title).to.equal('homework');
        });

        it('eager loads with non-id primary keys', async function () {
          this.User = this.sequelize.define('UserPKeagerone', {
            username: {
              type: DataTypes.STRING,
              primaryKey: true,
            },
          });
          this.Group = this.sequelize.define('GroupPKeagerone', {
            name: {
              type: DataTypes.STRING,
              primaryKey: true,
            },
          });
          this.Group.hasOne(this.User);

          await this.sequelize.sync({ force: true });
          await this.Group.create({ name: 'people' });
          await this.User.create({ username: 'someone', groupPKeageroneName: 'people' });

          const someGroup = await this.Group.findOne({
            where: {
              name: 'people',
            },
            include: [this.User],
          });

          expect(someGroup).to.exist;
          expect(someGroup.name).to.equal('people');
          expect(someGroup.userPKeagerone.username).to.equal('someone');
        });
      });

      describe('hasOne with alias', () => {
        it('throws an error if included DaoFactory is not referenced by alias', async function () {
          try {
            await this.Worker.findOne({ include: [this.Task] });
          } catch (error) {
            expect(error.message).to.equal(
              'Invalid Include received: no associations exist between "Worker" and "Task"',
            );
          }
        });

        describe('alias', () => {
          beforeEach(async function () {
            this.Worker.hasOne(this.Task, { as: 'ToDo' });

            await this.init(() => {
              return this.worker.setToDo(this.task);
            });
          });

          it("throws an error indicating an incorrect alias was entered if an association and alias exist but the alias doesn't match", async function () {
            try {
              await this.Worker.findOne({ include: [{ model: this.Task, as: 'Work' }] });
            } catch (error) {
              expect(error.message).to
                .equal(`Association with alias "Work" does not exist on Worker.
The following associations are defined on "Worker": "ToDo"`);
            }
          });

          it('returns the associated task via worker.task', async function () {
            const worker = await this.Worker.findOne({
              where: { name: 'worker' },
              include: [{ model: this.Task, as: 'ToDo' }],
            });

            expect(worker).to.exist;
            expect(worker.ToDo).to.exist;
            expect(worker.ToDo.title).to.equal('homework');
          });

          it('returns the associated task via worker.task when daoFactory is aliased with model', async function () {
            const worker = await this.Worker.findOne({
              where: { name: 'worker' },
              include: [{ model: this.Task, as: 'ToDo' }],
            });

            expect(worker.ToDo.title).to.equal('homework');
          });

          it('allows mulitple assocations of the same model with different alias', async function () {
            this.Worker.hasOne(this.Task, { as: 'DoTo' });

            await this.init(() => {
              return this.Worker.findOne({
                include: [
                  { model: this.Task, as: 'ToDo' },
                  { model: this.Task, as: 'DoTo' },
                ],
              });
            });
          });
        });
      });

      describe('hasMany', () => {
        beforeEach(async function () {
          this.Worker.hasMany(this.Task);

          await this.init(() => {
            return this.worker.setTasks([this.task]);
          });
        });

        it('throws an error if included DaoFactory is not associated', async function () {
          try {
            await this.Task.findOne({ include: [this.Worker] });
          } catch (error) {
            expect(error.message).to.equal(
              'Invalid Include received: no associations exist between "Task" and "Worker"',
            );
          }
        });

        it('returns the associated tasks via worker.tasks', async function () {
          const worker = await this.Worker.findOne({
            where: { name: 'worker' },
            include: [this.Task],
          });

          expect(worker).to.exist;
          expect(worker.tasks).to.exist;
          expect(worker.tasks[0].title).to.equal('homework');
        });

        it('including two has many relations should not result in duplicate values', async function () {
          this.Contact = this.sequelize.define('Contact', { name: DataTypes.STRING });
          this.Photo = this.sequelize.define('Photo', { img: DataTypes.TEXT });
          this.PhoneNumber = this.sequelize.define('PhoneNumber', { phone: DataTypes.TEXT });

          this.Contact.hasMany(this.Photo, { as: 'Photos' });
          this.Contact.hasMany(this.PhoneNumber);

          await this.sequelize.sync({ force: true });
          const someContact = await this.Contact.create({ name: 'Boris' });
          const somePhoto = await this.Photo.create({ img: 'img.jpg' });
          const somePhone1 = await this.PhoneNumber.create({ phone: '000000' });
          const somePhone2 = await this.PhoneNumber.create({ phone: '111111' });
          await someContact.setPhotos([somePhoto]);
          await someContact.setPhoneNumbers([somePhone1, somePhone2]);

          const fetchedContact = await this.Contact.findOne({
            where: {
              name: 'Boris',
            },
            include: [this.PhoneNumber, { model: this.Photo, as: 'Photos' }],
          });

          expect(fetchedContact).to.exist;
          expect(fetchedContact.Photos.length).to.equal(1);
          expect(fetchedContact.phoneNumbers.length).to.equal(2);
        });

        it('eager loads with non-id primary keys', async function () {
          this.User = this.sequelize.define('UserPKeagerone', {
            username: {
              type: DataTypes.STRING,
              primaryKey: true,
            },
          });
          this.Group = this.sequelize.define('GroupPKeagerone', {
            name: {
              type: DataTypes.STRING,
              primaryKey: true,
            },
          });
          this.Group.belongsToMany(this.User, { through: 'group_user' });
          this.User.belongsToMany(this.Group, { through: 'group_user' });

          await this.sequelize.sync({ force: true });
          const someUser = await this.User.create({ username: 'someone' });
          const someGroup = await this.Group.create({ name: 'people' });
          await someUser.setGroupPKeagerones([someGroup]);

          const someUser0 = await this.User.findOne({
            where: {
              username: 'someone',
            },
            include: [this.Group],
          });

          expect(someUser0).to.exist;
          expect(someUser0.username).to.equal('someone');
          expect(someUser0.groupPKeagerones[0].name).to.equal('people');
        });
      });

      describe('hasMany with alias', () => {
        it('throws an error if included DaoFactory is not referenced by alias', async function () {
          try {
            await this.Worker.findOne({ include: [this.Task] });
          } catch (error) {
            expect(error.message).to.equal(
              'Invalid Include received: no associations exist between "Worker" and "Task"',
            );
          }
        });

        describe('alias', () => {
          beforeEach(async function () {
            this.Worker.hasMany(this.Task, { as: 'ToDos' });

            await this.init(() => {
              return this.worker.setToDos([this.task]);
            });
          });

          it("throws an error indicating an incorrect alias was entered if an association and alias exist but the alias doesn't match", async function () {
            try {
              await this.Worker.findOne({ include: [{ model: this.Task, as: 'Work' }] });
            } catch (error) {
              expect(error.message).to
                .equal(`Association with alias "Work" does not exist on Worker.
The following associations are defined on "Worker": "ToDos"`);
            }
          });

          it('returns the associated task via worker.task', async function () {
            const worker = await this.Worker.findOne({
              where: { name: 'worker' },
              include: [{ model: this.Task, as: 'ToDos' }],
            });

            expect(worker).to.exist;
            expect(worker.ToDos).to.exist;
            expect(worker.ToDos[0].title).to.equal('homework');
          });

          it('returns the associated task via worker.task when daoFactory is aliased with model', async function () {
            const worker = await this.Worker.findOne({
              where: { name: 'worker' },
              include: [{ model: this.Task, as: 'ToDos' }],
            });

            expect(worker.ToDos[0].title).to.equal('homework');
          });

          it('allows mulitple assocations of the same model with different alias', async function () {
            this.Worker.hasMany(this.Task, { as: 'DoTos' });

            await this.init(() => {
              return this.Worker.findOne({
                include: [
                  { model: this.Task, as: 'ToDos' },
                  { model: this.Task, as: 'DoTos' },
                ],
              });
            });
          });
        });
      });

      describe('hasMany (N:M) with alias', () => {
        beforeEach(function () {
          this.Product = this.sequelize.define('Product', { title: DataTypes.STRING });
          this.Tag = this.sequelize.define('Tag', { name: DataTypes.STRING });
        });

        it('returns the associated models when using through as string and alias', async function () {
          this.Product.belongsToMany(this.Tag, { as: 'tags', through: 'product_tag' });
          this.Tag.belongsToMany(this.Product, { as: 'products', through: 'product_tag' });

          await this.sequelize.sync();

          await Promise.all([
            this.Product.bulkCreate([
              { title: 'Chair' },
              { title: 'Desk' },
              { title: 'Handbag' },
              { title: 'Dress' },
              { title: 'Jan' },
            ]),
            this.Tag.bulkCreate([{ name: 'Furniture' }, { name: 'Clothing' }, { name: 'People' }]),
          ]);

          const [products, tags] = await Promise.all([this.Product.findAll(), this.Tag.findAll()]);

          this.products = products;
          this.tags = tags;

          await Promise.all([
            products[0].setTags([tags[0], tags[1]]),
            products[1].addTag(tags[0]),
            products[2].addTag(tags[1]),
            products[3].setTags([tags[1]]),
            products[4].setTags([tags[2]]),
          ]);

          await Promise.all([
            (async () => {
              const tag = await this.Tag.findOne({
                where: {
                  id: tags[0].id,
                },
                include: [{ model: this.Product, as: 'products' }],
              });

              expect(tag).to.exist;
              expect(tag.products.length).to.equal(2);
            })(),
            tags[1].getProducts().then(products => {
              expect(products.length).to.equal(3);
            }),
            (async () => {
              const product = await this.Product.findOne({
                where: {
                  id: products[0].id,
                },
                include: [{ model: this.Tag, as: 'tags' }],
              });

              expect(product).to.exist;
              expect(product.tags.length).to.equal(2);
            })(),
            products[1].getTags().then(tags => {
              expect(tags.length).to.equal(1);
            }),
          ]);
        });

        it('returns the associated models when using through as model and alias', async function () {
          // Exactly the same code as the previous test, just with a through model instance, and promisified
          const ProductTag = this.sequelize.define('product_tag');

          this.Product.belongsToMany(this.Tag, { as: 'tags', through: ProductTag });
          this.Tag.belongsToMany(this.Product, { as: 'products', through: ProductTag });

          await this.sequelize.sync();

          await Promise.all([
            this.Product.bulkCreate([
              { title: 'Chair' },
              { title: 'Desk' },
              { title: 'Handbag' },
              { title: 'Dress' },
              { title: 'Jan' },
            ]),
            this.Tag.bulkCreate([{ name: 'Furniture' }, { name: 'Clothing' }, { name: 'People' }]),
          ]);

          const [products, tags] = await Promise.all([this.Product.findAll(), this.Tag.findAll()]);

          this.products = products;
          this.tags = tags;

          await Promise.all([
            products[0].setTags([tags[0], tags[1]]),
            products[1].addTag(tags[0]),
            products[2].addTag(tags[1]),
            products[3].setTags([tags[1]]),
            products[4].setTags([tags[2]]),
          ]);

          await Promise.all([
            expect(
              this.Tag.findOne({
                where: {
                  id: this.tags[0].id,
                },
                include: [{ model: this.Product, as: 'products' }],
              }),
            )
              .to.eventually.have.property('products')
              .to.have.length(2),
            expect(
              this.Product.findOne({
                where: {
                  id: this.products[0].id,
                },
                include: [{ model: this.Tag, as: 'tags' }],
              }),
            )
              .to.eventually.have.property('tags')
              .to.have.length(2),
            expect(this.tags[1].getProducts()).to.eventually.have.length(3),
            expect(this.products[1].getTags()).to.eventually.have.length(1),
          ]);
        });
      });
    });

    describe('queryOptions', () => {
      beforeEach(async function () {
        const user = await this.User.create({ username: 'barfooz' });
        this.user = user;
      });

      it('should return a DAO when queryOptions are not set', async function () {
        const user = await this.User.findOne({ where: { username: 'barfooz' } });
        expect(user).to.be.instanceOf(this.User);
      });

      it('should return a DAO when raw is false', async function () {
        const user = await this.User.findOne({ where: { username: 'barfooz' }, raw: false });
        expect(user).to.be.instanceOf(this.User);
      });

      it('should return raw data when raw is true', async function () {
        const user = await this.User.findOne({ where: { username: 'barfooz' }, raw: true });
        expect(user).to.not.be.instanceOf(this.User);
        expect(user).to.be.instanceOf(Object);
      });
    });

    it('should support logging', async function () {
      const spy = sinon.spy();

      await this.User.findOne({
        where: {},
        logging: spy,
      });

      expect(spy.called).to.be.ok;
    });

    describe('rejectOnEmpty mode', () => {
      it('throws error when record not found by findOne', async function () {
        await expect(
          this.User.findOne({
            where: {
              username: 'ath-kantam-pradakshnami',
            },
            rejectOnEmpty: true,
          }),
        ).to.eventually.be.rejectedWith(Sequelize.EmptyResultError);
      });

      it('throws error when record not found by findByPk', async function () {
        await expect(
          this.User.findByPk(2, {
            rejectOnEmpty: true,
          }),
        ).to.eventually.be.rejectedWith(Sequelize.EmptyResultError);
      });

      it('throws error when record not found by find', async function () {
        await expect(
          this.User.findOne({
            where: {
              username: 'some-username-that-is-not-used-anywhere',
            },
            rejectOnEmpty: true,
          }),
        ).to.eventually.be.rejectedWith(Sequelize.EmptyResultError);
      });

      it('works from model options', async () => {
        const Model = current.define(
          'Test',
          {
            username: DataTypes.STRING(100),
          },
          {
            rejectOnEmpty: true,
          },
        );

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

        await expect(
          Model.findOne({
            where: {
              username: 'some-username-that-is-not-used-anywhere',
            },
          }),
        ).to.eventually.be.rejectedWith(Sequelize.EmptyResultError);
      });

      it('override model options', async () => {
        const Model = current.define(
          'Test',
          {
            username: DataTypes.STRING(100),
          },
          {
            rejectOnEmpty: true,
          },
        );

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

        await expect(
          Model.findOne({
            rejectOnEmpty: false,
            where: {
              username: 'some-username-that-is-not-used-anywhere',
            },
          }),
        ).to.eventually.be.deep.equal(null);
      });

      it('resolve null when disabled', async () => {
        const Model = current.define('Test', {
          username: DataTypes.STRING(100),
        });

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

        await expect(
          Model.findOne({
            where: {
              username: 'some-username-that-is-not-used-anywhere-for-sure-this-time',
            },
          }),
        ).to.eventually.be.equal(null);
      });
    });
  });
});
