'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 assert = require('node:assert');

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

describe(Support.getTestDialectTeaser('BelongsTo'), () => {
  describe('Model.associations', () => {
    it('should store all associations when associating to the same table multiple times', function () {
      const User = this.sequelize.define('User', {});
      const Group = this.sequelize.define('Group', {});

      Group.belongsTo(User);
      Group.belongsTo(User, { foreignKey: 'primaryGroupId', as: 'primaryUsers' });
      Group.belongsTo(User, { foreignKey: 'secondaryGroupId', as: 'secondaryUsers' });

      expect(Object.keys(Group.associations)).to.deep.equal([
        'user',
        'primaryUsers',
        'secondaryUsers',
      ]);
    });
  });

  describe('get', () => {
    describe('multiple', () => {
      it('should fetch associations for multiple instances', async function () {
        const User = this.sequelize.define('User', {});
        const Task = this.sequelize.define('Task', {});

        Task.User = Task.belongsTo(User, { as: 'user' });

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

        const tasks = await Promise.all([
          Task.create(
            {
              id: 1,
              user: { id: 1 },
            },
            {
              include: [Task.User],
            },
          ),
          Task.create(
            {
              id: 2,
              user: { id: 2 },
            },
            {
              include: [Task.User],
            },
          ),
          Task.create({
            id: 3,
          }),
        ]);

        const result = await Task.User.get(tasks);
        expect(result.get(tasks[0].id).id).to.equal(tasks[0].user.id);
        expect(result.get(tasks[1].id).id).to.equal(tasks[1].user.id);
        expect(result.get(tasks[2].id)).to.be.undefined;
      });
    });
  });

  describe('getAssociation', () => {
    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 });
        const Group = sequelize.define('Group', { name: DataTypes.STRING });

        Group.belongsTo(User);

        await sequelize.sync({ force: true });
        const user = await User.create({ username: 'foo' });
        const group = await Group.create({ name: 'bar' });
        const t = await sequelize.startUnmanagedTransaction();
        await group.setUser(user, { transaction: t });
        const groups = await Group.findAll();
        const associatedUser = await groups[0].getUser();
        expect(associatedUser).to.be.null;
        const groups0 = await Group.findAll({ transaction: t });
        const associatedUser0 = await groups0[0].getUser({ transaction: t });
        expect(associatedUser0).to.be.not.null;
        await t.rollback();
      });
    }

    it("should be able to handle a where object that's a first class citizen.", async function () {
      const User = this.sequelize.define('UserXYZ', {
        username: DataTypes.STRING,
        gender: DataTypes.STRING,
      });
      const Task = this.sequelize.define('TaskXYZ', {
        title: DataTypes.STRING,
        status: DataTypes.STRING,
      });

      Task.belongsTo(User);

      await User.sync({ force: true });
      // Can't use Promise.all cause of foreign key references
      await Task.sync({ force: true });

      const [userA, , task] = await Promise.all([
        User.create({ username: 'foo', gender: 'male' }),
        User.create({ username: 'bar', gender: 'female' }),
        Task.create({ title: 'task', status: 'inactive' }),
      ]);

      await task.setUserXYZ(userA);
      const user = await task.getUserXYZ({ where: { gender: 'female' } });
      expect(user).to.be.null;
    });

    if (current.dialect.supports.schemas) {
      it('supports schemas', async function () {
        const User = this.sequelize
          .define('UserXYZ', { username: DataTypes.STRING, gender: DataTypes.STRING })
          .withSchema('archive');
        const Task = this.sequelize
          .define('TaskXYZ', { title: DataTypes.STRING, status: DataTypes.STRING })
          .withSchema('archive');

        Task.belongsTo(User);

        await this.sequelize.createSchema('archive');
        await User.sync({ force: true });
        await Task.sync({ force: true });

        const [user0, task] = await Promise.all([
          User.create({ username: 'foo', gender: 'male' }),
          Task.create({ title: 'task', status: 'inactive' }),
        ]);

        await task.setUserXYZ(user0);
        const user = await task.getUserXYZ();
        expect(user).to.be.ok;
        await this.sequelize.queryInterface.dropAllTables({ schema: 'archive' });
        await this.sequelize.dropSchema('archive');
        const schemas = await this.sequelize.queryInterface.listSchemas();
        expect(schemas).to.not.include('archive');
      });

      it('supports schemas when defining custom foreign key attribute #9029', async function () {
        const User = this.sequelize
          .define('UserXYZ', {
            uid: {
              type: DataTypes.INTEGER,
              primaryKey: true,
              autoIncrement: true,
              allowNull: false,
            },
          })
          .withSchema('archive');
        const Task = this.sequelize
          .define('TaskXYZ', {
            user_id: {
              type: DataTypes.INTEGER,
              references: { model: User, key: 'uid' },
            },
          })
          .withSchema('archive');

        Task.belongsTo(User, { foreignKey: 'user_id' });

        await this.sequelize.createSchema('archive');
        await User.sync({ force: true });
        await Task.sync({ force: true });
        const user0 = await User.create({});
        const task = await Task.create({});
        await task.setUserXYZ(user0);
        const user = await task.getUserXYZ();
        expect(user).to.be.ok;
      });
    }
  });

  describe('setAssociation', () => {
    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 });
        const Group = sequelize.define('Group', { name: DataTypes.STRING });

        Group.belongsTo(User);

        await sequelize.sync({ force: true });
        const user = await User.create({ username: 'foo' });
        const group = await Group.create({ name: 'bar' });
        const t = await sequelize.startUnmanagedTransaction();
        await group.setUser(user, { transaction: t });
        const groups = await Group.findAll();
        const associatedUser = await groups[0].getUser();
        expect(associatedUser).to.be.null;
        await t.rollback();
      });
    }

    it('can set the association with declared primary keys...', async function () {
      const User = this.sequelize.define('UserXYZ', {
        user_id: { type: DataTypes.INTEGER, primaryKey: true },
        username: DataTypes.STRING,
      });
      const Task = this.sequelize.define('TaskXYZ', {
        task_id: { type: DataTypes.INTEGER, primaryKey: true },
        title: DataTypes.STRING,
      });

      Task.belongsTo(User, { foreignKey: 'user_id' });

      await this.sequelize.sync({ force: true });
      const user = await User.create({ user_id: 1, username: 'foo' });
      const task = await Task.create({ task_id: 1, title: 'task' });
      await task.setUserXYZ(user);
      const user1 = await task.getUserXYZ();
      expect(user1).not.to.be.null;

      await task.setUserXYZ(null);
      const user0 = await task.getUserXYZ();
      expect(user0).to.be.null;
    });

    it('clears the association if null is passed', async function () {
      const User = this.sequelize.define('UserXYZ', { username: DataTypes.STRING });
      const Task = this.sequelize.define('TaskXYZ', { title: DataTypes.STRING });

      Task.belongsTo(User);

      await this.sequelize.sync({ force: true });
      const user = await User.create({ username: 'foo' });
      const task = await Task.create({ title: 'task' });
      await task.setUserXYZ(user);
      const user1 = await task.getUserXYZ();
      expect(user1).not.to.be.null;

      await task.setUserXYZ(null);
      const user0 = await task.getUserXYZ();
      expect(user0).to.be.null;
    });

    it('should throw a ForeignKeyConstraintError if the associated record does not exist', async function () {
      const User = this.sequelize.define('UserXYZ', { username: DataTypes.STRING });
      const Task = this.sequelize.define('TaskXYZ', { title: DataTypes.STRING });

      Task.belongsTo(User);

      await this.sequelize.sync({ force: true });
      await expect(Task.create({ title: 'task', userXYZId: 5 })).to.be.rejectedWith(
        Sequelize.ForeignKeyConstraintError,
      );
      const task = await Task.create({ title: 'task' });

      await expect(
        Task.update({ title: 'taskUpdate', userXYZId: 5 }, { where: { id: task.id } }),
      ).to.be.rejectedWith(Sequelize.ForeignKeyConstraintError);
    });

    it('supports passing the primary key instead of an object', async function () {
      const User = this.sequelize.define('UserXYZ', { username: DataTypes.STRING });
      const Task = this.sequelize.define('TaskXYZ', { title: DataTypes.STRING });

      Task.belongsTo(User);

      await this.sequelize.sync({ force: true });
      const user = await User.create({ id: 15, username: 'jansemand' });
      const task = await Task.create({});
      await task.setUserXYZ(user.id);
      const user0 = await task.getUserXYZ();
      expect(user0.username).to.equal('jansemand');
    });

    it('should support logging', async function () {
      const User = this.sequelize.define('UserXYZ', { username: DataTypes.STRING });
      const Task = this.sequelize.define('TaskXYZ', { title: DataTypes.STRING });
      const spy = sinon.spy();

      Task.belongsTo(User);

      await this.sequelize.sync({ force: true });
      const user = await User.create();
      const task = await Task.create({});
      await task.setUserXYZ(user, { logging: spy });
      expect(spy.called).to.be.ok;
    });

    it('should not clobber atributes', async function () {
      const Comment = this.sequelize.define('comment', {
        text: DataTypes.STRING,
      });

      const Post = this.sequelize.define('post', {
        title: DataTypes.STRING,
      });

      Post.hasOne(Comment);
      Comment.belongsTo(Post);

      await this.sequelize.sync();

      const post = await Post.create({
        title: 'Post title',
      });

      const comment = await Comment.create({
        text: 'OLD VALUE',
      });

      comment.text = 'UPDATED VALUE';
      await comment.setPost(post);
      expect(comment.text).to.equal('UPDATED VALUE');
    });

    it('should set the foreign key value without saving when using save: false', async function () {
      const Comment = this.sequelize.define('comment', {
        text: DataTypes.STRING,
      });

      const Post = this.sequelize.define('post', {
        title: DataTypes.STRING,
      });

      Post.hasMany(Comment, { foreignKey: 'post_id' });
      Comment.belongsTo(Post, { foreignKey: 'post_id' });

      await this.sequelize.sync({ force: true });
      const [post, comment] = await Promise.all([Post.create(), Comment.create()]);
      expect(comment.get('post_id')).not.to.be.ok;

      const setter = await comment.setPost(post, { save: false });

      expect(setter).to.be.undefined;
      expect(comment.get('post_id')).to.equal(post.get('id'));
      expect(comment.changed('post_id')).to.be.true;
    });

    it('supports setting same association twice', async function () {
      const Home = this.sequelize.define('home', {});
      const User = this.sequelize.define('user');

      Home.belongsTo(User);

      await this.sequelize.sync({ force: true });
      const [home, user] = await Promise.all([Home.create(), User.create()]);
      await home.setUser(user);
      expect(await home.getUser()).to.have.property('id', user.id);
    });
  });

  describe('createAssociation', () => {
    it('creates an associated model instance', async function () {
      const User = this.sequelize.define('User', { username: DataTypes.STRING });
      const Task = this.sequelize.define('Task', { title: DataTypes.STRING });

      Task.belongsTo(User);

      await this.sequelize.sync({ force: true });
      const task = await Task.create({ title: 'task' });
      const user = await task.createUser({ username: 'bob' });
      expect(user).not.to.be.null;
      expect(user.username).to.equal('bob');
    });

    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 });
        const Group = sequelize.define('Group', { name: DataTypes.STRING });

        Group.belongsTo(User);

        await sequelize.sync({ force: true });
        const group = await Group.create({ name: 'bar' });
        const t = await sequelize.startUnmanagedTransaction();
        await group.createUser({ username: 'foo' }, { transaction: t });
        const user = await group.getUser();
        expect(user).to.be.null;

        const user0 = await group.getUser({ transaction: t });
        expect(user0).not.to.be.null;

        await t.rollback();
      });
    }
  });

  describe('foreign key', () => {
    it('should setup underscored field with foreign keys when using underscored', function () {
      const User = this.sequelize.define(
        'User',
        { username: DataTypes.STRING },
        { underscored: true },
      );
      const Account = this.sequelize.define(
        'Account',
        { name: DataTypes.STRING },
        { underscored: true },
      );

      User.belongsTo(Account);

      expect(User.getAttributes().accountId).to.exist;
      expect(User.getAttributes().accountId.field).to.equal('account_id');
    });

    it('should use model name when using camelcase', function () {
      const User = this.sequelize.define(
        'User',
        { username: DataTypes.STRING },
        { underscored: false },
      );
      const Account = this.sequelize.define(
        'Account',
        { name: DataTypes.STRING },
        { underscored: false },
      );

      User.belongsTo(Account);

      expect(User.getAttributes().accountId).to.exist;
      expect(User.getAttributes().accountId.field).to.equal('accountId');
    });

    it('should support specifying the field of a foreign key', async function () {
      const User = this.sequelize.define(
        'User',
        { username: DataTypes.STRING },
        { underscored: false },
      );
      const Account = this.sequelize.define(
        'Account',
        { title: DataTypes.STRING },
        { underscored: false },
      );

      User.belongsTo(Account, {
        foreignKey: {
          name: 'AccountId',
          field: 'account_id',
        },
      });

      expect(User.getAttributes().AccountId).to.exist;
      expect(User.getAttributes().AccountId.field).to.equal('account_id');

      await Account.sync({ force: true });
      // Can't use Promise.all cause of foreign key references
      await User.sync({ force: true });

      const [user1, account] = await Promise.all([
        User.create({ username: 'foo' }),
        Account.create({ title: 'pepsico' }),
      ]);

      await user1.setAccount(account);
      const user0 = await user1.getAccount();
      expect(user0).to.not.be.null;

      const user = await User.findOne({
        where: { username: 'foo' },
        include: [Account],
      });

      // the sql query should correctly look at account_id instead of AccountId
      expect(user.account).to.exist;
    });

    it('should set foreignKey on foreign table', async function () {
      const Mail = this.sequelize.define('mail', {}, { timestamps: false });
      const Entry = this.sequelize.define('entry', {}, { timestamps: false });
      const User = this.sequelize.define('user', {}, { timestamps: false });

      Entry.belongsTo(User, {
        as: 'owner',
        foreignKey: {
          name: 'ownerId',
          allowNull: false,
        },
      });
      Entry.belongsTo(Mail, {
        as: 'mail',
        foreignKey: {
          name: 'mailId',
          allowNull: false,
        },
      });
      Mail.belongsToMany(User, {
        as: 'recipients',
        through: {
          model: 'MailRecipients',
          timestamps: false,
        },
        otherKey: {
          name: 'recipientId',
          allowNull: false,
        },
        foreignKey: {
          name: 'mailId',
          allowNull: false,
        },
      });
      Mail.hasMany(Entry, {
        as: 'entries',
        foreignKey: {
          name: 'mailId',
          allowNull: false,
        },
      });
      User.hasMany(Entry, {
        as: 'entries',
        foreignKey: {
          name: 'ownerId',
          allowNull: false,
        },
      });

      await this.sequelize.sync({ force: true });
      await User.create(dialect === 'db2' ? { id: 1 } : {});
      const mail = await Mail.create(dialect === 'db2' ? { id: 1 } : {});
      await Entry.create({ mailId: mail.id, ownerId: 1 });
      await Entry.create({ mailId: mail.id, ownerId: 1 });
      // set recipients
      await mail.setRecipients([1]);

      const result = await Entry.findAndCountAll({
        offset: 0,
        limit: 10,
        order: [['id', 'DESC']],
        include: [
          {
            association: Entry.associations.mail,
            include: [
              {
                association: Mail.associations.recipients,
                through: {
                  where: {
                    recipientId: 1,
                  },
                },
                required: true,
              },
            ],
            required: true,
          },
        ],
      });

      expect(result.count).to.equal(2);
      expect(result.rows[0].get({ plain: true })).to.deep.equal({
        id: 2,
        ownerId: 1,
        mailId: 1,
        mail: {
          id: 1,
          recipients: [
            {
              id: 1,
              MailRecipients: {
                mailId: 1,
                recipientId: 1,
              },
            },
          ],
        },
      });
    });
  });

  describe('foreign key constraints', () => {
    it('are enabled by default', async function () {
      const Task = this.sequelize.define('Task', { title: DataTypes.STRING });
      const User = this.sequelize.define('User', { username: DataTypes.STRING });

      Task.belongsTo(User); // defaults to SET NULL

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

      const user = await User.create({ username: 'foo' });
      const task = await Task.create({ title: 'task' });
      await task.setUser(user);
      await user.destroy();
      await task.reload();
      expect(task.userId).to.equal(null);
    });

    it('should be possible to disable them', async function () {
      const Task = this.sequelize.define('Task', { title: DataTypes.STRING });
      const User = this.sequelize.define('User', { username: DataTypes.STRING });

      Task.belongsTo(User, { foreignKeyConstraints: false });

      await this.sequelize.sync({ force: true });
      const user = await User.create({ username: 'foo' });
      const task = await Task.create({ title: 'task' });
      await task.setUser(user);
      await user.destroy();
      await task.reload();
      expect(task.userId).to.equal(user.id);
    });

    it('can cascade deletes', async function () {
      const Task = this.sequelize.define('Task', { title: DataTypes.STRING });
      const User = this.sequelize.define('User', { username: DataTypes.STRING });

      Task.belongsTo(User, { foreignKey: { onDelete: 'cascade' } });

      await this.sequelize.sync({ force: true });
      const user = await User.create({ username: 'foo' });
      const task = await Task.create({ title: 'task' });
      await task.setUser(user);
      await user.destroy();
      const tasks = await Task.findAll();
      expect(tasks).to.have.length(0);
    });

    if (current.dialect.supports.constraints.restrict) {
      it('can restrict deletes', async function () {
        const Task = this.sequelize.define('Task', { title: DataTypes.STRING });
        const User = this.sequelize.define('User', { username: DataTypes.STRING });

        Task.belongsTo(User, { foreignKey: { onDelete: 'restrict' } });

        await this.sequelize.sync({ force: true });
        const user = await User.create({ username: 'foo' });
        const task = await Task.create({ title: 'task' });
        await task.setUser(user);
        await expect(user.destroy()).to.eventually.be.rejectedWith(
          Sequelize.ForeignKeyConstraintError,
        );
        const tasks = await Task.findAll();
        expect(tasks).to.have.length(1);
      });

      it('can restrict updates', async function () {
        const Task = this.sequelize.define('Task', { title: DataTypes.STRING });
        const User = this.sequelize.define('User', { username: DataTypes.STRING });

        Task.belongsTo(User, { foreignKey: { onUpdate: 'restrict' } });

        await this.sequelize.sync({ force: true });
        const user = await User.create({ username: 'foo' });
        const task = await Task.create({ title: 'task' });
        await task.setUser(user);

        // Changing the id of a DAO requires a little dance since
        // the `UPDATE` query generated by `save()` uses `id` in the
        // `WHERE` clause

        const tableName = User.table;

        await expect(
          user.sequelize.queryInterface.update(user, tableName, { id: 999 }, { id: user.id }),
        ).to.eventually.be.rejectedWith(Sequelize.ForeignKeyConstraintError);

        // Should fail due to FK restriction
        const tasks = await Task.findAll();

        expect(tasks).to.have.length(1);
      });
    }

    // NOTE: mssql does not support changing an autoincrement primary key
    if (!['mssql', 'db2', 'ibmi'].includes(dialect)) {
      it('can cascade updates', async function () {
        const Task = this.sequelize.define('Task', { title: DataTypes.STRING });
        const User = this.sequelize.define('User', { username: DataTypes.STRING });

        Task.belongsTo(User, { foreignKey: { onUpdate: 'cascade' } });

        await this.sequelize.sync({ force: true });
        const user = await User.create({ username: 'foo' });
        const task = await Task.create({ title: 'task' });
        await task.setUser(user);

        // Changing the id of a DAO requires a little dance since
        // the `UPDATE` query generated by `save()` uses `id` in the
        // `WHERE` clause

        const tableName = User.table;
        await user.sequelize.queryInterface.update(user, tableName, { id: 999 }, { id: user.id });
        const tasks = await Task.findAll();
        expect(tasks).to.have.length(1);
        expect(tasks[0].userId).to.equal(999);
      });
    }
  });

  describe('association column', () => {
    it('has correct type and name for non-id primary keys with non-integer type', async function () {
      const User = this.sequelize.define('UserPKBT', {
        username: {
          type: DataTypes.STRING,
        },
      });

      const Group = this.sequelize.define('GroupPKBT', {
        name: {
          type: DataTypes.STRING,
          primaryKey: true,
        },
      });

      User.belongsTo(Group);

      await this.sequelize.sync({ force: true });
      expect(User.getAttributes().groupPKBTName.type).to.an.instanceof(DataTypes.STRING);
    });

    it('should support a non-primary key as the association column on a target without a primary key', async function () {
      const User = this.sequelize.define('User', {
        username: { type: DataTypes.STRING, unique: true },
      });
      const Task = this.sequelize.define('Task', { title: DataTypes.STRING });

      User.removeAttribute('id');
      Task.belongsTo(User, { foreignKey: 'user_name', targetKey: 'username' });

      await this.sequelize.sync({ force: true });
      const newUser = await User.create({ username: 'bob' });
      const newTask = await Task.create({ title: 'some task' });
      await newTask.setUser(newUser);
      const foundTask = await Task.findOne({ where: { title: 'some task' } });
      const foundUser = await foundTask.getUser();
      await expect(foundUser.username).to.equal('bob');
      const foreignKeysDescriptions = await this.sequelize.queryInterface.showConstraints(Task, {
        constraintType: 'FOREIGN KEY',
      });
      expect(foreignKeysDescriptions[0]).to.deep.include({
        referencedColumnNames: ['username'],
        referencedTableName: 'Users',
        columnNames: ['user_name'],
      });
    });

    it('should support a non-primary unique key as the association column', async function () {
      const User = this.sequelize.define('User', {
        username: {
          type: DataTypes.STRING,
          field: 'user_name',
          unique: true,
        },
      });
      const Task = this.sequelize.define('Task', {
        title: DataTypes.STRING,
      });

      Task.belongsTo(User, { foreignKey: 'user_name', targetKey: 'username' });

      await this.sequelize.sync({ force: true });
      const newUser = await User.create({ username: 'bob' });
      const newTask = await Task.create({ title: 'some task' });
      await newTask.setUser(newUser);
      const foundTask = await Task.findOne({ where: { title: 'some task' } });
      const foundUser = await foundTask.getUser();
      await expect(foundUser.username).to.equal('bob');
      const foreignKeysDescriptions = await this.sequelize.queryInterface.showConstraints(Task, {
        constraintType: 'FOREIGN KEY',
      });
      expect(foreignKeysDescriptions[0]).to.deep.include({
        referencedColumnNames: ['user_name'],
        referencedTableName: 'Users',
        columnNames: ['user_name'],
      });
    });

    it('should support a non-primary key as the association column with a field option', async function () {
      const User = this.sequelize.define('User', {
        username: {
          type: DataTypes.STRING,
          field: 'the_user_name_field',
          unique: true,
        },
      });
      const Task = this.sequelize.define('Task', { title: DataTypes.STRING });

      User.removeAttribute('id');
      Task.belongsTo(User, { foreignKey: 'user_name', targetKey: 'username' });

      await this.sequelize.sync({ force: true });
      const newUser = await User.create({ username: 'bob' });
      const newTask = await Task.create({ title: 'some task' });
      await newTask.setUser(newUser);
      const foundTask = await Task.findOne({ where: { title: 'some task' } });
      const foundUser = await foundTask.getUser();
      await expect(foundUser.username).to.equal('bob');
      const foreignKeysDescriptions = await this.sequelize.queryInterface.showConstraints(Task, {
        constraintType: 'FOREIGN KEY',
      });
      expect(foreignKeysDescriptions[0]).to.deep.include({
        referencedColumnNames: ['the_user_name_field'],
        referencedTableName: 'Users',
        columnNames: ['user_name'],
      });
    });

    it('should support a non-primary key as the association column in a table with a composite primary key', async function () {
      const User = this.sequelize.define('User', {
        username: {
          type: DataTypes.STRING,
          field: 'the_user_name_field',
          unique: true,
        },
        age: {
          type: DataTypes.INTEGER,
          field: 'the_user_age_field',
          primaryKey: true,
        },
        weight: {
          type: DataTypes.INTEGER,
          field: 'the_user_weight_field',
          primaryKey: true,
        },
      });
      const Task = this.sequelize.define('Task', { title: DataTypes.STRING });

      Task.belongsTo(User, { foreignKey: 'user_name', targetKey: 'username' });

      await this.sequelize.sync({ force: true });
      const newUser = await User.create({ username: 'bob', age: 18, weight: 40 });
      const newTask = await Task.create({ title: 'some task' });
      await newTask.setUser(newUser);
      const foundTask = await Task.findOne({ where: { title: 'some task' } });
      const foundUser = await foundTask.getUser();
      await expect(foundUser.username).to.equal('bob');
      const foreignKeysDescriptions = await this.sequelize.queryInterface.showConstraints(Task, {
        constraintType: 'FOREIGN KEY',
      });
      expect(foreignKeysDescriptions[0]).to.deep.include({
        referencedColumnNames: ['the_user_name_field'],
        referencedTableName: 'Users',
        columnNames: ['user_name'],
      });
    });
  });

  describe('association options', () => {
    it('can specify data type for auto-generated relational keys', async function () {
      const User = this.sequelize.define('UserXYZ', { username: DataTypes.STRING });
      const dataTypes = [DataTypes.INTEGER, DataTypes.STRING];
      const Tasks = {};

      if (current.dialect.supports.dataTypes.BIGINT) {
        dataTypes.push(DataTypes.BIGINT);
      }

      for (const dataType of dataTypes) {
        const tableName = `TaskXYZ_${dataType.getDataTypeId()}`;
        Tasks[dataType] = this.sequelize.define(tableName, { title: DataTypes.STRING });
        Tasks[dataType].belongsTo(User, {
          foreignKey: { name: 'userId', type: dataType },
          foreignKeyConstraints: false,
        });
      }

      await this.sequelize.sync({ force: true });
      for (const dataType of dataTypes) {
        expect(Tasks[dataType].getAttributes().userId.type).to.be.an.instanceof(dataType);
      }
    });

    describe('allows the user to provide an attribute definition object as foreignKey', () => {
      it('works with a column that hasnt been defined before', function () {
        const Task = this.sequelize.define('task', {});
        const User = this.sequelize.define('user', {});

        Task.belongsTo(User, {
          foreignKey: {
            allowNull: false,
            name: 'uid',
          },
        });

        expect(Task.getAttributes().uid).to.be.ok;
        expect(Task.getAttributes().uid.allowNull).to.be.false;
        const targetTable = Task.getAttributes().uid.references.table;
        assert(typeof targetTable === 'object');

        expect(targetTable).to.deep.equal(User.table);

        expect(Task.getAttributes().uid.references.key).to.equal('id');
      });

      it('works when taking a column directly from the object', function () {
        const User = this.sequelize.define('user', {
          uid: {
            type: DataTypes.INTEGER,
            primaryKey: true,
          },
        });
        const Profile = this.sequelize.define('project', {
          user_id: {
            type: DataTypes.INTEGER,
            allowNull: false,
          },
        });

        Profile.belongsTo(User, { foreignKey: Profile.getAttributes().user_id });

        expect(Profile.getAttributes().user_id).to.be.ok;
        const targetTable = Profile.getAttributes().user_id.references.table;
        assert(typeof targetTable === 'object');

        expect(targetTable).to.deep.equal(User.table);
        expect(Profile.getAttributes().user_id.references.key).to.equal('uid');
        expect(Profile.getAttributes().user_id.allowNull).to.be.false;
      });

      it('works when merging with an existing definition', function () {
        const Task = this.sequelize.define('task', {
          projectId: {
            defaultValue: 42,
            type: DataTypes.INTEGER,
          },
        });
        const Project = this.sequelize.define('project', {});

        Task.belongsTo(Project, { foreignKey: { allowNull: true } });

        expect(Task.getAttributes().projectId).to.be.ok;
        expect(Task.getAttributes().projectId.defaultValue).to.equal(42);
        expect(Task.getAttributes().projectId.allowNull).to.be.ok;
      });
    });
  });

  describe('Eager loading', () => {
    beforeEach(function () {
      this.Individual = this.sequelize.define('individual', {
        name: DataTypes.STRING,
      });
      this.Hat = this.sequelize.define('hat', {
        name: DataTypes.STRING,
      });
      this.Individual.belongsTo(this.Hat, {
        as: 'personwearinghat',
      });
    });

    it('should load with an alias', async function () {
      await this.sequelize.sync({ force: true });

      const [individual1, hat] = await Promise.all([
        this.Individual.create({ name: 'Foo Bar' }),
        this.Hat.create({ name: 'Baz' }),
      ]);

      await individual1.setPersonwearinghat(hat);

      const individual0 = await this.Individual.findOne({
        where: { name: 'Foo Bar' },
        include: [{ model: this.Hat, as: 'personwearinghat' }],
      });

      expect(individual0.name).to.equal('Foo Bar');
      expect(individual0.personwearinghat.name).to.equal('Baz');

      const individual = await this.Individual.findOne({
        where: { name: 'Foo Bar' },
        include: [
          {
            model: this.Hat,
            as: 'personwearinghat',
          },
        ],
      });

      expect(individual.name).to.equal('Foo Bar');
      expect(individual.personwearinghat.name).to.equal('Baz');
    });

    it('should load all', async function () {
      await this.sequelize.sync({ force: true });

      const [individual0, hat] = await Promise.all([
        this.Individual.create({ name: 'Foo Bar' }),
        this.Hat.create({ name: 'Baz' }),
      ]);

      await individual0.setPersonwearinghat(hat);

      const individual = await this.Individual.findOne({
        where: { name: 'Foo Bar' },
        include: [{ all: true }],
      });

      expect(individual.name).to.equal('Foo Bar');
      expect(individual.personwearinghat.name).to.equal('Baz');
    });
  });
});
