import { vi, expect } from 'vitest'
import mongodb from 'mongodb-legacy'
import sinon from 'sinon'
import MockRequest from '../helpers/MockRequest.mjs'
import { InvalidEmailError } from '../../../../app/src/Features/Errors/Errors.js'

const { ObjectId } = mongodb

const modulePath =
  '../../../../app/src/Features/Subscription/SubscriptionGroupHandler.mjs'

describe('SubscriptionGroupHandler', function () {
  beforeEach(async function (ctx) {
    ctx.adminUser_id = '12321'
    ctx.newEmail = 'bob@smith.com'
    ctx.user_id = '3121321'
    ctx.email = 'jim@example.com'
    ctx.user = { _id: ctx.user_id, email: ctx.newEmail }
    ctx.subscription_id = '31DSd1123D'
    ctx.adding = 1
    ctx.paymentMethod = { cardType: 'Visa', lastFour: '1111' }
    ctx.PaymentProviderEntities = {
      MEMBERS_LIMIT_ADD_ON_CODE: 'additional-license',
    }
    ctx.localPlanInSettings = {
      membersLimit: 5,
      membersLimitAddOn: ctx.PaymentProviderEntities.MEMBERS_LIMIT_ADD_ON_CODE,
    }

    ctx.subscription = {
      admin_id: ctx.adminUser_id,
      manager_ids: [ctx.adminUser_id],
      _id: ctx.subscription_id,
      membersLimit: 100,
    }

    ctx.changeRequest = {
      timeframe: 'now',
      subscription: {
        id: 'test_id',
      },
    }

    ctx.termsAndConditionsUpdate = {
      termsAndConditions: 'T&C copy',
    }

    ctx.poNumberAndTermsAndConditionsUpdate = {
      poNumber: '4444',
      ...ctx.termsAndConditionsUpdate,
    }

    ctx.recurlySubscription = {
      id: 123,
      addOns: [
        {
          code: ctx.PaymentProviderEntities.MEMBERS_LIMIT_ADD_ON_CODE,
          quantity: 1,
        },
      ],
      getRequestForAddOnUpdate: sinon.stub().returns(ctx.changeRequest),
      getRequestForGroupPlanUpgrade: sinon.stub().returns(ctx.changeRequest),
      getRequestForAddOnPurchase: sinon.stub().returns(ctx.changeRequest),
      getRequestForFlexibleLicensingGroupPlanUpgrade: sinon
        .stub()
        .returns(ctx.changeRequest),
      getRequestForPoNumberAndTermsAndConditionsUpdate: sinon
        .stub()
        .returns(ctx.poNumberAndTermsAndConditionsUpdate),
      getRequestForTermsAndConditionsUpdate: sinon
        .stub()
        .returns(ctx.termsAndConditionsUpdate),
      currency: 'USD',
      hasAddOn(code) {
        return this.addOns.some(addOn => addOn.code === code)
      },
      get isCollectionMethodManual() {
        return false
      },
    }

    ctx.SubscriptionLocator = {
      promises: {
        getUsersSubscription: sinon.stub().resolves({
          groupPlan: true,
          recurlyStatus: {
            state: 'active',
          },
        }),
        getSubscriptionByMemberIdAndId: sinon.stub(),
        getSubscription: sinon.stub().resolves(ctx.subscription),
      },
    }

    ctx.changePreview = {
      currency: 'USD',
    }

    ctx.SubscriptionController = {
      makeChangePreview: sinon.stub().resolves(ctx.changePreview),
      getPlanNameForDisplay: sinon.stub().resolves(),
    }

    ctx.SubscriptionUpdater = {
      promises: {
        removeUserFromGroup: sinon.stub().resolves(),
        getSubscription: sinon.stub().resolves(),
      },
    }

    ctx.Subscription = {
      updateOne: sinon.stub().returns({ exec: sinon.stub().resolves }),
      updateMany: sinon.stub().returns({ exec: sinon.stub().resolves }),
      findOne: sinon.stub().returns({ exec: sinon.stub().resolves }),
    }

    ctx.User = {
      find: sinon.stub().returns({ exec: sinon.stub().resolves }),
    }

    ctx.SessionManager = {
      getLoggedInUserId: sinon.stub().returns(ctx.user._id),
    }

    ctx.previewSubscriptionChange = {
      nextAddOns: [
        {
          code: ctx.PaymentProviderEntities.MEMBERS_LIMIT_ADD_ON_CODE,
          quantity: ctx.recurlySubscription.addOns[0].quantity + ctx.adding,
        },
      ],
      subscription: {
        planName: 'test plan',
      },
    }

    ctx.applySubscriptionChange = {}

    ctx.RecurlyClient = {
      promises: {
        getSubscription: sinon.stub().resolves(ctx.recurlySubscription),
        getPaymentMethod: sinon.stub().resolves(ctx.paymentMethod),
        previewSubscriptionChange: sinon
          .stub()
          .resolves(ctx.previewSubscriptionChange),
        applySubscriptionChangeRequest: sinon
          .stub()
          .resolves(ctx.applySubscriptionChange),
        updateSubscriptionDetails: sinon.stub().resolves(),
      },
    }

    ctx.PlansLocator = {
      findLocalPlanInSettings: sinon.stub().returns(ctx.localPlanInSettings),
    }

    ctx.SubscriptionHandler = {
      promises: {
        syncSubscription: sinon.stub().resolves(),
      },
    }

    ctx.TeamInvitesHandler = {
      promises: {
        revokeInvite: sinon.stub().resolves(),
        createInvite: sinon.stub().resolves(),
      },
    }

    ctx.GroupPlansData = {
      enterprise: {
        collaborator: {
          USD: {
            5: {
              price_in_cents: 10000,
              additional_license_legacy_price_in_cents: 5000,
            },
          },
        },
      },
      educational: {
        collaborator: {
          USD: {
            5: {
              price_in_cents: 10000,
              additional_license_legacy_price_in_cents: 5000,
            },
          },
        },
      },
    }

    ctx.Modules = {
      promises: {
        hooks: {
          fire: sinon.stub(),
        },
      },
    }

    ctx.Modules.promises.hooks.fire
      .withArgs('generateTermsAndConditions')
      .resolves(['T&Cs'])
      .withArgs('getPaymentFromRecord')
      .resolves([
        {
          subscription: ctx.recurlySubscription,
          account: { hasPastDueInvoice: false },
        },
      ])
      .withArgs('previewSubscriptionChangeRequest')
      .resolves([ctx.previewSubscriptionChange])
      .withArgs('previewGroupPlanUpgrade')
      .resolves([{ subscriptionChange: ctx.previewSubscriptionChange }])

    vi.mock('../../../../app/src/Features/Errors/Errors.js', () =>
      vi.importActual('../../../../app/src/Features/Errors/Errors.js')
    )
    vi.doMock(
      '../../../../app/src/Features/Subscription/SubscriptionUpdater.mjs',
      () => ({
        default: ctx.SubscriptionUpdater,
      })
    )

    vi.doMock(
      '../../../../app/src/Features/Subscription/SubscriptionLocator',
      () => ({
        default: ctx.SubscriptionLocator,
      })
    )

    vi.doMock(
      '../../../../app/src/Features/Subscription/SubscriptionController',
      () => ({
        default: ctx.SubscriptionController,
      })
    )

    vi.doMock(
      '../../../../app/src/Features/Subscription/SubscriptionHandler',
      () => ({
        default: ctx.SubscriptionHandler,
      })
    )

    vi.doMock(
      '../../../../app/src/Features/Subscription/TeamInvitesHandler',
      () => ({
        default: ctx.TeamInvitesHandler,
      })
    )

    vi.doMock('../../../../app/src/models/Subscription', () => ({
      Subscription: ctx.Subscription,
    }))

    vi.doMock('../../../../app/src/models/User', () => ({
      User: ctx.User,
    }))

    vi.doMock(
      '../../../../app/src/Features/Subscription/RecurlyClient',
      () => ({
        default: ctx.RecurlyClient,
      })
    )

    vi.doMock('../../../../app/src/Features/Subscription/PlansLocator', () => ({
      default: ctx.PlansLocator,
    }))

    vi.doMock(
      '../../../../app/src/Features/Subscription/PaymentProviderEntities',
      () => ({
        default: ctx.PaymentProviderEntities,
      })
    )

    vi.doMock(
      '../../../../app/src/Features/Authentication/SessionManager',
      () => ({
        default: ctx.SessionManager,
      })
    )

    vi.doMock(
      '../../../../app/src/Features/Subscription/GroupPlansData',
      () => ({
        default: ctx.GroupPlansData,
      })
    )

    vi.doMock('../../../../app/src/infrastructure/Modules', () => ({
      default: ctx.Modules,
    }))

    ctx.Handler = (await import(modulePath)).default
  })

  describe('removeUserFromGroup', function () {
    it('should call the subscription updater to remove the user', async function (ctx) {
      const auditLog = { ipAddress: '0:0:0:0', initiatorId: ctx.user._id }
      await ctx.Handler.promises.removeUserFromGroup(
        ctx.adminUser_id,
        ctx.user._id,
        auditLog
      )

      ctx.SubscriptionUpdater.promises.removeUserFromGroup
        .calledWith(ctx.adminUser_id, ctx.user._id, auditLog)
        .should.equal(true)
    })
  })

  describe('replaceUserReferencesInGroups', function () {
    beforeEach(async function (ctx) {
      ctx.oldId = 'ba5eba11'
      ctx.newId = '5ca1ab1e'
      await ctx.Handler.promises.replaceUserReferencesInGroups(
        ctx.oldId,
        ctx.newId
      )
    })

    it('replaces the admin_id', function (ctx) {
      ctx.Subscription.updateOne
        .calledWith({ admin_id: ctx.oldId }, { admin_id: ctx.newId })
        .should.equal(true)
    })

    it('replaces the manager_ids', function (ctx) {
      ctx.Subscription.updateMany
        .calledWith(
          { manager_ids: 'ba5eba11' },
          { $addToSet: { manager_ids: '5ca1ab1e' } }
        )
        .should.equal(true)

      ctx.Subscription.updateMany
        .calledWith(
          { manager_ids: 'ba5eba11' },
          { $pull: { manager_ids: 'ba5eba11' } }
        )
        .should.equal(true)
    })

    it('replaces the member ids', function (ctx) {
      ctx.Subscription.updateMany
        .calledWith(
          { member_ids: ctx.oldId },
          { $addToSet: { member_ids: ctx.newId } }
        )
        .should.equal(true)

      ctx.Subscription.updateMany
        .calledWith(
          { member_ids: ctx.oldId },
          { $pull: { member_ids: ctx.oldId } }
        )
        .should.equal(true)
    })
  })

  describe('isUserPartOfGroup', function () {
    beforeEach(function (ctx) {
      ctx.subscription_id = '123ed13123'
    })

    it('should return true when user is part of subscription', async function (ctx) {
      ctx.SubscriptionLocator.promises.getSubscriptionByMemberIdAndId.resolves({
        _id: ctx.subscription_id,
      })
      const partOfGroup = await ctx.Handler.promises.isUserPartOfGroup(
        ctx.user_id,
        ctx.subscription_id
      )
      partOfGroup.should.equal(true)
    })

    it('should return false when no subscription is found', async function (ctx) {
      ctx.SubscriptionLocator.promises.getSubscriptionByMemberIdAndId.resolves(
        null
      )
      const partOfGroup = await ctx.Handler.promises.isUserPartOfGroup(
        ctx.user_id,
        ctx.subscription_id
      )
      partOfGroup.should.equal(false)
    })
  })

  describe('getTotalConfirmedUsersInGroup', function () {
    describe('for existing subscriptions', function () {
      beforeEach(function (ctx) {
        ctx.subscription.member_ids = ['12321', '3121321']
      })
      it('should call the subscription locator and return 2 users', async function (ctx) {
        const count = await ctx.Handler.promises.getTotalConfirmedUsersInGroup(
          ctx.subscription_id
        )
        ctx.SubscriptionLocator.promises.getSubscription
          .calledWith(ctx.subscription_id)
          .should.equal(true)
        count.should.equal(2)
      })
    })
    describe('for nonexistent subscriptions', function () {
      it('should return undefined', async function (ctx) {
        const count =
          await ctx.Handler.promises.getTotalConfirmedUsersInGroup('fake-id')
        expect(count).not.to.exist
      })
    })
  })

  describe('getUsersGroupSubscriptionDetails', function () {
    beforeEach(function (ctx) {
      ctx.req = new MockRequest(vi)
      ctx.PlansLocator.findLocalPlanInSettings = sinon.stub().returns({
        ...ctx.localPlanInSettings,
        canUseFlexibleLicensing: true,
      })
    })

    it('should throw if the subscription is not a group plan', async function (ctx) {
      ctx.SubscriptionLocator.promises.getUsersSubscription = sinon
        .stub()
        .resolves({ groupPlan: false })

      await expect(
        ctx.Handler.promises.getUsersGroupSubscriptionDetails(ctx.adminUser_id)
      ).to.be.rejectedWith('User subscription is not a group plan')
    })

    it('should return users group subscription details', async function (ctx) {
      const data = await ctx.Handler.promises.getUsersGroupSubscriptionDetails(
        ctx.adminUser_id
      )

      expect(data).to.deep.equal({
        userId: ctx.adminUser_id,
        subscription: {
          groupPlan: true,
          recurlyStatus: {
            state: 'active',
          },
        },
        plan: {
          membersLimit: 5,
          membersLimitAddOn:
            ctx.PaymentProviderEntities.MEMBERS_LIMIT_ADD_ON_CODE,
          canUseFlexibleLicensing: true,
        },
        paymentProviderSubscription: ctx.recurlySubscription,
      })
    })
  })

  describe('add seats subscription change', function () {
    beforeEach(function (ctx) {
      ctx.req = new MockRequest(vi)
      Object.assign(ctx.req.body, { adding: ctx.adding })
      ctx.PlansLocator.findLocalPlanInSettings = sinon.stub().returns({
        ...ctx.localPlanInSettings,
        canUseFlexibleLicensing: true,
      })
    })

    describe('has "additional-license" add-on', function () {
      beforeEach(function (ctx) {
        ctx.recurlySubscription.addOns = [
          {
            code: ctx.PaymentProviderEntities.MEMBERS_LIMIT_ADD_ON_CODE,
            quantity: 6,
          },
        ]
        ctx.prevQuantity = ctx.recurlySubscription.addOns[0].quantity
        ctx.previewSubscriptionChange.nextAddOns = [
          {
            code: ctx.PaymentProviderEntities.MEMBERS_LIMIT_ADD_ON_CODE,
            quantity: ctx.prevQuantity + ctx.adding,
          },
        ]
      })

      afterEach(function (ctx) {
        sinon.assert.notCalled(
          ctx.recurlySubscription.getRequestForAddOnPurchase
        )

        ctx.recurlySubscription.getRequestForAddOnUpdate
          .calledWith(
            ctx.PaymentProviderEntities.MEMBERS_LIMIT_ADD_ON_CODE,
            ctx.recurlySubscription.addOns[0].quantity + ctx.adding
          )
          .should.equal(true)
      })

      describe('previewAddSeatsSubscriptionChange', function () {
        it('should return the subscription change preview', async function (ctx) {
          const preview =
            await ctx.Handler.promises.previewAddSeatsSubscriptionChange(
              ctx.adminUser_id,
              ctx.adding
            )
          ctx.Modules.promises.hooks.fire
            .calledWith('getPaymentFromRecord', {
              groupPlan: true,
              recurlyStatus: {
                state: 'active',
              },
            })
            .should.equal(true)
          ctx.Modules.promises.hooks.fire
            .calledWith('previewSubscriptionChangeRequest', ctx.changeRequest)
            .should.equal(true)
          ctx.SubscriptionController.makeChangePreview
            .calledWith(
              {
                type: 'add-on-update',
                addOn: {
                  code: ctx.PaymentProviderEntities.MEMBERS_LIMIT_ADD_ON_CODE,
                  quantity:
                    ctx.previewSubscriptionChange.nextAddOns[0].quantity,
                  prevQuantity: ctx.prevQuantity,
                },
              },
              ctx.previewSubscriptionChange
            )
            .should.equal(true)
          preview.should.equal(ctx.changePreview)
        })
      })

      describe('createAddSeatsSubscriptionChange', function () {
        it('should change the subscription', async function (ctx) {
          ctx.recurlySubscription = {
            ...ctx.recurlySubscription,
            get isCollectionMethodManual() {
              return true
            },
          }
          ctx.Modules.promises.hooks.fire
            .withArgs('getPaymentFromRecord')
            .resolves([
              {
                subscription: ctx.recurlySubscription,
                account: { hasPastDueInvoice: false },
              },
            ])

          const result =
            await ctx.Handler.promises.createAddSeatsSubscriptionChange(
              ctx.adminUser_id,
              ctx.adding,
              '123'
            )
          ctx.Modules.promises.hooks.fire
            .calledWith(
              'updateSubscriptionDetails',
              sinon.match
                .has('poNumber')
                .and(sinon.match.has('termsAndConditions'))
            )
            .should.equal(true)
          ctx.Modules.promises.hooks.fire
            .calledWith(
              'applySubscriptionChangeRequestAndSync',
              ctx.changeRequest
            )
            .should.equal(true)
          expect(result).to.deep.equal({
            adding: ctx.req.body.adding,
          })
        })
      })
    })

    describe('updateSubscriptionPaymentTerms', function () {
      describe('accounts with PO number', function () {
        it('should update the subscription PO number and T&C', async function (ctx) {
          await ctx.Handler.promises.updateSubscriptionPaymentTerms(
            ctx.recurlySubscription,
            ctx.poNumberAndTermsAndConditionsUpdate.poNumber
          )
          ctx.recurlySubscription.getRequestForPoNumberAndTermsAndConditionsUpdate
            .calledWithMatch(
              ctx.poNumberAndTermsAndConditionsUpdate.poNumber,
              'T&Cs'
            )
            .should.equal(true)
          ctx.Modules.promises.hooks.fire
            .calledWith(
              'updateSubscriptionDetails',
              ctx.poNumberAndTermsAndConditionsUpdate
            )
            .should.equal(true)
        })
      })

      describe('accounts with no PO number', function () {
        it('should update the subscription T&C only', async function (ctx) {
          await ctx.Handler.promises.updateSubscriptionPaymentTerms(
            ctx.recurlySubscription
          )
          ctx.recurlySubscription.getRequestForTermsAndConditionsUpdate
            .calledWithMatch('T&Cs')
            .should.equal(true)
          ctx.Modules.promises.hooks.fire
            .calledWith(
              'updateSubscriptionDetails',
              ctx.termsAndConditionsUpdate
            )
            .should.equal(true)
        })
      })
    })

    describe('has no "additional-license" add-on', function () {
      beforeEach(function (ctx) {
        ctx.recurlySubscription.addOns = []
        ctx.prevQuantity = ctx.recurlySubscription.addOns[0]?.quantity ?? 0
        ctx.previewSubscriptionChange.nextAddOns = [
          {
            code: ctx.PaymentProviderEntities.MEMBERS_LIMIT_ADD_ON_CODE,
            quantity: ctx.prevQuantity + ctx.adding,
          },
        ]
        ctx.PlansLocator.findLocalPlanInSettings = sinon.stub().returns({
          ...ctx.localPlanInSettings,
          planCode: 'group_collaborator_5_enterprise',
          canUseFlexibleLicensing: true,
        })
      })

      afterEach(function (ctx) {
        sinon.assert.notCalled(ctx.recurlySubscription.getRequestForAddOnUpdate)
      })

      describe('previewAddSeatsSubscriptionChange', function () {
        let preview

        afterEach(function (ctx) {
          ctx.Modules.promises.hooks.fire
            .calledWith('getPaymentFromRecord', {
              groupPlan: true,
              recurlyStatus: {
                state: 'active',
              },
            })
            .should.equal(true)
          ctx.Modules.promises.hooks.fire
            .calledWith('previewSubscriptionChangeRequest', ctx.changeRequest)
            .should.equal(true)
          ctx.SubscriptionController.makeChangePreview
            .calledWith(
              {
                type: 'add-on-update',
                addOn: {
                  code: ctx.PaymentProviderEntities.MEMBERS_LIMIT_ADD_ON_CODE,
                  quantity:
                    ctx.previewSubscriptionChange.nextAddOns[0].quantity,
                  prevQuantity: ctx.prevQuantity,
                },
              },
              ctx.previewSubscriptionChange
            )
            .should.equal(true)
          preview.should.equal(ctx.changePreview)
        })

        it('should return the subscription change preview with legacy add-on price', async function (ctx) {
          ctx.recurlySubscription.planPrice =
            ctx.GroupPlansData.enterprise.collaborator.USD[5].price_in_cents /
              100 -
            1

          preview =
            await ctx.Handler.promises.previewAddSeatsSubscriptionChange(
              ctx.adminUser_id,
              ctx.adding
            )
          ctx.recurlySubscription.getRequestForAddOnPurchase
            .calledWithExactly(
              ctx.PaymentProviderEntities.MEMBERS_LIMIT_ADD_ON_CODE,
              ctx.adding,
              ctx.GroupPlansData.enterprise.collaborator.USD[5]
                .additional_license_legacy_price_in_cents / 100
            )
            .should.equal(true)
        })

        it('should return the subscription change preview with non-legacy add-on price', async function (ctx) {
          ctx.recurlySubscription.planPrice =
            ctx.GroupPlansData.enterprise.collaborator.USD[5].price_in_cents /
            100

          preview =
            await ctx.Handler.promises.previewAddSeatsSubscriptionChange(
              ctx.adminUser_id,
              ctx.adding
            )
          ctx.recurlySubscription.getRequestForAddOnPurchase
            .calledWithExactly(
              ctx.PaymentProviderEntities.MEMBERS_LIMIT_ADD_ON_CODE,
              ctx.adding,
              undefined
            )
            .should.equal(true)
        })

        it('should return the subscription change preview with legacy add-on price for small educational group', async function (ctx) {
          ctx.PlansLocator.findLocalPlanInSettings = sinon.stub().returns({
            ...ctx.localPlanInSettings,
            planCode: 'group_collaborator_5_educational',
            canUseFlexibleLicensing: true,
          })
          ctx.recurlySubscription.planPrice =
            ctx.GroupPlansData.enterprise.collaborator.USD[5].price_in_cents /
              100 +
            1

          preview =
            await ctx.Handler.promises.previewAddSeatsSubscriptionChange(
              ctx.adminUser_id,
              ctx.adding
            )
          ctx.recurlySubscription.getRequestForAddOnPurchase
            .calledWithExactly(
              ctx.PaymentProviderEntities.MEMBERS_LIMIT_ADD_ON_CODE,
              ctx.adding,
              ctx.GroupPlansData.enterprise.collaborator.USD[5]
                .additional_license_legacy_price_in_cents / 100
            )
            .should.equal(true)
        })

        it('should return the subscription change preview with non-legacy add-on price for small educational group', async function (ctx) {
          ctx.PlansLocator.findLocalPlanInSettings = sinon.stub().returns({
            ...ctx.localPlanInSettings,
            planCode: 'group_collaborator_5_educational',
            canUseFlexibleLicensing: true,
          })
          ctx.recurlySubscription.planPrice =
            ctx.GroupPlansData.enterprise.collaborator.USD[5].price_in_cents /
            100

          preview =
            await ctx.Handler.promises.previewAddSeatsSubscriptionChange(
              ctx.adminUser_id,
              ctx.adding
            )
          ctx.recurlySubscription.getRequestForAddOnPurchase
            .calledWithExactly(
              ctx.PaymentProviderEntities.MEMBERS_LIMIT_ADD_ON_CODE,
              ctx.adding,
              undefined
            )
            .should.equal(true)
        })
      })
    })
  })

  describe('ensureFlexibleLicensingEnabled', function () {
    it('should throw if the subscription can not use flexible licensing', async function (ctx) {
      await expect(
        ctx.Handler.promises.ensureFlexibleLicensingEnabled({
          canUseFlexibleLicensing: false,
        })
      ).to.be.rejectedWith('The group plan does not support flexible licensing')
    })

    it('should not throw if the subscription can use flexible licensing', async function (ctx) {
      await expect(
        ctx.Handler.promises.ensureFlexibleLicensingEnabled({
          canUseFlexibleLicensing: true,
        })
      ).to.not.be.rejected
    })
  })

  describe('ensureSubscriptionIsActive', function () {
    it('should throw if the subscription is not active', async function (ctx) {
      await expect(
        ctx.Handler.promises.ensureSubscriptionIsActive(ctx.subscription)
      ).to.be.rejectedWith('The subscription is not active')
    })

    it('should not throw if the subscription is active', async function (ctx) {
      await expect(
        ctx.Handler.promises.ensureSubscriptionIsActive({
          recurlyStatus: { state: 'active' },
        })
      ).to.not.be.rejected
    })
  })

  describe('ensureSubscriptionCollectionMethodIsNotManual', function () {
    it('should throw if the subscription is manually collected', async function (ctx) {
      await expect(
        ctx.Handler.promises.ensureSubscriptionCollectionMethodIsNotManual({
          get isCollectionMethodManual() {
            return true
          },
        })
      ).to.be.rejectedWith('This subscription is being collected manually')
    })

    it('should not throw if the subscription is automatically collected', async function (ctx) {
      await expect(
        ctx.Handler.promises.ensureSubscriptionCollectionMethodIsNotManual({
          get isCollectionMethodManual() {
            return false
          },
        })
      ).to.not.be.rejected
    })
  })

  describe('ensureSubscriptionHasNoPastDueInvoice', function () {
    it('should throw if the subscription has past due invoice', async function (ctx) {
      ctx.Modules.promises.hooks.fire
        .withArgs('getPaymentFromRecord')
        .resolves([{ account: { hasPastDueInvoice: true } }])
      await expect(
        ctx.Handler.promises.ensureSubscriptionHasNoPastDueInvoice(
          ctx.subscription
        )
      ).to.be.rejectedWith('This subscription has a past due invoice')
    })

    it('should not throw if the subscription has no past due invoice', async function (ctx) {
      await expect(
        ctx.Handler.promises.ensureSubscriptionHasNoPastDueInvoice(
          ctx.subscription
        )
      ).to.not.be.rejected
    })
  })

  describe('ensureSubscriptionHasAdditionalLicenseAddOnWhenCollectionMethodIsManual', function () {
    it('should throw if the subscription is manually collected and has no additional license add-on', async function (ctx) {
      await expect(
        ctx.Handler.promises.ensureSubscriptionHasAdditionalLicenseAddOnWhenCollectionMethodIsManual(
          {
            isCollectionMethodManual: true,
            hasAddOn: sinon
              .stub()
              .withArgs('additional-license')
              .returns(false),
          }
        )
      ).to.be.rejectedWith(
        'This subscription is being collected manually has no "additional-license" add-on'
      )
    })

    it('should not throw if the subscription is not manually collected and has no additional license add-on and ', async function (ctx) {
      await expect(
        ctx.Handler.promises.ensureSubscriptionHasAdditionalLicenseAddOnWhenCollectionMethodIsManual(
          {
            isCollectionMethodManual: false,
            hasAddOn: sinon
              .stub()
              .withArgs('additional-license')
              .returns(false),
          }
        )
      ).to.not.be.rejected
    })

    it('should not throw if the subscription is not manually collected and has additional license add-on', async function (ctx) {
      await expect(
        ctx.Handler.promises.ensureSubscriptionHasAdditionalLicenseAddOnWhenCollectionMethodIsManual(
          {
            isCollectionMethodManual: true,
            hasAddOn: sinon.stub().withArgs('additional-license').returns(true),
          }
        )
      ).to.not.be.rejected
    })
  })

  describe('getGroupPlanUpgradePreview', function () {
    it('should generate preview for subscription upgrade', async function (ctx) {
      const result = await ctx.Handler.promises.getGroupPlanUpgradePreview(
        ctx.user_id
      )
      result.should.equal(ctx.changePreview)
    })
  })

  describe('checkBillingInfoExistence', function () {
    it('should invoke the payment method function when collection method is "automatic"', async function (ctx) {
      await ctx.Handler.promises.checkBillingInfoExistence(
        ctx.recurlySubscription,
        ctx.adminUser_id
      )
      ctx.Modules.promises.hooks.fire
        .calledWith('getPaymentMethod', ctx.adminUser_id)
        .should.equal(true)
    })

    it('shouldn’t invoke the payment method function when collection method is "manual"', async function (ctx) {
      const recurlySubscription = {
        ...ctx.recurlySubscription,
        get isCollectionMethodManual() {
          return true
        },
      }
      await ctx.Handler.promises.checkBillingInfoExistence(
        recurlySubscription,
        ctx.adminUser_id
      )
      ctx.RecurlyClient.promises.getPaymentMethod.should.not.have.been.called
    })
  })

  describe('updateGroupMembersBulk', function () {
    const inviterId = new ObjectId()

    let members
    let emailList
    let callUpdateGroupMembersBulk

    beforeEach(function (ctx) {
      members = [
        {
          _id: new ObjectId(),
          email: 'user1@example.com',
          emails: [{ email: 'user1@example.com' }],
        },
        {
          _id: new ObjectId(),
          email: 'user2-alias@example.com',
          emails: [
            {
              email: 'user2-alias@example.com',
            },
            {
              email: 'user2@example.com',
            },
          ],
        },
        {
          _id: new ObjectId(),
          email: 'user3@example.com',
          emails: [{ email: 'user3@example.com' }],
        },
      ]

      emailList = [
        'user1@example.com',
        'user2@example.com',
        'new-user@example.com', // primary email of existing user
        'new-user-2@example.com', // secondary email of existing user
      ]
      callUpdateGroupMembersBulk = async (options = {}) => {
        ctx.Subscription.findOne = sinon
          .stub()
          .returns({ exec: sinon.stub().resolves(ctx.subscription) })

        ctx.User.find = sinon
          .stub()
          .returns({ exec: sinon.stub().resolves(members) })

        return await ctx.Handler.promises.updateGroupMembersBulk(
          inviterId,
          ctx.subscription._id,
          emailList,
          options
        )
      }
    })

    it('throws an error when any of the emails is invalid', async function () {
      emailList.push('invalid@email')

      await expect(
        callUpdateGroupMembersBulk({ commit: true })
      ).to.be.rejectedWith(InvalidEmailError)
    })

    describe('with commit = false', function () {
      describe('with removeMembersNotIncluded = false', function () {
        it('should preview zero users to delete, and should not send invites', async function (ctx) {
          const result = await callUpdateGroupMembersBulk()

          expect(result).to.deep.equal({
            emailsToSendInvite: [
              'new-user@example.com',
              'new-user-2@example.com',
            ],
            emailsToRevokeInvite: [],
            membersToRemove: [],
            currentMemberCount: 3,
            newTotalCount: 5,
            membersLimit: ctx.subscription.membersLimit,
          })

          expect(ctx.TeamInvitesHandler.promises.createInvite).not.to.have.been
            .called

          expect(ctx.SubscriptionUpdater.promises.removeUserFromGroup).not.to
            .have.been.called
        })
      })

      describe('with removeMembersNotIncluded = true', function () {
        it('should preview the users to be deleted, and should not send invites', async function (ctx) {
          const result = await callUpdateGroupMembersBulk({
            removeMembersNotIncluded: true,
          })

          expect(result).to.deep.equal({
            emailsToSendInvite: [
              'new-user@example.com',
              'new-user-2@example.com',
            ],
            emailsToRevokeInvite: [],
            membersToRemove: [members[2]._id],
            currentMemberCount: 3,
            newTotalCount: 4,
            membersLimit: ctx.subscription.membersLimit,
          })

          expect(ctx.TeamInvitesHandler.promises.createInvite).not.to.have.been
            .called

          expect(ctx.SubscriptionUpdater.promises.removeUserFromGroup).not.to
            .have.been.called
        })

        it('should preview but not revoke invites to emails that are no longer invited', async function (ctx) {
          ctx.subscription.teamInvites = [
            { email: 'new-user@example.com' },
            { email: 'no-longer-invited@example.com' },
          ]

          const result = await callUpdateGroupMembersBulk({
            removeMembersNotIncluded: true,
          })

          expect(result.emailsToRevokeInvite).to.deep.equal([
            'no-longer-invited@example.com',
          ])

          expect(ctx.TeamInvitesHandler.promises.revokeInvite).not.to.have.been
            .called
        })
      })

      it('does not throw an error when the member limit is reached', async function (ctx) {
        ctx.subscription.membersLimit = 3
        const result = await callUpdateGroupMembersBulk()

        expect(result.membersLimit).to.equal(3)
        expect(result.newTotalCount).to.equal(5)
      })
    })

    describe('with commit = true', function () {
      describe('with removeMembersNotIncluded = false', function () {
        it('should preview zero users to delete, and should send invites', async function (ctx) {
          const result = await callUpdateGroupMembersBulk({ commit: true })

          expect(result).to.deep.equal({
            emailsToSendInvite: [
              'new-user@example.com',
              'new-user-2@example.com',
            ],
            emailsToRevokeInvite: [],
            membersToRemove: [],
            currentMemberCount: 3,
            newTotalCount: 5,
            membersLimit: ctx.subscription.membersLimit,
          })

          expect(ctx.SubscriptionUpdater.promises.removeUserFromGroup).not.to
            .have.been.called

          expect(
            ctx.TeamInvitesHandler.promises.createInvite.callCount
          ).to.equal(2)

          expect(
            ctx.TeamInvitesHandler.promises.createInvite
          ).to.have.been.calledWith(
            inviterId,
            ctx.subscription,
            'new-user@example.com'
          )

          expect(
            ctx.TeamInvitesHandler.promises.createInvite
          ).to.have.been.calledWith(
            inviterId,
            ctx.subscription,
            'new-user-2@example.com'
          )
        })

        it('should not send invites to emails already invited', async function (ctx) {
          ctx.subscription.teamInvites = [{ email: 'new-user@example.com' }]

          const result = await callUpdateGroupMembersBulk({ commit: true })

          expect(result.emailsToSendInvite).to.deep.equal([
            'new-user-2@example.com',
          ])

          expect(
            ctx.TeamInvitesHandler.promises.createInvite.callCount
          ).to.equal(1)

          expect(
            ctx.TeamInvitesHandler.promises.createInvite
          ).to.have.been.calledWith(
            inviterId,
            ctx.subscription,
            'new-user-2@example.com'
          )
        })

        it('should preview and not revoke invites to emails that are no longer invited', async function (ctx) {
          ctx.subscription.teamInvites = [
            { email: 'new-user@example.com' },
            { email: 'no-longer-invited@example.com' },
          ]

          const result = await callUpdateGroupMembersBulk({
            commit: true,
          })

          expect(result.emailsToRevokeInvite).to.deep.equal([])

          expect(ctx.TeamInvitesHandler.promises.revokeInvite).not.to.have.been
            .called
        })
      })

      describe('with removeMembersNotIncluded = true', function () {
        it('should remove users from group, and should send invites', async function (ctx) {
          const result = await callUpdateGroupMembersBulk({
            commit: true,
            removeMembersNotIncluded: true,
          })

          expect(result).to.deep.equal({
            emailsToSendInvite: [
              'new-user@example.com',
              'new-user-2@example.com',
            ],
            emailsToRevokeInvite: [],
            membersToRemove: [members[2]._id],
            currentMemberCount: 3,
            newTotalCount: 4,
            membersLimit: ctx.subscription.membersLimit,
          })

          expect(
            ctx.SubscriptionUpdater.promises.removeUserFromGroup.callCount
          ).to.equal(1)

          expect(
            ctx.SubscriptionUpdater.promises.removeUserFromGroup
          ).to.have.been.calledWith(ctx.subscription._id, members[2]._id, {
            initiatorId: inviterId,
          })

          expect(
            ctx.TeamInvitesHandler.promises.createInvite.callCount
          ).to.equal(2)

          expect(
            ctx.TeamInvitesHandler.promises.createInvite
          ).to.have.been.calledWith(
            inviterId,
            ctx.subscription,
            'new-user@example.com'
          )

          expect(
            ctx.TeamInvitesHandler.promises.createInvite
          ).to.have.been.calledWith(
            inviterId,
            ctx.subscription,
            'new-user-2@example.com'
          )
        })

        it('should send invites and revoke invites to emails no longer invited', async function (ctx) {
          ctx.subscription.teamInvites = [
            { email: 'new-user@example.com' },
            { email: 'no-longer-invited@example.com' },
          ]

          const result = await callUpdateGroupMembersBulk({
            commit: true,
            removeMembersNotIncluded: true,
          })

          expect(result.emailsToSendInvite).to.deep.equal([
            'new-user-2@example.com',
          ])

          expect(result.emailsToRevokeInvite).to.deep.equal([
            'no-longer-invited@example.com',
          ])

          expect(
            ctx.TeamInvitesHandler.promises.createInvite.callCount
          ).to.equal(1)

          expect(
            ctx.TeamInvitesHandler.promises.createInvite
          ).to.have.been.calledWith(
            inviterId,
            ctx.subscription,
            'new-user-2@example.com'
          )

          expect(
            ctx.TeamInvitesHandler.promises.revokeInvite.callCount
          ).to.equal(1)

          expect(
            ctx.TeamInvitesHandler.promises.revokeInvite
          ).to.have.been.calledWith(
            inviterId,
            ctx.subscription,
            'no-longer-invited@example.com'
          )
        })
      })

      it('throws an error when the member limit is reached', async function (ctx) {
        ctx.subscription.membersLimit = 3
        await expect(
          callUpdateGroupMembersBulk({ commit: true })
        ).to.be.rejectedWith('limit reached')
      })
    })
  })
})
