'use strict';

const chai = require('chai');

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

describe(Support.getTestDialectTeaser('Include'), () => {
  describe('LIMIT', () => {
    /*
     * shortcut for building simple {name: 'foo'} seed data
     */
    function build(...args) {
      return args.map(arg => ({ name: arg }));
    }

    /*
     * association overview
     * [Task]N---N[Project]N---N[User]N---N[Hobby]
     *                            1
     *                            |
     *                            |
     *                            |
     *                            N
     *            [Comment]N---1[Post]N---N[Tag]N---1[Color]
     *                            1
     *                            |
     *                            |
     *                            |
     *                            N
     *                        [Footnote]
     */
    beforeEach(function () {
      this.Project = this.sequelize.define(
        'Project',
        {
          name: {
            type: DataTypes.STRING,
            primaryKey: true,
          },
        },
        { timestamps: false },
      );

      this.User = this.sequelize.define(
        'User',
        {
          name: {
            type: DataTypes.STRING,
            primaryKey: true,
          },
        },
        { timestamps: false },
      );

      this.Task = this.sequelize.define(
        'Task',
        {
          name: {
            type: DataTypes.STRING,
            primaryKey: true,
          },
        },
        { timestamps: false },
      );

      this.Hobby = this.sequelize.define(
        'Hobby',
        {
          name: {
            type: DataTypes.STRING,
            primaryKey: true,
          },
        },
        { timestamps: false },
      );

      this.User.belongsToMany(this.Project, { through: 'user_project' });
      this.Project.belongsToMany(this.User, { through: 'user_project' });

      this.Project.belongsToMany(this.Task, { through: 'task_project' });
      this.Task.belongsToMany(this.Project, { through: 'task_project' });

      this.User.belongsToMany(this.Hobby, { through: 'user_hobby' });
      this.Hobby.belongsToMany(this.User, { through: 'user_hobby' });

      this.Post = this.sequelize.define(
        'Post',
        {
          name: {
            type: DataTypes.STRING,
            primaryKey: true,
          },
        },
        { timestamps: false },
      );

      this.Comment = this.sequelize.define(
        'Comment',
        {
          name: {
            type: DataTypes.STRING,
            primaryKey: true,
          },
        },
        { timestamps: false },
      );

      this.Tag = this.sequelize.define(
        'Tag',
        {
          name: {
            type: DataTypes.STRING,
            primaryKey: true,
          },
        },
        { timestamps: false },
      );

      this.Color = this.sequelize.define(
        'Color',
        {
          name: {
            type: DataTypes.STRING,
            primaryKey: true,
          },
        },
        { timestamps: false },
      );

      this.Footnote = this.sequelize.define(
        'Footnote',
        {
          name: {
            type: DataTypes.STRING,
            primaryKey: true,
          },
        },
        { timestamps: false },
      );

      this.Post.hasMany(this.Comment);
      this.Comment.belongsTo(this.Post);

      this.Post.belongsToMany(this.Tag, { through: 'post_tag' });
      this.Tag.belongsToMany(this.Post, { through: 'post_tag' });

      this.Post.hasMany(this.Footnote);
      this.Footnote.belongsTo(this.Post);

      this.User.hasMany(this.Post);
      this.Post.belongsTo(this.User);

      this.Tag.belongsTo(this.Color);
      this.Color.hasMany(this.Tag);
    });

    /*
     * many-to-many
     */
    it('supports many-to-many association with where clause', async function () {
      await this.sequelize.sync({ force: true });

      const [projects, users] = await Promise.all([
        this.Project.bulkCreate(build('alpha', 'bravo', 'charlie')),
        this.User.bulkCreate(build('Alice', 'Bob')),
      ]);

      await Promise.all([
        projects[0].addUser(users[0]),
        projects[1].addUser(users[1]),
        projects[2].addUser(users[0]),
      ]);

      const result = await this.Project.findAll({
        include: [
          {
            model: this.User,
            where: {
              name: 'Alice',
            },
          },
        ],
        order: ['name'],
        limit: 1,
        offset: 1,
      });

      expect(result.length).to.equal(1);
      expect(result[0].name).to.equal('charlie');
    });

    it('supports 2 levels of required many-to-many associations', async function () {
      await this.sequelize.sync({ force: true });

      const [projects, users, hobbies] = await Promise.all([
        this.Project.bulkCreate(build('alpha', 'bravo', 'charlie')),
        this.User.bulkCreate(build('Alice', 'Bob')),
        this.Hobby.bulkCreate(build('archery', 'badminton')),
      ]);

      await Promise.all([
        projects[0].addUser(users[0]),
        projects[1].addUser(users[1]),
        projects[2].addUser(users[0]),
        users[0].addHobby(hobbies[0]),
      ]);

      const result = await this.Project.findAll({
        include: [
          {
            model: this.User,
            required: true,
            include: [
              {
                model: this.Hobby,
                required: true,
              },
            ],
          },
        ],
        order: ['name'],
        limit: 1,
        offset: 1,
      });

      expect(result.length).to.equal(1);
      expect(result[0].name).to.equal('charlie');
    });

    it('supports 2 levels of required many-to-many associations with where clause', async function () {
      await this.sequelize.sync({ force: true });

      const [projects, users, hobbies] = await Promise.all([
        this.Project.bulkCreate(build('alpha', 'bravo', 'charlie')),
        this.User.bulkCreate(build('Alice', 'Bob')),
        this.Hobby.bulkCreate(build('archery', 'badminton')),
      ]);

      await Promise.all([
        projects[0].addUser(users[0]),
        projects[1].addUser(users[1]),
        projects[2].addUser(users[0]),
        users[0].addHobby(hobbies[0]),
        users[1].addHobby(hobbies[1]),
      ]);

      const result = await this.Project.findAll({
        include: [
          {
            model: this.User,
            required: true,
            include: [
              {
                model: this.Hobby,
                where: {
                  name: 'archery',
                },
              },
            ],
          },
        ],
        order: ['name'],
        limit: 1,
        offset: 1,
      });

      expect(result.length).to.equal(1);
      expect(result[0].name).to.equal('charlie');
    });

    it('supports 2 levels of required many-to-many associations with through.where clause', async function () {
      await this.sequelize.sync({ force: true });

      const [projects, users, hobbies] = await Promise.all([
        this.Project.bulkCreate(build('alpha', 'bravo', 'charlie')),
        this.User.bulkCreate(build('Alice', 'Bob')),
        this.Hobby.bulkCreate(build('archery', 'badminton')),
      ]);

      await Promise.all([
        projects[0].addUser(users[0]),
        projects[1].addUser(users[1]),
        projects[2].addUser(users[0]),
        users[0].addHobby(hobbies[0]),
        users[1].addHobby(hobbies[1]),
      ]);

      const result = await this.Project.findAll({
        include: [
          {
            model: this.User,
            required: true,
            include: [
              {
                model: this.Hobby,
                required: true,
                through: {
                  where: {
                    hobbyName: 'archery',
                  },
                },
              },
            ],
          },
        ],
        order: ['name'],
        limit: 1,
        offset: 1,
      });

      expect(result.length).to.equal(1);
      expect(result[0].name).to.equal('charlie');
    });

    it('supports 3 levels of required many-to-many associations with where clause', async function () {
      await this.sequelize.sync({ force: true });

      const [tasks, projects, users, hobbies] = await Promise.all([
        this.Task.bulkCreate(build('alpha', 'bravo', 'charlie')),
        this.Project.bulkCreate(build('alpha', 'bravo', 'charlie')),
        this.User.bulkCreate(build('Alice', 'Bob', 'Charlotte')),
        this.Hobby.bulkCreate(build('archery', 'badminton')),
      ]);

      await Promise.all([
        tasks[0].addProject(projects[0]),
        tasks[1].addProject(projects[1]),
        tasks[2].addProject(projects[2]),
        projects[0].addUser(users[0]),
        projects[1].addUser(users[1]),
        projects[2].addUser(users[0]),
        users[0].addHobby(hobbies[0]),
        users[1].addHobby(hobbies[1]),
      ]);

      const result = await this.Task.findAll({
        include: [
          {
            model: this.Project,
            required: true,
            include: [
              {
                model: this.User,
                required: true,
                include: [
                  {
                    model: this.Hobby,
                    where: {
                      name: 'archery',
                    },
                  },
                ],
              },
            ],
          },
        ],
        order: ['name'],
        limit: 1,
        offset: 1,
      });

      expect(result.length).to.equal(1);
      expect(result[0].name).to.equal('charlie');
    });

    it('supports required many-to-many association', async function () {
      await this.sequelize.sync({ force: true });

      const [projects, users] = await Promise.all([
        this.Project.bulkCreate(build('alpha', 'bravo', 'charlie')),
        this.User.bulkCreate(build('Alice', 'Bob')),
      ]);

      await Promise.all([
        // alpha
        projects[0].addUser(users[0]), // charlie
        projects[2].addUser(users[0]),
      ]);

      const result = await this.Project.findAll({
        include: [
          {
            model: this.User,
            required: true,
          },
        ],
        order: ['name'],
        limit: 1,
        offset: 1,
      });

      expect(result.length).to.equal(1);
      expect(result[0].name).to.equal('charlie');
    });

    it('supports 2 required many-to-many association', async function () {
      await this.sequelize.sync({ force: true });

      const [projects, users, tasks] = await Promise.all([
        this.Project.bulkCreate(build('alpha', 'bravo', 'charlie', 'delta')),
        this.User.bulkCreate(build('Alice', 'Bob', 'David')),
        this.Task.bulkCreate(build('a', 'c', 'd')),
      ]);

      await Promise.all([
        projects[0].addUser(users[0]),
        projects[0].addTask(tasks[0]),
        projects[1].addUser(users[1]),
        projects[2].addTask(tasks[1]),
        projects[3].addUser(users[2]),
        projects[3].addTask(tasks[2]),
      ]);

      const result = await this.Project.findAll({
        include: [
          {
            model: this.User,
            required: true,
          },
          {
            model: this.Task,
            required: true,
          },
        ],
        order: ['name'],
        limit: 1,
        offset: 1,
      });

      expect(result.length).to.equal(1);
      expect(result[0].name).to.equal('delta');
    });

    /*
     * one-to-many
     */
    it('supports required one-to-many association', async function () {
      await this.sequelize.sync({ force: true });

      const [posts, comments] = await Promise.all([
        this.Post.bulkCreate(build('alpha', 'bravo', 'charlie')),
        this.Comment.bulkCreate(build('comment0', 'comment1')),
      ]);

      await Promise.all([posts[0].addComment(comments[0]), posts[2].addComment(comments[1])]);

      const result = await this.Post.findAll({
        include: [
          {
            model: this.Comment,
            required: true,
          },
        ],
        order: ['name'],
        limit: 1,
        offset: 1,
      });

      expect(result.length).to.equal(1);
      expect(result[0].name).to.equal('charlie');
    });

    it('supports required one-to-many association with where clause', async function () {
      await this.sequelize.sync({ force: true });

      const [posts, comments] = await Promise.all([
        this.Post.bulkCreate(build('alpha', 'bravo', 'charlie')),
        this.Comment.bulkCreate(build('comment0', 'comment1', 'comment2')),
      ]);

      await Promise.all([
        posts[0].addComment(comments[0]),
        posts[1].addComment(comments[1]),
        posts[2].addComment(comments[2]),
      ]);

      const result = await this.Post.findAll({
        include: [
          {
            model: this.Comment,
            required: true,
            where: {
              [Op.or]: [
                {
                  name: 'comment0',
                },
                {
                  name: 'comment2',
                },
              ],
            },
          },
        ],
        order: ['name'],
        limit: 1,
        offset: 1,
      });

      expect(result.length).to.equal(1);
      expect(result[0].name).to.equal('charlie');
    });

    it('supports required one-to-many association with where clause (findOne)', async function () {
      await this.sequelize.sync({ force: true });

      const [posts, comments] = await Promise.all([
        this.Post.bulkCreate(build('alpha', 'bravo', 'charlie')),
        this.Comment.bulkCreate(build('comment0', 'comment1', 'comment2')),
      ]);

      await Promise.all([
        posts[0].addComment(comments[0]),
        posts[1].addComment(comments[1]),
        posts[2].addComment(comments[2]),
      ]);

      const post = await this.Post.findOne({
        include: [
          {
            model: this.Comment,
            required: true,
            where: {
              name: 'comment2',
            },
          },
        ],
      });

      expect(post.name).to.equal('charlie');
    });

    it('supports 2 levels of required one-to-many associations', async function () {
      await this.sequelize.sync({ force: true });

      const [users, posts, comments] = await Promise.all([
        this.User.bulkCreate(build('Alice', 'Bob', 'Charlotte', 'David')),
        this.Post.bulkCreate(build('post0', 'post1', 'post2')),
        this.Comment.bulkCreate(build('comment0', 'comment1', 'comment2')),
      ]);

      await Promise.all([
        users[0].addPost(posts[0]),
        users[1].addPost(posts[1]),
        users[3].addPost(posts[2]),
        posts[0].addComment(comments[0]),
        posts[2].addComment(comments[2]),
      ]);

      const result = await this.User.findAll({
        include: [
          {
            model: this.Post,
            required: true,
            include: [
              {
                model: this.Comment,
                required: true,
              },
            ],
          },
        ],
        order: ['name'],
        limit: 1,
        offset: 1,
      });

      expect(result.length).to.equal(1);
      expect(result[0].name).to.equal('David');
    });

    /*
     * mixed many-to-many, one-to-many and many-to-one
     */
    it('supports required one-to-many association with nested required many-to-many association', async function () {
      await this.sequelize.sync({ force: true });

      const [users, posts, tags] = await Promise.all([
        this.User.bulkCreate(build('Alice', 'Bob', 'Charlotte', 'David')),
        this.Post.bulkCreate(build('alpha', 'charlie', 'delta')),
        this.Tag.bulkCreate(build('atag', 'btag', 'dtag')),
      ]);

      await Promise.all([
        users[0].addPost(posts[0]),
        users[2].addPost(posts[1]),
        users[3].addPost(posts[2]),
        posts[0].addTag([tags[0]]),
        posts[2].addTag([tags[2]]),
      ]);

      const result = await this.User.findAll({
        include: [
          {
            model: this.Post,
            required: true,
            include: [
              {
                model: this.Tag,
                required: true,
              },
            ],
          },
        ],
        order: ['name'],
        limit: 1,
        offset: 1,
      });

      expect(result.length).to.equal(1);
      expect(result[0].name).to.equal('David');
    });

    it('supports required many-to-many association with nested required one-to-many association', async function () {
      await this.sequelize.sync({ force: true });

      const [projects, users, posts] = await Promise.all([
        this.Project.bulkCreate(build('alpha', 'bravo', 'charlie', 'delta')),
        this.User.bulkCreate(build('Alice', 'Bob', 'David')),
        this.Post.bulkCreate(build('post0', 'post1', 'post2')),
      ]);

      await Promise.all([
        projects[0].addUser(users[0]),
        projects[1].addUser(users[1]),
        projects[3].addUser(users[2]),
        users[0].addPost([posts[0]]),
        users[2].addPost([posts[2]]),
      ]);

      const result = await this.Project.findAll({
        include: [
          {
            model: this.User,
            required: true,
            include: [
              {
                model: this.Post,
                required: true,
                duplicating: true,
              },
            ],
          },
        ],
        order: ['name'],
        limit: 1,
        offset: 1,
      });

      expect(result.length).to.equal(1);
      expect(result[0].name).to.equal('delta');
    });

    it('supports required many-to-one association with nested many-to-many association with where clause', async function () {
      await this.sequelize.sync({ force: true });

      const [posts, users, hobbies] = await Promise.all([
        this.Post.bulkCreate(build('post0', 'post1', 'post2', 'post3')),
        this.User.bulkCreate(build('Alice', 'Bob', 'Charlotte', 'David')),
        this.Hobby.bulkCreate(build('archery', 'badminton')),
      ]);

      await Promise.all([
        posts[0].setUser(users[0]),
        posts[1].setUser(users[1]),
        posts[3].setUser(users[3]),
        users[0].addHobby(hobbies[0]),
        users[1].addHobby(hobbies[1]),
        users[3].addHobby(hobbies[0]),
      ]);

      const result = await this.Post.findAll({
        include: [
          {
            model: this.User,
            required: true,
            include: [
              {
                model: this.Hobby,
                where: {
                  name: 'archery',
                },
              },
            ],
          },
        ],
        order: ['name'],
        limit: 1,
        offset: 1,
      });

      expect(result.length).to.equal(1);
      expect(result[0].name).to.equal('post3');
    });

    it('supports required many-to-one association with nested many-to-many association with through.where clause', async function () {
      await this.sequelize.sync({ force: true });

      const [posts, users, hobbies] = await Promise.all([
        this.Post.bulkCreate(build('post0', 'post1', 'post2', 'post3')),
        this.User.bulkCreate(build('Alice', 'Bob', 'Charlotte', 'David')),
        this.Hobby.bulkCreate(build('archery', 'badminton')),
      ]);

      await Promise.all([
        posts[0].setUser(users[0]),
        posts[1].setUser(users[1]),
        posts[3].setUser(users[3]),
        users[0].addHobby(hobbies[0]),
        users[1].addHobby(hobbies[1]),
        users[3].addHobby(hobbies[0]),
      ]);

      const result = await this.Post.findAll({
        include: [
          {
            model: this.User,
            required: true,
            include: [
              {
                model: this.Hobby,
                required: true,
                through: {
                  where: {
                    hobbyName: 'archery',
                  },
                },
              },
            ],
          },
        ],
        order: ['name'],
        limit: 1,
        offset: 1,
      });

      expect(result.length).to.equal(1);
      expect(result[0].name).to.equal('post3');
    });

    it('supports required many-to-one association with multiple nested associations with where clause', async function () {
      await this.sequelize.sync({ force: true });

      const [comments, posts, users, tags] = await Promise.all([
        this.Comment.bulkCreate(
          build('comment0', 'comment1', 'comment2', 'comment3', 'comment4', 'comment5'),
        ),
        this.Post.bulkCreate(build('post0', 'post1', 'post2', 'post3', 'post4')),
        this.User.bulkCreate(build('Alice', 'Bob')),
        this.Tag.bulkCreate(build('tag0', 'tag1')),
      ]);

      await Promise.all([
        comments[0].setPost(posts[0]),
        comments[1].setPost(posts[1]),
        comments[3].setPost(posts[2]),
        comments[4].setPost(posts[3]),
        comments[5].setPost(posts[4]),
        posts[0].addTag(tags[0]),
        posts[3].addTag(tags[0]),
        posts[4].addTag(tags[0]),
        posts[1].addTag(tags[1]),
        posts[0].setUser(users[0]),
        posts[2].setUser(users[0]),
        posts[4].setUser(users[0]),
        posts[1].setUser(users[1]),
      ]);

      const result = await this.Comment.findAll({
        include: [
          {
            model: this.Post,
            required: true,
            include: [
              {
                model: this.User,
                where: {
                  name: 'Alice',
                },
              },
              {
                model: this.Tag,
                where: {
                  name: 'tag0',
                },
              },
            ],
          },
        ],
        order: ['name'],
        limit: 1,
        offset: 1,
      });

      expect(result.length).to.equal(1);
      expect(result[0].name).to.equal('comment5');
    });

    it('supports required many-to-one association with nested one-to-many association with where clause', async function () {
      await this.sequelize.sync({ force: true });

      const [comments, posts, footnotes] = await Promise.all([
        this.Comment.bulkCreate(build('comment0', 'comment1', 'comment2')),
        this.Post.bulkCreate(build('post0', 'post1', 'post2')),
        this.Footnote.bulkCreate(build('footnote0', 'footnote1', 'footnote2')),
      ]);

      await Promise.all([
        comments[0].setPost(posts[0]),
        comments[1].setPost(posts[1]),
        comments[2].setPost(posts[2]),
        posts[0].addFootnote(footnotes[0]),
        posts[1].addFootnote(footnotes[1]),
        posts[2].addFootnote(footnotes[2]),
      ]);

      const result = await this.Comment.findAll({
        include: [
          {
            model: this.Post,
            required: true,
            include: [
              {
                model: this.Footnote,
                where: {
                  [Op.or]: [
                    {
                      name: 'footnote0',
                    },
                    {
                      name: 'footnote2',
                    },
                  ],
                },
              },
            ],
          },
        ],
        order: ['name'],
        limit: 1,
        offset: 1,
      });

      expect(result.length).to.equal(1);
      expect(result[0].name).to.equal('comment2');
    });
  });
});
