'use strict';

const upperFirst = require('lodash/upperFirst');

const chai = require('chai');

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

describe(Support.getTestDialectTeaser('Include'), () => {
  describe('findOne', () => {
    it('should include a non required model, with conditions and two includes N:M 1:M', async function () {
      const A = this.sequelize.define('A', { name: DataTypes.STRING(40) }, { paranoid: true });
      const B = this.sequelize.define('B', { name: DataTypes.STRING(40) }, { paranoid: true });
      const C = this.sequelize.define('C', { name: DataTypes.STRING(40) }, { paranoid: true });
      const D = this.sequelize.define('D', { name: DataTypes.STRING(40) }, { paranoid: true });

      // Associations
      A.hasMany(B);

      B.belongsTo(D);
      B.belongsToMany(C, {
        through: 'BC',
      });

      C.belongsToMany(B, {
        through: 'BC',
      });

      D.hasMany(B);

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

      await A.findOne({
        include: [
          {
            model: B,
            required: false,
            include: [{ model: C, required: false }, { model: D }],
          },
        ],
      });
    });

    it('should work with a 1:M to M:1 relation with a where on the last include', async function () {
      const Model = this.sequelize.define('Model', {});
      const Model2 = this.sequelize.define('Model2', {});
      const Model4 = this.sequelize.define('Model4', { something: { type: DataTypes.INTEGER } });

      Model.belongsTo(Model2);
      Model2.hasMany(Model);

      Model2.hasMany(Model4);
      Model4.belongsTo(Model2);

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

      await Model.findOne({
        include: [
          {
            model: Model2,
            include: [{ model: Model4, where: { something: 2 } }],
          },
        ],
      });
    });

    it('should include a model with a where condition but no required', async function () {
      const User = this.sequelize.define('User', {}, { paranoid: false });
      const Task = this.sequelize.define(
        'Task',
        {
          deletedAt: {
            type: DataTypes.DATE,
          },
        },
        { paranoid: false },
      );

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

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

      const user0 = await User.create();

      await Task.bulkCreate([
        { userId: user0.get('id'), deletedAt: new Date() },
        { userId: user0.get('id'), deletedAt: new Date() },
        { userId: user0.get('id'), deletedAt: new Date() },
      ]);

      const user = await User.findOne({
        include: [{ model: Task, where: { deletedAt: null }, required: false }],
      });

      expect(user).to.be.ok;
      expect(user.tasks.length).to.equal(0);
    });

    it('should include a model with a where clause when the PK field name and attribute name are different', async function () {
      const User = this.sequelize.define('User', {
        id: {
          type: DataTypes.UUID,
          defaultValue: sql.uuidV4,
          field: 'main_id',
          primaryKey: true,
        },
      });
      const Task = this.sequelize.define('Task', {
        searchString: { type: DataTypes.STRING },
      });

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

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

      const user0 = await User.create();

      await Task.bulkCreate([
        { userId: user0.get('id'), searchString: 'one' },
        { userId: user0.get('id'), searchString: 'two' },
      ]);

      const user = await User.findOne({
        include: [{ model: Task, where: { searchString: 'one' } }],
      });

      expect(user).to.be.ok;
      expect(user.tasks.length).to.equal(1);
    });

    it('should include a model with a through.where and required true clause when the PK field name and attribute name are different', async function () {
      const A = this.sequelize.define('a', {});
      const B = this.sequelize.define('b', {});
      const AB = this.sequelize.define('a_b', {
        name: {
          type: DataTypes.STRING(40),
          field: 'name_id',
          primaryKey: true,
        },
      });

      A.belongsToMany(B, { through: AB });
      B.belongsToMany(A, { through: AB });

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

      const [a0, b] = await Promise.all([A.create({}), B.create({})]);
      await a0.addB(b, { through: { name: 'Foobar' } });

      const a = await A.findOne({
        include: [{ model: B, through: { where: { name: 'Foobar' } }, required: true }],
      });

      expect(a).to.not.equal(null);
      expect(a.get('bs')).to.have.length(1);
    });

    it('should still pull the main record when an included model is not required and has where restrictions without matches', async function () {
      const A = this.sequelize.define('a', {
        name: DataTypes.STRING(40),
      });
      const B = this.sequelize.define('b', {
        name: DataTypes.STRING(40),
      });

      A.belongsToMany(B, { through: 'a_b' });
      B.belongsToMany(A, { through: 'a_b' });

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

      await A.create({
        name: 'Foobar',
      });

      const a = await A.findOne({
        where: { name: 'Foobar' },
        include: [{ model: B, where: { name: 'idontexist' }, required: false }],
      });

      expect(a).to.not.equal(null);
      expect(a.get('bs')).to.deep.equal([]);
    });

    it('should support a nested include (with a where)', async function () {
      const A = this.sequelize.define('A', {
        name: DataTypes.STRING,
      });

      const B = this.sequelize.define('B', {
        flag: DataTypes.BOOLEAN,
      });

      const C = this.sequelize.define('C', {
        name: DataTypes.STRING,
      });

      A.hasOne(B);
      B.belongsTo(A);

      B.hasMany(C);
      C.belongsTo(B);

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

      const a = await A.findOne({
        include: [
          {
            model: B,
            where: { flag: true },
            include: [
              {
                model: C,
              },
            ],
          },
        ],
      });

      expect(a).to.not.exist;
    });

    it('should support a belongsTo with the targetKey option', 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' },
        include: [{ model: User }],
      });

      expect(foundTask).to.be.ok;
      expect(foundTask.user.username).to.equal('bob');
    });

    it('should support many levels of belongsTo (with a lower level having a where)', async function () {
      const A = this.sequelize.define('a', {});
      const B = this.sequelize.define('b', {});
      const C = this.sequelize.define('c', {});
      const D = this.sequelize.define('d', {});
      const E = this.sequelize.define('e', {});
      const F = this.sequelize.define('f', {});
      const G = this.sequelize.define('g', {
        name: DataTypes.STRING,
      });
      const H = this.sequelize.define('h', {
        name: DataTypes.STRING,
      });

      A.belongsTo(B);
      B.belongsTo(C);
      C.belongsTo(D);
      D.belongsTo(E);
      E.belongsTo(F);
      F.belongsTo(G);
      G.belongsTo(H);

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

      const [a0, b] = await Promise.all([
        A.create({}),
        (function (singles) {
          let promise = Promise.resolve();
          let previousInstance;
          let b;

          for (const model of singles) {
            const values = {};

            if (model.name === 'g') {
              values.name = 'yolo';
            }

            promise = (async () => {
              await promise;
              const instance = await model.create(values);
              if (previousInstance) {
                await previousInstance[`set${upperFirst(model.name)}`](instance);
                previousInstance = instance;

                return;
              }

              previousInstance = b = instance;
            })();
          }

          promise = promise.then(() => {
            return b;
          });

          return promise;
        })([B, C, D, E, F, G, H]),
      ]);

      await a0.setB(b);

      const a = await A.findOne({
        include: [
          {
            model: B,
            include: [
              {
                model: C,
                include: [
                  {
                    model: D,
                    include: [
                      {
                        model: E,
                        include: [
                          {
                            model: F,
                            include: [
                              {
                                model: G,
                                where: {
                                  name: 'yolo',
                                },
                                include: [{ model: H }],
                              },
                            ],
                          },
                        ],
                      },
                    ],
                  },
                ],
              },
            ],
          },
        ],
      });

      expect(a.b.c.d.e.f.g.h).to.be.ok;
    });

    it('should work with combinding a where and a scope', async function () {
      const User = this.sequelize.define(
        'User',
        {
          id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
          name: DataTypes.STRING,
        },
        { underscored: true },
      );

      const Post = this.sequelize.define(
        'Post',
        {
          id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true, unique: true },
          owner_id: { type: DataTypes.INTEGER, unique: 'combiIndex' },
          owner_type: {
            type: DataTypes.ENUM(['user', 'org']),
            defaultValue: 'user',
            unique: 'combiIndex',
          },
          private: { type: DataTypes.BOOLEAN, defaultValue: false },
        },
        { underscored: true },
      );

      User.hasMany(Post, {
        foreignKey: 'owner_id',
        scope: { owner_type: 'user' },
        as: 'UserPosts',
        foreignKeyConstraints: false,
      });
      Post.belongsTo(User, { foreignKey: 'owner_id', as: 'Owner', foreignKeyConstraints: false });

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

      await User.findOne({
        where: { id: 2 },
        include: [{ model: Post, as: 'UserPosts', where: { private: true } }],
      });
    });
  });
});
