'use strict';

const chai = require('chai');

const expect = chai.expect;
const Support = require('../../support');
const { DataTypes, Op, Sequelize } = require('@sequelize/core');
const {
  _validateIncludedElements,
} = require('@sequelize/core/_non-semver-use-at-your-own-risk_/model-internals.js');

const current = Support.sequelize;

describe(Support.getTestDialectTeaser('Model'), () => {
  describe('all', () => {
    it('can expand nested self-reference', () => {
      const Referral = current.define('referral');

      Referral.belongsTo(Referral);

      const options = { include: [{ all: true, nested: true }] };

      Referral._expandIncludeAll(options, Referral);

      expect(options.include).to.deep.equal([
        { as: 'referral', model: Referral, association: Referral.associations.referral },
      ]);
    });
  });

  describe('_validateIncludedElements', () => {
    beforeEach(function () {
      this.User = this.sequelize.define('User');
      this.Task = this.sequelize.define('Task', {
        title: DataTypes.STRING,
      });
      this.Company = this.sequelize.define('Company', {
        id: {
          type: DataTypes.INTEGER,
          primaryKey: true,
          autoIncrement: true,
          field: 'field_id',
        },
        name: DataTypes.STRING,
      });

      this.User.Tasks = this.User.hasMany(this.Task);
      this.User.Company = this.User.belongsTo(this.Company);
      this.Company.Employees = this.Company.hasMany(this.User);
      this.Company.Owner = this.Company.belongsTo(this.User, {
        as: 'Owner',
        foreignKey: 'ownerId',
      });
    });

    describe('attributes', () => {
      it("should not inject the aliased PK again, if it's already there", function () {
        let options = _validateIncludedElements({
          model: this.User,
          include: [
            {
              model: this.Company,
              attributes: ['name'],
            },
          ],
        });

        expect(options.include[0].attributes).to.deep.equal([['field_id', 'id'], 'name']);

        options = _validateIncludedElements(options);

        // Calling validate again shouldn't add the pk again
        expect(options.include[0].attributes).to.deep.equal([['field_id', 'id'], 'name']);
      });

      describe('include / exclude', () => {
        it('allows me to include additional attributes', function () {
          const options = _validateIncludedElements({
            model: this.User,
            include: [
              {
                model: this.Company,
                attributes: {
                  include: ['foobar'],
                },
              },
            ],
          });

          expect(options.include[0].attributes).to.deep.equal([
            ['field_id', 'id'],
            'name',
            'createdAt',
            'updatedAt',
            'ownerId',
            'foobar',
          ]);
        });

        it('allows me to exclude attributes', function () {
          const options = _validateIncludedElements({
            model: this.User,
            include: [
              {
                model: this.Company,
                attributes: {
                  exclude: ['name'],
                },
              },
            ],
          });

          expect(options.include[0].attributes).to.deep.equal([
            ['field_id', 'id'],
            'createdAt',
            'updatedAt',
            'ownerId',
          ]);
        });

        it('include takes precendence over exclude', function () {
          const options = _validateIncludedElements({
            model: this.User,
            include: [
              {
                model: this.Company,
                attributes: {
                  exclude: ['name'],
                  include: ['name'],
                },
              },
            ],
          });

          expect(options.include[0].attributes).to.deep.equal([
            ['field_id', 'id'],
            'createdAt',
            'updatedAt',
            'ownerId',
            'name',
          ]);
        });
      });
    });

    describe('scope', () => {
      beforeEach(function () {
        this.Project = this.sequelize.define(
          'project',
          {
            bar: {
              type: DataTypes.STRING,
              field: 'foo',
            },
          },
          {
            defaultScope: {
              where: {
                active: true,
              },
            },
            scopes: {
              this: {
                where: { this: true },
              },
              that: {
                where: { that: false },
                limit: 12,
              },
              attr: {
                attributes: ['baz'],
              },
              foobar: {
                where: {
                  bar: 42,
                },
              },
            },
          },
        );

        this.User.hasMany(this.Project);

        this.User.hasMany(this.Project.withScope('this'), { as: 'thisProject' });
      });

      it('adds the default scope to where', function () {
        const options = _validateIncludedElements({
          model: this.User,
          include: [{ model: this.Project, as: 'projects' }],
        });

        expect(options.include[0]).to.have.property('where').which.deep.equals({ active: true });
      });

      it('adds the where from a scoped model', function () {
        const options = _validateIncludedElements({
          model: this.User,
          include: [{ model: this.Project.withScope('that'), as: 'projects' }],
        });

        expect(options.include[0]).to.have.property('where').which.deep.equals({ that: false });
        expect(options.include[0]).to.have.property('limit').which.equals(12);
      });

      it('adds the attributes from a scoped model', function () {
        const options = _validateIncludedElements({
          model: this.User,
          include: [{ model: this.Project.withScope('attr'), as: 'projects' }],
        });

        expect(options.include[0]).to.have.property('attributes').which.deep.equals(['baz']);
      });

      it('merges where with the where from a scoped model', function () {
        const options = _validateIncludedElements({
          model: this.User,
          include: [
            { where: { active: false }, model: this.Project.withScope('that'), as: 'projects' },
          ],
        });

        expect(options.include[0].where).to.deep.equal({
          [Op.and]: [{ that: false }, { active: false }],
        });
      });

      it('add the where from a scoped associated model', function () {
        const options = _validateIncludedElements({
          model: this.User,
          include: [{ model: this.Project, as: 'thisProject' }],
        });

        expect(options.include[0]).to.have.property('where').which.deep.equals({ this: true });
      });

      it('handles a scope with an aliased column (.field)', function () {
        const options = _validateIncludedElements({
          model: this.User,
          include: [{ model: this.Project.withScope('foobar'), as: 'projects' }],
        });

        expect(options.include[0]).to.have.property('where').which.deep.equals({ bar: 42 });
      });
    });

    describe('duplicating', () => {
      it('should tag a hasMany association as duplicating: true if undefined', function () {
        const options = _validateIncludedElements({
          model: this.User,
          include: [this.User.Tasks],
        });

        expect(options.include[0].duplicating).to.equal(true);
      });

      it('should respect include.duplicating for a hasMany', function () {
        const options = _validateIncludedElements({
          model: this.User,
          include: [{ association: this.User.Tasks, duplicating: false }],
        });

        expect(options.include[0].duplicating).to.equal(false);
      });
    });

    describe('_conformInclude', () => {
      it('should expand association from string alias', function () {
        const options = {
          include: ['Owner'],
        };
        Sequelize.Model._conformIncludes(options, this.Company);

        expect(options.include[0]).to.deep.equal({
          model: this.User,
          association: this.Company.Owner,
          as: 'Owner',
        });
      });

      it('should expand string association', function () {
        const options = {
          include: [
            {
              association: 'Owner',
              attributes: ['id'],
            },
          ],
        };
        Sequelize.Model._conformIncludes(options, this.Company);

        expect(options.include[0]).to.deep.equal({
          model: this.User,
          association: this.Company.Owner,
          attributes: ['id'],
          as: 'Owner',
        });
      });

      it('should throw an error if invalid model is passed', function () {
        const options = {
          include: [
            {
              model: null,
            },
          ],
        };

        expect(() => {
          Sequelize.Model._conformIncludes(options, this.Company);
        }).to.throw(
          'Invalid Include received. Include has to be either a Model, an Association, the name of an association, or a plain object compatible with IncludeOptions.',
        );
      });

      it('should throw an error if invalid association is passed', function () {
        const options = {
          include: [
            {
              association: null,
            },
          ],
        };

        expect(() => {
          Sequelize.Model._conformIncludes(options, this.Company);
        }).to.throw(
          'Invalid Include received. Include has to be either a Model, an Association, the name of an association, or a plain object compatible with IncludeOptions.',
        );
      });
    });

    describe('getAssociationWithModel', () => {
      it('returns an association when there is a single unaliased association', function () {
        expect(this.User.getAssociationWithModel(this.Task)).to.equal(this.User.Tasks);
      });

      it('returns an association when there is a single aliased association', function () {
        const User = this.sequelize.define('User');
        const Task = this.sequelize.define('Task');
        const Tasks = Task.belongsTo(User, { as: 'owner' });
        expect(Task.getAssociationWithModel(User, 'owner')).to.equal(Tasks);
      });

      it('returns an association when there are multiple aliased associations', function () {
        expect(this.Company.getAssociationWithModel(this.User, 'Owner')).to.equal(
          this.Company.Owner,
        );
      });
    });

    describe('subQuery', () => {
      it('should be true if theres a duplicating association', function () {
        const options = _validateIncludedElements({
          model: this.User,
          include: [{ association: this.User.Tasks }],
          limit: 3,
        });

        expect(options.subQuery).to.equal(true);
      });

      it('should be false if theres a duplicating association but no limit', function () {
        const options = _validateIncludedElements({
          model: this.User,
          include: [{ association: this.User.Tasks }],
          limit: null,
        });

        expect(options.subQuery).to.equal(false);
      });

      it('should be true if theres a nested duplicating association', function () {
        const options = _validateIncludedElements({
          model: this.User,
          include: [
            {
              association: this.User.Company,
              include: [this.Company.Employees],
            },
          ],
          limit: 3,
        });

        expect(options.subQuery).to.equal(true);
      });

      it('should be false if theres a nested duplicating association but no limit', function () {
        const options = _validateIncludedElements({
          model: this.User,
          include: [
            {
              association: this.User.Company,
              include: [this.Company.Employees],
            },
          ],
          limit: null,
        });

        expect(options.subQuery).to.equal(false);
      });

      it('should tag a required hasMany association', function () {
        const options = _validateIncludedElements({
          model: this.User,
          include: [{ association: this.User.Tasks, required: true }],
          limit: 3,
        });

        expect(options.subQuery).to.equal(true);
        expect(options.include[0].subQuery).to.equal(false);
        expect(options.include[0].subQueryFilter).to.equal(true);
      });

      it('should not tag a required hasMany association with duplicating false', function () {
        const options = _validateIncludedElements({
          model: this.User,
          include: [{ association: this.User.Tasks, required: true, duplicating: false }],
          limit: 3,
        });

        expect(options.subQuery).to.equal(false);
        expect(options.include[0].subQuery).to.equal(false);
        expect(options.include[0].subQueryFilter).to.equal(false);
      });

      it('should not tag a separate hasMany association with subQuery true', function () {
        const options = _validateIncludedElements({
          model: this.Company,
          include: [
            {
              association: this.Company.Employees,
              separate: true,
              include: [{ association: this.User.Tasks, required: true }],
            },
          ],
          required: true,
        });

        expect(options.subQuery).to.equal(false);
        expect(options.include[0].subQuery).to.equal(false);
        expect(options.include[0].subQueryFilter).to.equal(false);
      });

      it('should tag a hasMany association with where', function () {
        const options = _validateIncludedElements({
          model: this.User,
          include: [{ association: this.User.Tasks, where: { title: Math.random().toString() } }],
          limit: 3,
        });

        expect(options.subQuery).to.equal(true);
        expect(options.include[0].subQuery).to.equal(false);
        expect(options.include[0].subQueryFilter).to.equal(true);
      });

      it('should not tag a hasMany association with where and duplicating false', function () {
        const options = _validateIncludedElements({
          model: this.User,
          include: [
            {
              association: this.User.Tasks,
              where: { title: Math.random().toString() },
              duplicating: false,
            },
          ],
          limit: 3,
        });

        expect(options.subQuery).to.equal(false);
        expect(options.include[0].subQuery).to.equal(false);
        expect(options.include[0].subQueryFilter).to.equal(false);
      });

      it('should tag a required belongsTo alongside a duplicating association', function () {
        const options = _validateIncludedElements({
          model: this.User,
          include: [
            { association: this.User.Company, required: true },
            { association: this.User.Tasks },
          ],
          limit: 3,
        });

        expect(options.subQuery).to.equal(true);
        expect(options.include[0].subQuery).to.equal(true);
      });

      it('should not tag a required belongsTo alongside a duplicating association with duplicating false', function () {
        const options = _validateIncludedElements({
          model: this.User,
          include: [
            { association: this.User.Company, required: true },
            { association: this.User.Tasks, duplicating: false },
          ],
          limit: 3,
        });

        expect(options.subQuery).to.equal(false);
        expect(options.include[0].subQuery).to.equal(false);
      });

      it('should tag a belongsTo association with where alongside a duplicating association', function () {
        const options = _validateIncludedElements({
          model: this.User,
          include: [
            { association: this.User.Company, where: { name: Math.random().toString() } },
            { association: this.User.Tasks },
          ],
          limit: 3,
        });

        expect(options.subQuery).to.equal(true);
        expect(options.include[0].subQuery).to.equal(true);
      });

      it('should tag a required belongsTo association alongside a duplicating association with a nested belongsTo', function () {
        const options = _validateIncludedElements({
          model: this.User,
          include: [
            {
              association: this.User.Company,
              required: true,
              include: [this.Company.Owner],
            },
            this.User.Tasks,
          ],
          limit: 3,
        });

        expect(options.subQuery).to.equal(true);
        expect(options.include[0].subQuery).to.equal(true);
        expect(options.include[0].include[0].subQuery).to.equal(false);
        expect(options.include[0].include[0].parent.subQuery).to.equal(true);
      });

      it('should tag a belongsTo association with where alongside a duplicating association with duplicating false', function () {
        const options = _validateIncludedElements({
          model: this.User,
          include: [
            { association: this.User.Company, where: { name: Math.random().toString() } },
            { association: this.User.Tasks, duplicating: false },
          ],
          limit: 3,
        });

        expect(options.subQuery).to.equal(false);
        expect(options.include[0].subQuery).to.equal(false);
      });
    });
  });
});
