'use strict';

const chai = require('chai');

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

const current = Support.sequelize;

if (current.dialect.supports.groupedLimit) {
  describe(Support.getTestDialectTeaser('Include'), () => {
    describe('separate', () => {
      it('should run a hasMany association in a separate query', async function () {
        const User = this.sequelize.define('User', {});
        const Task = this.sequelize.define('Task', {});
        const sqlSpy = sinon.spy();

        User.Tasks = User.hasMany(Task, { as: 'tasks' });

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

        await Promise.all([
          User.create(
            {
              id: 1,
              tasks: [{}, {}, {}],
            },
            {
              include: [User.Tasks],
            },
          ),
          User.create(
            {
              id: 2,
              tasks: [{}],
            },
            {
              include: [User.Tasks],
            },
          ),
        ]);

        const users = await User.findAll({
          include: [{ association: User.Tasks, separate: true }],
          order: [['id', 'ASC']],
          logging: sqlSpy,
        });

        expect(users[0].get('tasks')).to.be.ok;
        expect(users[0].get('tasks').length).to.equal(3);
        expect(users[1].get('tasks')).to.be.ok;
        expect(users[1].get('tasks').length).to.equal(1);

        expect(users[0].get('tasks')[0].createdAt).to.be.ok;
        expect(users[0].get('tasks')[0].updatedAt).to.be.ok;

        expect(sqlSpy).to.have.been.calledTwice;
      });

      it('should work even if the id was not included', async function () {
        const User = this.sequelize.define('User', {
          name: DataTypes.STRING,
        });
        const Task = this.sequelize.define('Task', {});
        const sqlSpy = sinon.spy();

        User.Tasks = User.hasMany(Task, { as: 'tasks' });

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

        await User.create(
          {
            id: 1,
            tasks: [{}, {}, {}],
          },
          {
            include: [User.Tasks],
          },
        );

        const users = await User.findAll({
          attributes: ['name'],
          include: [{ association: User.Tasks, separate: true }],
          order: [['id', 'ASC']],
          logging: sqlSpy,
        });

        expect(users[0].get('tasks')).to.be.ok;
        expect(users[0].get('tasks').length).to.equal(3);
        expect(sqlSpy).to.have.been.calledTwice;
      });

      it('should work even if include does not specify foreign key attribute with custom sourceKey', async function () {
        const User = this.sequelize.define('User', {
          name: DataTypes.STRING,
          userExtraId: {
            type: DataTypes.INTEGER,
            unique: true,
          },
        });
        const Task = this.sequelize.define('Task', {
          title: DataTypes.STRING,
        });
        const sqlSpy = sinon.spy();

        User.Tasks = User.hasMany(Task, {
          as: 'tasks',
          foreignKey: 'userId',
          sourceKey: 'userExtraId',
        });

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

        await User.create(
          {
            id: 1,
            userExtraId: 222,
            tasks: [{}, {}, {}],
          },
          {
            include: [User.Tasks],
          },
        );

        const users = await User.findAll({
          attributes: ['name'],
          include: [
            {
              attributes: ['title'],
              association: User.Tasks,
              separate: true,
            },
          ],
          order: [['id', 'ASC']],
          logging: sqlSpy,
        });

        expect(users[0].get('tasks')).to.be.ok;
        expect(users[0].get('tasks').length).to.equal(3);
        expect(sqlSpy).to.have.been.calledTwice;
      });

      it('should not break a nested include with null values', async function () {
        const User = this.sequelize.define('User', {});
        const Team = this.sequelize.define('Team', {});
        const Company = this.sequelize.define('Company', {});

        User.Team = User.belongsTo(Team);
        Team.Company = Team.belongsTo(Company);

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

        await User.findAll({
          include: [{ association: User.Team, include: [Team.Company] }],
        });
      });

      it('should run a hasMany association with limit in a separate query', async function () {
        const User = this.sequelize.define('User', {});
        const Task = this.sequelize.define('Task', {
          userId: {
            type: DataTypes.INTEGER,
            field: 'user_id',
          },
        });
        const sqlSpy = sinon.spy();

        User.Tasks = User.hasMany(Task, { as: 'tasks', foreignKey: 'userId' });

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

        await Promise.all([
          User.create(
            {
              id: 1,
              tasks: [{}, {}, {}],
            },
            {
              include: [User.Tasks],
            },
          ),
          User.create(
            {
              id: 2,
              tasks: [{}, {}, {}, {}],
            },
            {
              include: [User.Tasks],
            },
          ),
        ]);

        const users = await User.findAll({
          include: [{ association: User.Tasks, limit: 2 }],
          order: [['id', 'ASC']],
          logging: sqlSpy,
        });

        expect(users[0].get('tasks')).to.be.ok;
        expect(users[0].get('tasks').length).to.equal(2);
        expect(users[1].get('tasks')).to.be.ok;
        expect(users[1].get('tasks').length).to.equal(2);
        expect(sqlSpy).to.have.been.calledTwice;
      });

      it('should run a nested (from a non-separate include) hasMany association in a separate query', async function () {
        const User = this.sequelize.define('User', {});
        const Company = this.sequelize.define('Company');
        const Task = this.sequelize.define('Task', {});
        const sqlSpy = sinon.spy();

        User.Company = User.belongsTo(Company, { as: 'company' });
        Company.Tasks = Company.hasMany(Task, { as: 'tasks' });

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

        await Promise.all([
          User.create(
            {
              id: 1,
              company: {
                tasks: [{}, {}, {}],
              },
            },
            {
              include: [{ association: User.Company, include: [Company.Tasks] }],
            },
          ),
          User.create(
            {
              id: 2,
              company: {
                tasks: [{}],
              },
            },
            {
              include: [{ association: User.Company, include: [Company.Tasks] }],
            },
          ),
        ]);

        const users = await User.findAll({
          include: [
            {
              association: User.Company,
              include: [{ association: Company.Tasks, separate: true }],
            },
          ],
          order: [['id', 'ASC']],
          logging: sqlSpy,
        });

        expect(users[0].get('company').get('tasks')).to.be.ok;
        expect(users[0].get('company').get('tasks').length).to.equal(3);
        expect(users[1].get('company').get('tasks')).to.be.ok;
        expect(users[1].get('company').get('tasks').length).to.equal(1);
        expect(sqlSpy).to.have.been.calledTwice;
      });

      it('should work having a separate include between a parent and child include', async function () {
        const User = this.sequelize.define('User', {});
        const Project = this.sequelize.define('Project');
        const Company = this.sequelize.define('Company');
        const Task = this.sequelize.define('Task', {});
        const sqlSpy = sinon.spy();

        Company.Users = Company.hasMany(User, { as: 'users' });
        User.Tasks = User.hasMany(Task, { as: 'tasks' });
        Task.Project = Task.belongsTo(Project, { as: 'project' });

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

        await Promise.all([
          Company.create(
            {
              id: 1,
              users: [
                {
                  tasks: [{ project: {} }, { project: {} }, { project: {} }],
                },
              ],
            },
            {
              include: [
                {
                  association: Company.Users,
                  include: [
                    {
                      association: User.Tasks,
                      include: [Task.Project],
                    },
                  ],
                },
              ],
            },
          ),
        ]);

        const companies = await Company.findAll({
          include: [
            {
              association: Company.Users,
              include: [
                {
                  association: User.Tasks,
                  separate: true,
                  include: [Task.Project],
                },
              ],
            },
          ],
          order: [['id', 'ASC']],
          logging: sqlSpy,
        });

        expect(sqlSpy).to.have.been.calledTwice;

        expect(companies[0].users[0].tasks[0].project).to.be.ok;
      });

      it('should run two nested hasMany association in a separate queries', async function () {
        const User = this.sequelize.define('User', {});
        const Project = this.sequelize.define('Project', {});
        const Task = this.sequelize.define('Task', {});
        const sqlSpy = sinon.spy();

        User.Projects = User.hasMany(Project, { as: 'projects' });
        Project.Tasks = Project.hasMany(Task, { as: 'tasks' });

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

        await Promise.all([
          User.create(
            {
              id: 1,
              projects: [
                {
                  id: 1,
                  tasks: [{}, {}, {}],
                },
                {
                  id: 2,
                  tasks: [{}],
                },
              ],
            },
            {
              include: [{ association: User.Projects, include: [Project.Tasks] }],
            },
          ),
          User.create(
            {
              id: 2,
              projects: [
                {
                  id: 3,
                  tasks: [{}, {}],
                },
              ],
            },
            {
              include: [{ association: User.Projects, include: [Project.Tasks] }],
            },
          ),
        ]);

        const users = await User.findAll({
          include: [
            {
              association: User.Projects,
              separate: true,
              include: [{ association: Project.Tasks, separate: true }],
            },
          ],
          order: [['id', 'ASC']],
          logging: sqlSpy,
        });

        const u1projects = users[0].get('projects');

        expect(u1projects).to.be.ok;
        expect(u1projects[0].get('tasks')).to.be.ok;
        expect(u1projects[1].get('tasks')).to.be.ok;
        expect(u1projects.length).to.equal(2);

        // WTB ES2015 syntax ...
        expect(u1projects.find(p => p.id === 1).get('tasks').length).to.equal(3);
        expect(u1projects.find(p => p.id === 2).get('tasks').length).to.equal(1);

        expect(users[1].get('projects')).to.be.ok;
        expect(users[1].get('projects')[0].get('tasks')).to.be.ok;
        expect(users[1].get('projects').length).to.equal(1);
        expect(users[1].get('projects')[0].get('tasks').length).to.equal(2);

        expect(sqlSpy).to.have.been.calledThrice;
      });

      it('should work with two schema models in a hasMany association', async function () {
        const User = this.sequelize.define('User', {}, { schema: 'archive' });
        const Task = this.sequelize.define(
          'Task',
          {
            id: { type: DataTypes.INTEGER, primaryKey: true },
            title: DataTypes.STRING,
          },
          { schema: 'archive' },
        );

        User.Tasks = User.hasMany(Task, { as: 'tasks' });

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

        await Promise.all([
          User.create(
            {
              id: 1,
              tasks: [
                { id: 1, title: 'b' },
                { id: 2, title: 'd' },
                { id: 3, title: 'c' },
                { id: 4, title: 'a' },
              ],
            },
            {
              include: [User.Tasks],
            },
          ),
          User.create(
            {
              id: 2,
              tasks: [
                { id: 5, title: 'a' },
                { id: 6, title: 'c' },
                { id: 7, title: 'b' },
              ],
            },
            {
              include: [User.Tasks],
            },
          ),
        ]);

        const result = await User.findAll({
          include: [{ model: Task, limit: 2, as: 'tasks', order: [['id', 'ASC']] }],
          order: [['id', 'ASC']],
        });

        expect(result[0].tasks.length).to.equal(2);
        expect(result[0].tasks[0].title).to.equal('b');
        expect(result[0].tasks[1].title).to.equal('d');

        expect(result[1].tasks.length).to.equal(2);
        expect(result[1].tasks[0].title).to.equal('a');
        expect(result[1].tasks[1].title).to.equal('c');
        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('should work with required non-separate parent and required child', async function () {
        const User = this.sequelize.define('User', {});
        const Task = this.sequelize.define('Task', {});
        const Company = this.sequelize.define('Company', {});

        Task.User = Task.belongsTo(User);
        User.Tasks = User.hasMany(Task);
        User.Company = User.belongsTo(Company);

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

        const task = await Task.create({ id: 1 });
        const user = await task.createUser({ id: 2 });
        await user.createCompany({ id: 3 });

        const results = await Task.findAll({
          include: [
            {
              association: Task.User,
              required: true,
              include: [
                {
                  association: User.Tasks,
                  attributes: ['userId'],
                  separate: true,
                  include: [
                    {
                      association: Task.User,
                      attributes: ['id'],
                      required: true,
                      include: [
                        {
                          association: User.Company,
                        },
                      ],
                    },
                  ],
                },
              ],
            },
          ],
        });

        expect(results.length).to.equal(1);
        expect(results[0].id).to.equal(1);
        expect(results[0].user.id).to.equal(2);
        expect(results[0].user.tasks.length).to.equal(1);
        expect(results[0].user.tasks[0].user.id).to.equal(2);
        expect(results[0].user.tasks[0].user.company.id).to.equal(3);
      });
    });
  });
}
