const SandboxedModule = require('sandboxed-module')
const sinon = require('sinon')
const { expect } = require('chai')
const modulePath =
  '../../../../app/src/Features/Subscription/TeamInvitesHandler'

const { ObjectId } = require('mongodb-legacy')
const Errors = require('../../../../app/src/Features/Errors/Errors')

describe('TeamInvitesHandler', function () {
  beforeEach(function () {
    this.manager = {
      _id: '666666',
      first_name: 'Daenerys',
      last_name: 'Targaryen',
      email: 'daenerys@example.com',
      emails: [{ email: 'daenerys@example.com' }],
    }

    this.token = 'aaaaaaaaaaaaaaaaaaaaaa'

    this.teamInvite = {
      email: 'jorah@example.com',
      token: this.token,
    }
    // ensure teamInvite can be converted from Document to Object
    this.teamInvite.toObject = () => this.teamInvite

    this.subscription = {
      id: '55153a8014829a865bbf700d',
      _id: new ObjectId('55153a8014829a865bbf700d'),
      recurlySubscription_id: '1a2b3c4d5e6f7g',
      admin_id: this.manager._id,
      groupPlan: true,
      member_ids: [],
      teamInvites: [this.teamInvite],
      save: sinon.stub().resolves(),
    }

    this.SubscriptionLocator = {
      promises: {
        getUsersSubscription: sinon.stub(),
        getSubscription: sinon.stub().resolves(this.subscription),
      },
    }

    this.UserGetter = {
      promises: {
        getUser: sinon.stub().resolves(),
        getUserByAnyEmail: sinon.stub().resolves(),
        getUserByMainEmail: sinon.stub().resolves(),
      },
    }

    this.SubscriptionUpdater = {
      promises: {
        addUserToGroup: sinon.stub().resolves(),
        deleteSubscription: sinon.stub().resolves(),
      },
    }

    this.LimitationsManager = {
      teamHasReachedMemberLimit: sinon.stub().returns(false),
    }

    this.Subscription = {
      findOne: sinon.stub().resolves(),
      updateOne: sinon.stub().resolves(),
    }

    this.SSOConfig = {
      findById: sinon.stub().resolves(),
    }

    this.EmailHandler = {
      promises: {
        sendEmail: sinon.stub().resolves(null),
      },
    }

    this.newToken = 'bbbbbbbbb'

    this.crypto = {
      randomBytes: () => {
        return { toString: sinon.stub().returns(this.newToken) }
      },
    }

    this.UserGetter.promises.getUser
      .withArgs(this.manager._id)
      .resolves(this.manager)
    this.UserGetter.promises.getUserByAnyEmail
      .withArgs(this.manager.email)
      .resolves(this.manager)
    this.UserGetter.promises.getUserByMainEmail
      .withArgs(this.manager.email)
      .resolves(this.manager)

    this.SubscriptionLocator.promises.getUsersSubscription.resolves(
      this.subscription
    )

    this.NotificationsBuilder = {
      promises: {
        groupInvitation: sinon.stub().returns({
          create: sinon.stub().resolves(),
          read: sinon.stub().resolves(),
        }),
      },
    }

    this.Subscription.findOne.resolves(this.subscription)

    this.RecurlyClient = {
      promises: {
        terminateSubscriptionByUuid: sinon.stub().resolves(),
      },
    }

    this.TeamInvitesHandler = SandboxedModule.require(modulePath, {
      requires: {
        'mongodb-legacy': { ObjectId },
        crypto: this.crypto,
        '@overleaf/settings': { siteUrl: 'http://example.com' },
        '../../models/TeamInvite': { TeamInvite: (this.TeamInvite = {}) },
        '../../models/Subscription': { Subscription: this.Subscription },
        '../../models/SSOConfig': { SSOConfig: this.SSOConfig },
        '../User/UserGetter': this.UserGetter,
        './SubscriptionLocator': this.SubscriptionLocator,
        './SubscriptionUpdater': this.SubscriptionUpdater,
        './LimitationsManager': this.LimitationsManager,
        '../Email/EmailHandler': this.EmailHandler,
        '../Notifications/NotificationsBuilder': this.NotificationsBuilder,
        '../../infrastructure/Modules': (this.Modules = {
          promises: { hooks: { fire: sinon.stub().resolves() } },
        }),
        './RecurlyClient': this.RecurlyClient,
      },
    })
  })

  describe('getInvite', function () {
    it("returns the invite if there's one", async function () {
      const { invite, subscription } =
        await this.TeamInvitesHandler.promises.getInvite(this.token)

      expect(invite).to.deep.eq(this.teamInvite)
      expect(subscription).to.deep.eq(this.subscription)
    })

    it("returns teamNotFound if there's none", async function () {
      this.Subscription.findOne = sinon.stub().resolves(null)

      let error
      try {
        await this.TeamInvitesHandler.promises.getInvite(this.token)
      } catch (err) {
        error = err
      }

      expect(error).to.be.instanceOf(Errors.NotFoundError)
    })
  })

  describe('createInvite', function () {
    it('adds the team invite to the subscription', async function () {
      const invite = await this.TeamInvitesHandler.promises.createInvite(
        this.manager._id,
        this.subscription,
        'John.Snow@example.com'
      )
      expect(invite.token).to.eq(this.newToken)
      expect(invite.email).to.eq('john.snow@example.com')
      expect(invite.inviterName).to.eq(
        'Daenerys Targaryen (daenerys@example.com)'
      )
      expect(invite.invite).to.be.true
      expect(this.subscription.teamInvites).to.deep.include(invite)
    })

    it('sends an email', async function () {
      await this.TeamInvitesHandler.promises.createInvite(
        this.manager._id,
        this.subscription,
        'John.Snow@example.com'
      )

      this.EmailHandler.promises.sendEmail
        .calledWith(
          'verifyEmailToJoinTeam',
          sinon.match({
            to: 'john.snow@example.com',
            inviter: this.manager,
            acceptInviteUrl: `http://example.com/subscription/invites/${this.newToken}/`,
          })
        )
        .should.equal(true)
    })

    it('refreshes the existing invite if the email has already been invited', async function () {
      const originalInvite = Object.assign({}, this.teamInvite)

      const invite = await this.TeamInvitesHandler.promises.createInvite(
        this.manager._id,
        this.subscription,
        originalInvite.email
      )
      expect(invite).to.exist

      expect(this.subscription.teamInvites.length).to.eq(1)
      expect(this.subscription.teamInvites).to.deep.include(invite)

      expect(invite.email).to.eq(originalInvite.email)

      this.subscription.save.calledOnce.should.eq(true)
    })

    it('removes any legacy invite from the subscription', async function () {
      await this.TeamInvitesHandler.promises.createInvite(
        this.manager._id,
        this.subscription,
        'John.Snow@example.com'
      )

      this.Subscription.updateOne
        .calledWith(
          { _id: new ObjectId('55153a8014829a865bbf700d') },
          { $pull: { invited_emails: 'john.snow@example.com' } }
        )
        .should.eq(true)
    })

    it('add user to subscription if inviting self', async function () {
      const invite = await this.TeamInvitesHandler.promises.createInvite(
        this.manager._id,
        this.subscription,
        this.manager.email
      )
      sinon.assert.calledWith(
        this.SubscriptionUpdater.promises.addUserToGroup,
        this.subscription._id,
        this.manager._id
      )
      sinon.assert.notCalled(this.subscription.save)
      expect(invite.token).to.not.exist
      expect(invite.email).to.eq(this.manager.email)
      expect(invite.first_name).to.eq(this.manager.first_name)
      expect(invite.last_name).to.eq(this.manager.last_name)
      expect(invite.invite).to.be.false
    })

    it('sends an SSO invite if SSO is enabled and inviting self', async function () {
      this.subscription.ssoConfig = new ObjectId('abc123abc123abc123abc123')
      this.SSOConfig.findById
        .withArgs(this.subscription.ssoConfig)
        .resolves({ enabled: true })

      await this.TeamInvitesHandler.promises.createInvite(
        this.manager._id,
        this.subscription,
        this.manager.email
      )
      sinon.assert.calledWith(
        this.Modules.promises.hooks.fire,
        'sendGroupSSOReminder',
        this.manager._id,
        this.subscription._id
      )
    })

    it('does not send an SSO invite if SSO is disabled and inviting self', async function () {
      this.subscription.ssoConfig = new ObjectId('abc123abc123abc123abc123')
      this.SSOConfig.findById
        .withArgs(this.subscription.ssoConfig)
        .resolves({ enabled: false })

      await this.TeamInvitesHandler.promises.createInvite(
        this.manager._id,
        this.subscription,
        this.manager.email
      )
      sinon.assert.notCalled(this.Modules.promises.hooks.fire)
    })

    it('sends a notification if inviting registered user', async function () {
      const id = new ObjectId('6a6b3a8014829a865bbf700d')
      const managedUsersEnabled = false

      this.UserGetter.promises.getUserByMainEmail
        .withArgs('john.snow@example.com')
        .resolves({
          _id: id,
        })

      const invite = await this.TeamInvitesHandler.promises.createInvite(
        this.manager._id,
        this.subscription,
        'John.Snow@example.com'
      )
      this.NotificationsBuilder.promises
        .groupInvitation(
          id.toString(),
          this.subscription._id,
          managedUsersEnabled
        )
        .create.calledWith(invite)
        .should.eq(true)
    })
  })

  describe('importInvite', function () {
    beforeEach(function () {
      this.sentAt = new Date()
    })

    it('can imports an invite from v1', function () {
      this.TeamInvitesHandler.importInvite(
        this.subscription,
        'A-Team',
        'hannibal@a-team.org',
        'secret',
        this.sentAt,
        error => {
          expect(error).not.to.exist

          this.subscription.save.calledOnce.should.eq(true)

          const invite = this.subscription.teamInvites.find(
            i => i.email === 'hannibal@a-team.org'
          )
          expect(invite.token).to.eq('secret')
          expect(invite.sentAt).to.eq(this.sentAt)
        }
      )
    })
  })

  describe('acceptInvite', function () {
    beforeEach(function () {
      this.user = {
        id: '123456789',
        first_name: 'Tyrion',
        last_name: 'Lannister',
        email: 'tyrion@example.com',
      }

      this.user_subscription = {
        id: '66264b9125930b976cc0811e',
        _id: new ObjectId('66264b9125930b976cc0811e'),
        groupPlan: false,
        recurlySubscription_id: 'fa1b2cfa156gh',
        admin_id: '123456789',
        member_ids: [],
        teamInvites: [],
        save: sinon.stub().resolves(),
      }

      this.ipAddress = '127.0.0.1'

      this.UserGetter.promises.getUserByAnyEmail
        .withArgs(this.user.email)
        .resolves(this.user)

      this.SubscriptionLocator.promises.getUsersSubscription
        .withArgs(this.user.id)
        .resolves(this.user_subscription)

      this.subscription.teamInvites.push({
        email: 'john.snow@example.com',
        token: 'dddddddd',
        inviterName: 'Daenerys Targaryen (daenerys@example.com)',
      })
    })

    describe('with standard group', function () {
      it('adds the user to the team', async function () {
        await this.TeamInvitesHandler.promises.acceptInvite(
          'dddddddd',
          this.user.id,
          this.ipAddress
        )
        this.SubscriptionUpdater.promises.addUserToGroup
          .calledWith(this.subscription._id, this.user.id)
          .should.eq(true)
      })

      it('removes the invite from the subscription', async function () {
        await this.TeamInvitesHandler.promises.acceptInvite(
          'dddddddd',
          this.user.id,
          this.ipAddress
        )
        this.Subscription.updateOne
          .calledWith(
            { _id: new ObjectId('55153a8014829a865bbf700d') },
            { $pull: { teamInvites: { email: 'john.snow@example.com' } } }
          )
          .should.eq(true)
      })

      it('removes dashboard notification after they accepted group invitation', async function () {
        const managedUsersEnabled = false

        await this.TeamInvitesHandler.promises.acceptInvite(
          'dddddddd',
          this.user.id,
          this.ipAddress
        )
        sinon.assert.called(
          this.NotificationsBuilder.promises.groupInvitation(
            this.user.id,
            this.subscription._id,
            managedUsersEnabled
          ).read
        )
      })

      it('should not schedule an SSO invite reminder', async function () {
        await this.TeamInvitesHandler.promises.acceptInvite(
          'dddddddd',
          this.user.id,
          this.ipAddress
        )
        sinon.assert.notCalled(this.Modules.promises.hooks.fire)
      })
    })

    describe('with managed group', function () {
      it('should enroll the group member', async function () {
        this.subscription.managedUsersEnabled = true

        await this.TeamInvitesHandler.promises.acceptInvite(
          'dddddddd',
          this.user.id,
          this.ipAddress
        )
        sinon.assert.calledWith(
          this.SubscriptionUpdater.promises.deleteSubscription,
          this.user_subscription,
          { id: this.user.id, ip: this.ipAddress }
        )
        sinon.assert.calledWith(
          this.RecurlyClient.promises.terminateSubscriptionByUuid,
          this.user_subscription.recurlySubscription_id
        )
        sinon.assert.calledWith(
          this.Modules.promises.hooks.fire,
          'enrollInManagedSubscription',
          this.user.id,
          this.subscription
        )
      })

      it('should not delete the users subscription if that subscription is also the join target', async function () {
        this.subscription.managedUsersEnabled = true
        this.SubscriptionLocator.promises.getUsersSubscription
          .withArgs(this.user.id)
          .resolves(this.subscription)

        await this.TeamInvitesHandler.promises.acceptInvite(
          'dddddddd',
          this.user.id,
          this.ipAddress
        )

        sinon.assert.notCalled(
          this.SubscriptionUpdater.promises.deleteSubscription
        )
      })
    })

    describe('with group SSO enabled', function () {
      it('should schedule an SSO invite reminder', async function () {
        this.subscription.ssoConfig = 'ssoconfig1'
        this.SSOConfig.findById
          .withArgs('ssoconfig1')
          .resolves({ enabled: true })

        await this.TeamInvitesHandler.promises.acceptInvite(
          'dddddddd',
          this.user.id,
          this.ipAddress
        )
        sinon.assert.calledWith(
          this.Modules.promises.hooks.fire,
          'scheduleGroupSSOReminder',
          this.user.id,
          this.subscription._id
        )
      })
    })
  })

  describe('revokeInvite', function () {
    it('removes the team invite from the subscription', async function () {
      await this.TeamInvitesHandler.promises.revokeInvite(
        this.manager._id,
        this.subscription,
        'jorah@example.com'
      )
      this.Subscription.updateOne
        .calledWith(
          { _id: new ObjectId('55153a8014829a865bbf700d') },
          { $pull: { teamInvites: { email: 'jorah@example.com' } } }
        )
        .should.eq(true)

      this.Subscription.updateOne
        .calledWith(
          { _id: new ObjectId('55153a8014829a865bbf700d') },
          { $pull: { invited_emails: 'jorah@example.com' } }
        )
        .should.eq(true)
    })

    it('removes dashboard notification for pending group invitation', async function () {
      const managedUsersEnabled = false

      const pendingUser = {
        id: '1a2b',
        email: 'tyrion@example.com',
      }

      this.UserGetter.promises.getUserByAnyEmail
        .withArgs(pendingUser.email)
        .resolves(pendingUser)

      await this.TeamInvitesHandler.promises.revokeInvite(
        this.manager._id,
        this.subscription,
        pendingUser.email
      )

      sinon.assert.called(
        this.NotificationsBuilder.promises.groupInvitation(
          pendingUser.id,
          this.subscription._id,
          managedUsersEnabled
        ).read
      )
    })
  })

  describe('createTeamInvitesForLegacyInvitedEmail', function () {
    beforeEach(function () {
      this.subscription.invited_emails = [
        'eddard@example.com',
        'robert@example.com',
      ]
      this.TeamInvitesHandler.createInvite = sinon.stub().resolves(null)
      this.SubscriptionLocator.promises.getGroupsWithEmailInvite = sinon
        .stub()
        .resolves([this.subscription])
    })

    it('sends an invitation email to addresses in the legacy invited_emails field', async function () {
      const invites =
        await this.TeamInvitesHandler.promises.createTeamInvitesForLegacyInvitedEmail(
          'eddard@example.com'
        )

      expect(invites.length).to.eq(1)

      const [invite] = invites
      expect(invite.token).to.eq(this.newToken)
      expect(invite.email).to.eq('eddard@example.com')
      expect(invite.inviterName).to.eq(
        'Daenerys Targaryen (daenerys@example.com)'
      )
      expect(invite.invite).to.be.true
      expect(this.subscription.teamInvites).to.deep.include(invite)
    })
  })

  describe('validation', function () {
    it("doesn't create an invite if the team limit has been reached", async function () {
      this.LimitationsManager.teamHasReachedMemberLimit = sinon
        .stub()
        .returns(true)
      let error

      try {
        await this.TeamInvitesHandler.promises.createInvite(
          this.manager._id,
          this.subscription,
          'John.Snow@example.com'
        )
      } catch (err) {
        error = err
      }

      expect(error).to.exist

      expect(error).to.deep.equal({
        limitReached: true,
      })
    })

    it("doesn't create an invite if the subscription is not in a group plan", async function () {
      this.subscription.groupPlan = false
      let error

      try {
        await this.TeamInvitesHandler.promises.createInvite(
          this.manager._id,
          this.subscription,
          'John.Snow@example.com'
        )
      } catch (err) {
        error = err
      }

      expect(error).to.exist

      expect(error).to.deep.equal({
        wrongPlan: true,
      })
    })

    it("doesn't create an invite if the user is already part of the team", async function () {
      const member = {
        id: '1a2b',
        _id: '1a2b',
        email: 'tyrion@example.com',
      }

      this.subscription.member_ids = [member.id]
      this.UserGetter.promises.getUserByAnyEmail
        .withArgs(member.email)
        .resolves(member)

      let error

      try {
        await this.TeamInvitesHandler.promises.createInvite(
          this.manager._id,
          this.subscription,
          'tyrion@example.com'
        )
      } catch (err) {
        error = err
      }

      expect(error).to.exist

      expect(error).to.eql({
        alreadyInTeam: true,
      })
    })
  })
})
