'use strict';

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

describe('Model scope with associations', () => {
  beforeEach(async function () {
    const sequelize = this.sequelize;

    this.ScopeMe = this.sequelize.define(
      'ScopeMe',
      {
        username: DataTypes.STRING,
        email: DataTypes.STRING,
        access_level: DataTypes.INTEGER,
        other_value: DataTypes.INTEGER,
        parent_id: DataTypes.INTEGER,
      },
      {
        defaultScope: {
          where: {
            access_level: {
              [Op.gte]: 5,
            },
          },
        },
        scopes: {
          isTony: {
            where: {
              username: 'tony',
            },
          },
          includeActiveProjects() {
            return {
              include: [
                {
                  model: sequelize.models.getOrThrow('company'),
                  include: [sequelize.models.getOrThrow('project').withScope('active')],
                },
              ],
            };
          },
        },
      },
    );

    this.Project = this.sequelize.define(
      'project',
      {
        active: DataTypes.BOOLEAN,
      },
      {
        scopes: {
          active: {
            where: {
              active: true,
            },
          },
        },
      },
    );

    this.Company = this.sequelize.define(
      'company',
      {
        active: DataTypes.BOOLEAN,
      },
      {
        defaultScope: {
          where: { active: true },
        },
        scopes: {
          notActive: {
            where: {
              active: false,
            },
          },
          reversed: {
            order: [['id', 'DESC']],
          },
        },
      },
    );

    this.Profile = this.sequelize.define(
      'profile',
      {
        active: DataTypes.BOOLEAN,
      },
      {
        defaultScope: {
          where: { active: true },
        },
        scopes: {
          notActive: {
            where: {
              active: false,
            },
          },
        },
      },
    );

    this.Project.belongsToMany(this.Company, { through: 'CompanyProjects' });
    this.Company.belongsToMany(this.Project, { through: 'CompanyProjects' });

    this.ScopeMe.hasOne(this.Profile, { foreignKey: 'userId' });

    this.ScopeMe.belongsTo(this.Company);
    this.UserAssociation = this.Company.hasMany(this.ScopeMe, { as: 'users' });

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

    const [u1, u2, u3, u4, u5, c1, c2] = await Promise.all([
      this.ScopeMe.create({
        id: 1,
        username: 'dan',
        email: 'dan@sequelizejs.com',
        access_level: 5,
        other_value: 10,
        parent_id: 1,
      }),
      this.ScopeMe.create({
        id: 2,
        username: 'tobi',
        email: 'tobi@fakeemail.com',
        access_level: 10,
        other_value: 11,
        parent_id: 2,
      }),
      this.ScopeMe.create({
        id: 3,
        username: 'tony',
        email: 'tony@sequelizejs.com',
        access_level: 3,
        other_value: 7,
        parent_id: 1,
      }),
      this.ScopeMe.create({
        id: 4,
        username: 'fred',
        email: 'fred@foobar.com',
        access_level: 3,
        other_value: 7,
        parent_id: 1,
      }),
      this.ScopeMe.create({
        id: 5,
        username: 'bob',
        email: 'bob@foobar.com',
        access_level: 1,
        other_value: 9,
        parent_id: 5,
      }),
      this.Company.create({ id: 1, active: true }),
      this.Company.create({ id: 2, active: false }),
    ]);

    await Promise.all([c1.setUsers([u1, u2, u3, u4]), c2.setUsers([u5])]);
  });

  describe('include', () => {
    it('should scope columns properly', async function () {
      // Will error with ambigous column if id is not scoped properly to `Company`.`id`
      await expect(
        this.Company.findAll({
          where: { id: 1 },
          include: [this.UserAssociation],
        }),
      ).not.to.be.rejected;
    });

    it('should apply default scope when including an associations', async function () {
      const obj = await this.Company.findAll({
        include: [this.UserAssociation],
      });

      const company = await obj[0];
      expect(company.users).to.have.length(2);
    });

    it('should apply default scope when including a model', async function () {
      const obj = await this.Company.findAll({
        include: [{ model: this.ScopeMe, as: 'users' }],
      });

      const company = await obj[0];
      expect(company.users).to.have.length(2);
    });

    it('should be able to include a scoped model', async function () {
      const obj = await this.Company.findAll({
        include: [{ model: this.ScopeMe.withScope('isTony'), as: 'users' }],
      });

      const company = await obj[0];
      expect(company.users).to.have.length(1);
      expect(company.users[0].get('username')).to.equal('tony');
    });
  });

  describe('get', () => {
    beforeEach(async function () {
      const [p, companies] = await Promise.all([
        this.Project.create(),
        this.Company.withoutScope().findAll(),
      ]);

      await p.setCompanies(companies);
    });

    describe('it should be able to unscope', () => {
      it('hasMany', async function () {
        const company = await this.Company.findByPk(1);
        const users = await company.getUsers({ scope: false });
        expect(users).to.have.length(4);
      });

      it('hasOne', async function () {
        await this.Profile.create({
          active: false,
          userId: 1,
        });

        const user = await this.ScopeMe.findByPk(1);
        const profile = await user.getProfile({ scope: false });
        expect(profile).to.be.ok;
      });

      it('belongsTo', async function () {
        const user = await this.ScopeMe.withoutScope().findOne({ where: { username: 'bob' } });
        const company = await user.getCompany({ scope: false });
        expect(company).to.be.ok;
      });

      it('belongsToMany', async function () {
        const obj = await this.Project.findAll();
        const p = await obj[0];
        const companies = await p.getCompanies({ scope: false });
        expect(companies).to.have.length(2);
      });
    });

    describe('it should apply default scope', () => {
      it('hasMany', async function () {
        const company = await this.Company.findByPk(1);
        const users = await company.getUsers();
        expect(users).to.have.length(2);
      });

      it('hasOne', async function () {
        await this.Profile.create({
          active: false,
          userId: 1,
        });

        const user = await this.ScopeMe.findByPk(1);
        const profile = await user.getProfile();
        expect(profile).not.to.be.ok;
      });

      it('belongsTo', async function () {
        const user = await this.ScopeMe.withoutScope().findOne({ where: { username: 'bob' } });
        const company = await user.getCompany();
        expect(company).not.to.be.ok;
      });

      it('belongsToMany', async function () {
        const obj = await this.Project.findAll();
        const p = await obj[0];
        const companies = await p.getCompanies();
        expect(companies).to.have.length(1);
        expect(companies[0].get('active')).to.be.ok;
      });
    });

    describe('it should be able to apply another scope', () => {
      it('hasMany', async function () {
        const company = await this.Company.findByPk(1);
        const users = await company.getUsers({ scope: 'isTony' });
        expect(users).to.have.length(1);
        expect(users[0].get('username')).to.equal('tony');
      });

      it('hasOne', async function () {
        await this.Profile.create({
          active: true,
          userId: 1,
        });

        const user = await this.ScopeMe.findByPk(1);
        const profile = await user.getProfile({ scope: 'notActive' });
        expect(profile).not.to.be.ok;
      });

      it('belongsTo', async function () {
        const user = await this.ScopeMe.withoutScope().findOne({ where: { username: 'bob' } });
        const company = await user.getCompany({ scope: 'notActive' });
        expect(company).to.be.ok;
      });

      it('belongsToMany', async function () {
        const obj = await this.Project.findAll();
        const p = await obj[0];
        const companies = await p.getCompanies({ scope: 'reversed' });
        expect(companies).to.have.length(2);
        expect(companies[0].id).to.equal(2);
        expect(companies[1].id).to.equal(1);
      });
    });
  });

  describe('scope with includes', () => {
    beforeEach(async function () {
      const [c, p1, p2] = await Promise.all([
        this.Company.findByPk(1),
        this.Project.create({ id: 1, active: true }),
        this.Project.create({ id: 2, active: false }),
      ]);

      await c.setProjects([p1, p2]);
    });

    it('should scope columns properly', async function () {
      await expect(this.ScopeMe.withScope('includeActiveProjects').findAll()).not.to.be.rejected;
    });

    it('should apply scope conditions', async function () {
      const user = await this.ScopeMe.withScope('includeActiveProjects').findOne({
        where: { id: 1 },
      });
      expect(user.company.projects).to.have.length(1);
    });

    describe('with different format', () => {
      it('should not throw error', async function () {
        const Child = this.sequelize.define('Child');
        const Parent = this.sequelize.define(
          'Parent',
          {},
          {
            defaultScope: {
              include: [{ model: Child }],
            },
            scopes: {
              children: {
                include: [Child],
              },
            },
          },
        );
        Parent.addScope('alsoChildren', {
          include: [{ model: Child }],
        });

        Child.belongsTo(Parent);
        Parent.hasOne(Child);

        await this.sequelize.sync({ force: true });
        const [child, parent] = await Promise.all([Child.create(), Parent.create()]);
        await parent.setChild(child);

        await Parent.withScope('children', 'alsoChildren').findOne();
      });
    });

    describe('with find options', () => {
      it('should merge includes correctly', async function () {
        const Child = this.sequelize.define('Child', { name: DataTypes.STRING });
        const Parent = this.sequelize.define('Parent', { name: DataTypes.STRING });
        Parent.addScope('testScope1', {
          include: [
            {
              model: Child,
              where: {
                name: 'child2',
              },
            },
          ],
        });
        Parent.hasMany(Child);

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

        await Promise.all([
          Parent.create({ name: 'parent1' }).then(parent => parent.createChild({ name: 'child1' })),
          Parent.create({ name: 'parent2' }).then(parent => parent.createChild({ name: 'child2' })),
        ]);

        const parent = await Parent.withScope('testScope1').findOne({
          include: [
            {
              model: Child,
              attributes: { exclude: ['name'] },
            },
          ],
        });

        expect(parent.get('name')).to.equal('parent2');
        expect(parent.children).to.have.length(1);
        expect(parent.children[0].dataValues).not.to.have.property('name');
      });
    });
  });

  describe('scope with options', () => {
    it('should return correct object included foreign_key', async function () {
      const Child = this.sequelize.define(
        'Child',
        {
          secret: DataTypes.STRING,
        },
        {
          scopes: {
            public: {
              attributes: {
                exclude: ['secret'],
              },
            },
          },
        },
      );
      const Parent = this.sequelize.define('Parent');
      Child.belongsTo(Parent);
      Parent.hasOne(Child);

      await this.sequelize.sync({ force: true });
      await Child.create({ secret: 'super secret' });
      const user = await Child.withScope('public').findOne();
      expect(user.dataValues).to.have.property('parentId');
      expect(user.dataValues).not.to.have.property('secret');
    });

    it('should return correct object included foreign_key with defaultScope', async function () {
      const Child = this.sequelize.define(
        'Child',
        {
          secret: DataTypes.STRING,
        },
        {
          defaultScope: {
            attributes: {
              exclude: ['secret'],
            },
          },
        },
      );
      const Parent = this.sequelize.define('Parent');
      Child.belongsTo(Parent);

      await this.sequelize.sync({ force: true });
      await Child.create({ secret: 'super secret' });
      const user = await Child.findOne();
      expect(user.dataValues).to.have.property('parentId');
      expect(user.dataValues).not.to.have.property('secret');
    });

    it('should not throw error', async function () {
      const Clientfile = this.sequelize.define('clientfile');
      const Mission = this.sequelize.define('mission', { secret: DataTypes.STRING });
      const Building = this.sequelize.define('building');
      const MissionAssociation = Clientfile.hasOne(Mission);
      const BuildingAssociation = Clientfile.hasOne(Building);

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

      await Clientfile.findAll({
        include: [
          {
            association: 'mission',
            where: {
              secret: 'foo',
            },
          },
          {
            association: 'building',
          },
        ],
      });

      await Clientfile.findAll({
        include: [
          {
            association: MissionAssociation,
            where: {
              secret: 'foo',
            },
          },
          {
            association: BuildingAssociation,
          },
        ],
      });
    });
  });
});
