'use strict';

const chai = require('chai');

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

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

describe(Support.getTestDialectTeaser('associations'), () => {
  describe('scope', () => {
    beforeEach(function () {
      this.Post = this.sequelize.define('post', {});
      this.Image = this.sequelize.define('image', {});
      this.Question = this.sequelize.define('question', {});
      this.Comment = this.sequelize.define('comment', {
        title: DataTypes.STRING,
        type: DataTypes.STRING,
        commentable: DataTypes.STRING,
        commentable_id: DataTypes.INTEGER,
        isMain: {
          field: 'is_main',
          type: DataTypes.BOOLEAN,
          defaultValue: false,
        },
      });

      this.Comment.prototype.getItem = function () {
        return this[`get${upperFirst(this.get('commentable'))}`]();
      };

      this.Post.addScope('withComments', {
        include: [
          {
            model: this.Comment,
            as: 'comments',
          },
        ],
      });
      this.Post.addScope('withMainComment', {
        include: [
          {
            model: this.Comment,
            as: 'mainComment',
          },
        ],
      });
      this.Post.hasMany(this.Comment, {
        foreignKey: 'commentable_id',
        scope: {
          commentable: 'post',
        },
        foreignKeyConstraints: false,
      });
      this.Post.hasMany(this.Comment, {
        foreignKey: 'commentable_id',
        as: 'coloredComments',
        scope: {
          commentable: 'post',
          type: { [Op.in]: ['blue', 'green'] },
        },
        foreignKeyConstraints: false,
      });
      this.Post.hasOne(this.Comment, {
        foreignKey: 'commentable_id',
        as: 'mainComment',
        scope: {
          commentable: 'post',
          isMain: true,
        },
        foreignKeyConstraints: false,
      });
      this.Comment.belongsTo(this.Post, {
        foreignKey: 'commentable_id',
        as: 'post',
        foreignKeyConstraints: false,
      });

      this.Image.hasMany(this.Comment, {
        foreignKey: 'commentable_id',
        scope: {
          commentable: 'image',
        },
        foreignKeyConstraints: false,
      });
      this.Comment.belongsTo(this.Image, {
        foreignKey: 'commentable_id',
        as: 'image',
        foreignKeyConstraints: false,
      });

      this.Question.hasMany(this.Comment, {
        foreignKey: 'commentable_id',
        scope: {
          commentable: 'question',
        },
        foreignKeyConstraints: false,
        inverse: {
          as: 'question',
        },
      });
    });

    describe('1:1', () => {
      it('should create, find and include associations with scope values', async function () {
        await this.sequelize.sync({ force: true });

        const [post1] = await Promise.all([
          this.Post.create(),
          this.Comment.create({
            title: 'I am a comment',
          }),
          this.Comment.create({
            title: 'I am a main comment',
            isMain: true,
          }),
        ]);

        this.post = post1;

        const comment0 = await post1.createComment({
          title: 'I am a post comment',
        });

        expect(comment0.get('commentable')).to.equal('post');
        expect(comment0.get('isMain')).to.be.false;
        const post0 = await this.Post.withScope('withMainComment').findByPk(this.post.get('id'));
        expect(post0.mainComment).to.be.null;

        const mainComment1 = await post0.createMainComment({
          title: 'I am a main post comment',
        });

        this.mainComment = mainComment1;
        expect(mainComment1.get('commentable')).to.equal('post');
        expect(mainComment1.get('isMain')).to.be.true;
        const post = await this.Post.withScope('withMainComment').findByPk(this.post.id);
        expect(post.mainComment.get('id')).to.equal(this.mainComment.get('id'));
        const mainComment0 = await post.getMainComment();
        expect(mainComment0.get('commentable')).to.equal('post');
        expect(mainComment0.get('isMain')).to.be.true;

        const comment = await this.Comment.create({
          title: 'I am a future main comment',
        });

        await this.post.setMainComment(comment);
        const mainComment = await this.post.getMainComment();
        expect(mainComment.get('commentable')).to.equal('post');
        expect(mainComment.get('isMain')).to.be.true;
        expect(mainComment.get('title')).to.equal('I am a future main comment');
      });
      it('should create included association with scope values', async function () {
        await this.sequelize.sync({ force: true });

        const post0 = await this.Post.create(
          {
            mainComment: {
              title: 'I am a main comment created with a post',
            },
          },
          {
            include: [{ model: this.Comment, as: 'mainComment' }],
          },
        );

        expect(post0.mainComment.get('commentable')).to.equal('post');
        expect(post0.mainComment.get('isMain')).to.be.true;
        const post = await this.Post.withScope('withMainComment').findByPk(post0.id);
        expect(post.mainComment.get('commentable')).to.equal('post');
        expect(post.mainComment.get('isMain')).to.be.true;
      });
    });

    describe('1:M', () => {
      it('should create, find and include associations with scope values', async function () {
        await this.sequelize.sync({ force: true });

        const [post1, image1, question1, commentA, commentB] = await Promise.all([
          this.Post.create(),
          this.Image.create(),
          this.Question.create(),
          this.Comment.create({
            title: 'I am a image comment',
          }),
          this.Comment.create({
            title: 'I am a question comment',
          }),
        ]);

        this.post = post1;
        this.image = image1;
        this.question = question1;

        await Promise.all([
          post1.createComment({
            title: 'I am a post comment',
          }),
          image1.addComment(commentA),
          question1.setComments([commentB]),
        ]);

        const comments = await this.Comment.findAll();
        for (const comment of comments) {
          expect(comment.get('commentable')).to.be.ok;
        }

        expect(
          comments
            .map(comment => {
              return comment.get('commentable');
            })
            .sort(),
        ).to.deep.equal(['image', 'post', 'question']);

        const [postComments, imageComments, questionComments] = await Promise.all([
          this.post.getComments(),
          this.image.getComments(),
          this.question.getComments(),
        ]);

        expect(postComments.length).to.equal(1);
        expect(postComments[0].get('title')).to.equal('I am a post comment');
        expect(imageComments.length).to.equal(1);
        expect(imageComments[0].get('title')).to.equal('I am a image comment');
        expect(questionComments.length).to.equal(1);
        expect(questionComments[0].get('title')).to.equal('I am a question comment');

        const [postComment, imageComment, questionComment] = [
          postComments[0],
          imageComments[0],
          questionComments[0],
        ];
        const [post0, image0, question0] = await Promise.all([
          postComment.getItem(),
          imageComment.getItem(),
          questionComment.getItem(),
        ]);
        expect(post0).to.be.instanceof(this.Post);
        expect(image0).to.be.instanceof(this.Image);
        expect(question0).to.be.instanceof(this.Question);

        const [post, image, question] = await Promise.all([
          this.Post.findOne({
            include: [
              {
                model: this.Comment,
                as: 'comments',
              },
            ],
          }),
          this.Image.findOne({
            include: [
              {
                model: this.Comment,
                as: 'comments',
              },
            ],
          }),
          this.Question.findOne({
            include: [
              {
                model: this.Comment,
                as: 'comments',
              },
            ],
          }),
        ]);

        expect(post.comments.length).to.equal(1);
        expect(post.comments[0].get('title')).to.equal('I am a post comment');
        expect(image.comments.length).to.equal(1);
        expect(image.comments[0].get('title')).to.equal('I am a image comment');
        expect(question.comments.length).to.equal(1);
        expect(question.comments[0].get('title')).to.equal('I am a question comment');
      });
      it('should make the same query if called multiple time (#4470)', async function () {
        const logs = [];
        const logging = function (log) {
          // removing 'executing(<uuid> || 'default'}) :' from logs
          logs.push(log.slice(Math.max(0, log.indexOf(':') + 1)));
        };

        await this.sequelize.sync({ force: true });
        const post = await this.Post.create();

        await post.createComment({
          title: 'I am a post comment',
        });

        await this.Post.withScope('withComments').findAll({
          logging,
        });

        await this.Post.withScope('withComments').findAll({
          logging,
        });

        expect(logs[0]).to.equal(logs[1]);
      });
      it('should created included association with scope values', async function () {
        await this.sequelize.sync({ force: true });
        let post = await this.Post.create(
          {
            comments: [
              {
                title: 'I am a comment created with a post',
              },
              {
                title: 'I am a second comment created with a post',
              },
            ],
          },
          {
            include: [{ model: this.Comment, as: 'comments' }],
          },
        );
        this.post = post;
        for (const comment of post.comments) {
          expect(comment.get('commentable')).to.equal('post');
        }

        post = await this.Post.withScope('withComments').findByPk(this.post.id);
        for (const comment of post.comments) {
          expect(comment.get('commentable')).to.equal('post');
        }
      });
      it('should include associations with operator scope values', async function () {
        await this.sequelize.sync({ force: true });

        const [post0, commentA, commentB, commentC] = await Promise.all([
          this.Post.create(),
          this.Comment.create({
            title: 'I am a blue comment',
            type: 'blue',
          }),
          this.Comment.create({
            title: 'I am a red comment',
            type: 'red',
          }),
          this.Comment.create({
            title: 'I am a green comment',
            type: 'green',
          }),
        ]);

        this.post = post0;
        await post0.addComments([commentA, commentB, commentC]);

        const post = await this.Post.findByPk(this.post.id, {
          include: [
            {
              model: this.Comment,
              as: 'coloredComments',
            },
          ],
        });

        expect(post.coloredComments.length).to.equal(2);
        for (const comment of post.coloredComments) {
          expect(comment.type).to.match(/blue|green/);
        }
      });
      it('should not mutate scope when running SELECT query (#12868)', async function () {
        await this.sequelize.sync({ force: true });
        await this.Post.findOne({
          where: {},
          include: [
            {
              association: this.Post.associations.mainComment,
              attributes: ['id'],
              required: true,
              where: {},
            },
          ],
        });
        expect(this.Post.associations.mainComment.scope.isMain).to.equal(true);
      });
    });

    if (Support.getTestDialect() !== 'sqlite3') {
      describe('N:M', () => {
        describe('on the target', () => {
          beforeEach(function () {
            this.Post = this.sequelize.define('post', {});
            this.Tag = this.sequelize.define('tag', {
              type: DataTypes.STRING,
            });
            this.PostTag = this.sequelize.define('post_tag');

            this.Post.belongsToMany(this.Tag, {
              as: 'categories',
              through: this.PostTag,
              scope: { type: 'category' },
            });
            this.Post.belongsToMany(this.Tag, {
              as: 'tags',
              through: this.PostTag,
              scope: { type: 'tag' },
            });

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

          it('should create, find and include associations with scope values', async function () {
            const [postA0, postB0, postC0, categoryA, categoryB, tagA, tagB] = await Promise.all([
              this.Post.create(),
              this.Post.create(),
              this.Post.create(),
              this.Tag.create({ type: 'category' }),
              this.Tag.create({ type: 'category' }),
              this.Tag.create({ type: 'tag' }),
              this.Tag.create({ type: 'tag' }),
            ]);

            this.postA = postA0;
            this.postB = postB0;
            this.postC = postC0;

            await Promise.all([
              postA0.addCategory(categoryA),
              postA0.createTag(),
              postB0.addTag(tagA),
              postC0.createCategory(),
            ]);

            // we're calling 'setX' methods after the different 'addX' methods because
            // setCategories is not supposed to overwrite tags and vice-versa.
            // tags & categories use the same through table so this could happen is the association scope is not handled correctly.
            await postB0.setCategories([categoryB]);
            await postC0.setTags([tagB]);

            const [
              postACategories,
              postBCategories,
              postCCategories,
              postATags,
              postBTags,
              postCTags,
            ] = await Promise.all([
              this.postA.getCategories(),
              this.postB.getCategories(),
              this.postC.getCategories(),
              this.postA.getTags(),
              this.postB.getTags(),
              this.postC.getTags(),
            ]);

            expect([
              postACategories.length,
              postATags.length,
              postBCategories.length,
              postBTags.length,
              postCCategories.length,
              postCTags.length,
            ]).to.eql([1, 1, 1, 1, 1, 1]);

            expect([
              postACategories[0].get('type'),
              postATags[0].get('type'),
              postBCategories[0].get('type'),
              postBTags[0].get('type'),
              postCCategories[0].get('type'),
              postCTags[0].get('type'),
            ]).to.eql(['category', 'tag', 'category', 'tag', 'category', 'tag']);

            const [postA, postB, postC] = await Promise.all([
              this.Post.findOne({
                where: {
                  id: this.postA.get('id'),
                },
                include: [
                  { model: this.Tag, as: 'tags' },
                  { model: this.Tag, as: 'categories' },
                ],
              }),
              this.Post.findOne({
                where: {
                  id: this.postB.get('id'),
                },
                include: [
                  { model: this.Tag, as: 'tags' },
                  { model: this.Tag, as: 'categories' },
                ],
              }),
              this.Post.findOne({
                where: {
                  id: this.postC.get('id'),
                },
                include: [
                  { model: this.Tag, as: 'tags' },
                  { model: this.Tag, as: 'categories' },
                ],
              }),
            ]);

            expect(postA.get('categories').length).to.equal(1);
            expect(postA.get('tags').length).to.equal(1);
            expect(postB.get('categories').length).to.equal(1);
            expect(postB.get('tags').length).to.equal(1);
            expect(postC.get('categories').length).to.equal(1);
            expect(postC.get('tags').length).to.equal(1);

            expect(postA.get('categories')[0].get('type')).to.equal('category');
            expect(postA.get('tags')[0].get('type')).to.equal('tag');
            expect(postB.get('categories')[0].get('type')).to.equal('category');
            expect(postB.get('tags')[0].get('type')).to.equal('tag');
            expect(postC.get('categories')[0].get('type')).to.equal('category');
            expect(postC.get('tags')[0].get('type')).to.equal('tag');
          });
        });

        describe('on the through model', () => {
          beforeEach(function () {
            this.Post = this.sequelize.define('post', {});
            this.Image = this.sequelize.define('image', {});
            this.Question = this.sequelize.define('question', {});

            this.ItemTag = this.sequelize.define('item_tag', {
              id: {
                type: DataTypes.INTEGER,
                primaryKey: true,
                autoIncrement: true,
              },
              tag_id: {
                type: DataTypes.INTEGER,
                unique: 'item_tag_taggable',
              },
              taggable: {
                type: DataTypes.STRING,
                unique: 'item_tag_taggable',
              },
              taggable_id: {
                type: DataTypes.INTEGER,
                unique: 'item_tag_taggable',
                references: null,
              },
            });
            this.Tag = this.sequelize.define('tag', {
              name: DataTypes.STRING,
            });

            this.Post.belongsToMany(this.Tag, {
              through: {
                model: this.ItemTag,
                unique: false,
                scope: {
                  taggable: 'post',
                },
              },
              foreignKey: 'taggable_id',
              otherKey: 'tag_id',
              foreignKeyConstraints: false,
              inverse: {
                foreignKeyConstraints: false,
              },
            });

            this.Image.belongsToMany(this.Tag, {
              through: {
                model: this.ItemTag,
                unique: false,
                scope: {
                  taggable: 'image',
                },
              },
              foreignKey: 'taggable_id',
              otherKey: 'tag_id',
              foreignKeyConstraints: false,
              inverse: {
                foreignKeyConstraints: false,
              },
            });

            this.Question.belongsToMany(this.Tag, {
              through: {
                model: this.ItemTag,
                unique: false,
                scope: {
                  taggable: 'question',
                },
              },
              foreignKey: 'taggable_id',
              otherKey: 'tag_id',
              foreignKeyConstraints: false,
              inverse: {
                foreignKeyConstraints: false,
              },
            });
          });

          it('should create, find and include associations with scope values', async function () {
            await Promise.all([
              this.Post.sync({ force: true }),
              this.Image.sync({ force: true }),
              this.Question.sync({ force: true }),
              this.Tag.sync({ force: true }),
            ]);

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

            const [post0, image0, question0, tagA, tagB, tagC] = await Promise.all([
              this.Post.create(),
              this.Image.create(),
              this.Question.create(),
              this.Tag.create({ name: 'tagA' }),
              this.Tag.create({ name: 'tagB' }),
              this.Tag.create({ name: 'tagC' }),
            ]);

            this.post = post0;
            this.image = image0;
            this.question = question0;

            await Promise.all([
              post0.setTags([tagA]).then(async () => {
                return Promise.all([post0.createTag({ name: 'postTag' }), post0.addTag(tagB)]);
              }),
              image0.setTags([tagB]).then(async () => {
                return Promise.all([image0.createTag({ name: 'imageTag' }), image0.addTag(tagC)]);
              }),
              question0.setTags([tagC]).then(async () => {
                return Promise.all([
                  question0.createTag({ name: 'questionTag' }),
                  question0.addTag(tagA),
                ]);
              }),
            ]);

            const [postTags, imageTags, questionTags] = await Promise.all([
              this.post.getTags(),
              this.image.getTags(),
              this.question.getTags(),
            ]);
            expect(postTags.length).to.equal(3);
            expect(imageTags.length).to.equal(3);
            expect(questionTags.length).to.equal(3);

            expect(
              postTags
                .map(tag => {
                  return tag.name;
                })
                .sort(),
            ).to.deep.equal(['postTag', 'tagA', 'tagB']);

            expect(
              imageTags
                .map(tag => {
                  return tag.name;
                })
                .sort(),
            ).to.deep.equal(['imageTag', 'tagB', 'tagC']);

            expect(
              questionTags
                .map(tag => {
                  return tag.name;
                })
                .sort(),
            ).to.deep.equal(['questionTag', 'tagA', 'tagC']);

            const [post, image, question] = await Promise.all([
              this.Post.findOne({
                where: {},
                include: [this.Tag],
              }),
              this.Image.findOne({
                where: {},
                include: [this.Tag],
              }),
              this.Question.findOne({
                where: {},
                include: [this.Tag],
              }),
            ]);

            expect(post.tags.length).to.equal(3);
            expect(image.tags.length).to.equal(3);
            expect(question.tags.length).to.equal(3);

            expect(
              post.tags
                .map(tag => {
                  return tag.name;
                })
                .sort(),
            ).to.deep.equal(['postTag', 'tagA', 'tagB']);

            expect(
              image.tags
                .map(tag => {
                  return tag.name;
                })
                .sort(),
            ).to.deep.equal(['imageTag', 'tagB', 'tagC']);

            expect(
              question.tags
                .map(tag => {
                  return tag.name;
                })
                .sort(),
            ).to.deep.equal(['questionTag', 'tagA', 'tagC']);
          });
        });
      });
    }
  });
});
