import { beforeEach, describe, it, vi } from 'vitest'
import sinon from 'sinon'

const modulePath =
  '../../../../app/src/Features/Subscription/SubscriptionGroupController'

describe('SubscriptionGroupController', function () {
  beforeEach(async function (ctx) {
    ctx.user = { _id: '!@312431', email: 'user@email.com' }
    ctx.adminUserId = '123jlkj'
    ctx.subscriptionId = '123434325412'
    ctx.user_email = 'bob@gmail.com'
    ctx.req = {
      session: {
        user: {
          _id: ctx.adminUserId,
          email: ctx.user_email,
        },
      },
      params: {
        subscriptionId: ctx.subscriptionId,
      },
      query: {},
    }

    ctx.subscription = {
      _id: ctx.subscriptionId,
      teamName: 'Cool group',
      groupPlan: true,
      membersLimit: 5,
      planCode: 'group_collaborator_10',
    }

    ctx.plan = {
      canUseFlexibleLicensing: true,
    }

    ctx.recurlySubscription = {
      get isCollectionMethodManual() {
        return true
      },
    }

    ctx.previewSubscriptionChangeData = {
      change: {},
      currency: 'USD',
    }

    ctx.createSubscriptionChangeData = { adding: 1 }

    ctx.paymentMethod = { cardType: 'Visa', lastFour: '1111' }

    ctx.SubscriptionGroupHandler = {
      promises: {
        removeUserFromGroup: sinon.stub().resolves(),
        getUsersGroupSubscriptionDetails: sinon.stub().resolves({
          subscription: ctx.subscription,
          plan: ctx.plan,
          paymentProviderSubscription: ctx.recurlySubscription,
        }),
        previewAddSeatsSubscriptionChange: sinon
          .stub()
          .resolves(ctx.previewSubscriptionChangeData),
        createAddSeatsSubscriptionChange: sinon
          .stub()
          .resolves(ctx.createSubscriptionChangeData),
        ensureFlexibleLicensingEnabled: sinon.stub().resolves(),
        ensureSubscriptionIsActive: sinon.stub().resolves(),
        ensureSubscriptionCollectionMethodIsNotManual: sinon.stub().resolves(),
        ensureSubscriptionHasNoPastDueInvoice: sinon.stub().resolves(),
        getGroupPlanUpgradePreview: sinon
          .stub()
          .resolves(ctx.previewSubscriptionChangeData),
        checkBillingInfoExistence: sinon.stub().resolves(ctx.paymentMethod),
        updateSubscriptionPaymentTerms: sinon.stub().resolves(),
        ensureSubscriptionHasAdditionalLicenseAddOnWhenCollectionMethodIsManual:
          sinon.stub().resolves(),
      },
    }

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

    ctx.SessionManager = {
      getLoggedInUserId(session) {
        return session.user._id
      },
      getSessionUser(session) {
        return session.user
      },
    }

    ctx.UserAuditLogHandler = {
      promises: {
        addEntry: sinon.stub().resolves(),
      },
    }

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

    ctx.UserGetter = {
      promises: {
        getUserEmail: sinon.stub().resolves(ctx.user.email),
      },
    }

    ctx.paymentMethod = { cardType: 'Visa', lastFour: '1111' }

    ctx.RecurlyClient = {
      promises: {
        getPaymentMethod: sinon.stub().resolves(ctx.paymentMethod),
      },
    }

    ctx.SubscriptionController = {}

    ctx.SubscriptionModel = { Subscription: {} }

    ctx.PlansHelper = {
      isProfessionalGroupPlan: sinon.stub().returns(false),
    }

    ctx.Errors = {
      MissingBillingInfoError: class extends Error {},
      ManuallyCollectedError: class extends Error {},
      InactiveError: class extends Error {},
      SubtotalLimitExceededError: class extends Error {},
      HasPastDueInvoiceError: class extends Error {},
      HasNoAdditionalLicenseWhenManuallyCollectedError: class extends Error {},
      PaymentActionRequiredError: class extends Error {
        constructor(info) {
          super('Payment action required')
          this.info = info
        }
      },
      MultiplePendingChangesError: class extends Error {},
    }

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

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

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

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

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

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

    vi.doMock('../../../../app/src/Features/Errors/ErrorController', () => ({
      default: (ctx.ErrorController = {
        notFound: sinon.stub(),
      }),
    }))

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

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

    vi.doMock(
      '../../../../app/src/Features/Subscription/PlansHelper',
      () => ctx.PlansHelper
    )

    vi.doMock(
      '../../../../app/src/Features/Subscription/Errors',
      () => ctx.Errors
    )

    vi.doMock(
      '../../../../app/src/models/Subscription',
      () => ctx.SubscriptionModel
    )

    vi.doMock('@overleaf/settings', () => ({
      default: { adminUrl: 'https://admin.overleaf.com' },
    }))

    vi.doMock('@overleaf/logger', () => ({
      default: {
        err: sinon.stub(),
        error: sinon.stub(),
        warn: sinon.stub(),
        log: sinon.stub(),
        debug: sinon.stub(),
      },
    }))

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

  describe('removeUserFromGroup', function () {
    it('should use the subscription id for the logged in user and take the user id from the params', async function (ctx) {
      await new Promise(resolve => {
        const userIdToRemove = '31231'
        ctx.req.params = { user_id: userIdToRemove }
        ctx.req.entity = ctx.subscription

        const res = {
          sendStatus: () => {
            ctx.SubscriptionGroupHandler.promises.removeUserFromGroup
              .calledWith(ctx.subscriptionId, userIdToRemove, {
                initiatorId: ctx.req.session.user._id,
                ipAddress: ctx.req.ip,
              })
              .should.equal(true)
            resolve()
          },
        }
        ctx.Controller.removeUserFromGroup(ctx.req, res, resolve)
      })
    })

    it('should log that the user has been removed', async function (ctx) {
      await new Promise(resolve => {
        const userIdToRemove = '31231'
        ctx.req.params = { user_id: userIdToRemove }
        ctx.req.entity = ctx.subscription

        const res = {
          sendStatus: () => {
            sinon.assert.calledWith(
              ctx.UserAuditLogHandler.promises.addEntry,
              userIdToRemove,
              'remove-from-group-subscription',
              ctx.adminUserId,
              ctx.req.ip,
              { subscriptionId: ctx.subscriptionId }
            )
            resolve()
          },
        }
        ctx.Controller.removeUserFromGroup(ctx.req, res, resolve)
      })
    })

    it('should call the group SSO hooks with group SSO enabled', async function (ctx) {
      await new Promise(resolve => {
        const userIdToRemove = '31231'
        ctx.req.params = { user_id: userIdToRemove }
        ctx.req.entity = ctx.subscription
        ctx.Modules.promises.hooks.fire
          .withArgs('hasGroupSSOEnabled', ctx.subscription)
          .resolves([true])

        const res = {
          sendStatus: () => {
            ctx.Modules.promises.hooks.fire
              .calledWith('hasGroupSSOEnabled', ctx.subscription)
              .should.equal(true)
            ctx.Modules.promises.hooks.fire
              .calledWith(
                'unlinkUserFromGroupSSO',
                userIdToRemove,
                ctx.subscriptionId
              )
              .should.equal(true)
            sinon.assert.calledTwice(ctx.Modules.promises.hooks.fire)
            resolve()
          },
        }
        ctx.Controller.removeUserFromGroup(ctx.req, res, resolve)
      })
    })

    it('should call the group SSO hooks with group SSO disabled', async function (ctx) {
      await new Promise(resolve => {
        const userIdToRemove = '31231'
        ctx.req.params = { user_id: userIdToRemove }
        ctx.req.entity = ctx.subscription
        ctx.Modules.promises.hooks.fire
          .withArgs('hasGroupSSOEnabled', ctx.subscription)
          .resolves([false])

        const res = {
          sendStatus: () => {
            ctx.Modules.promises.hooks.fire
              .calledWith('hasGroupSSOEnabled', ctx.subscription)
              .should.equal(true)
            sinon.assert.calledOnce(ctx.Modules.promises.hooks.fire)
            resolve()
          },
        }
        ctx.Controller.removeUserFromGroup(ctx.req, res, resolve)
      })
    })
  })

  describe('removeSelfFromGroup', function () {
    it('gets subscription and remove user', async function (ctx) {
      await new Promise(resolve => {
        ctx.req.query = { subscriptionId: ctx.subscriptionId }
        const memberUserIdToremove = 123456789
        ctx.req.session.user._id = memberUserIdToremove

        const res = {
          sendStatus: () => {
            sinon.assert.calledWith(
              ctx.SubscriptionLocator.promises.getSubscription,
              ctx.subscriptionId
            )
            sinon.assert.calledWith(
              ctx.SubscriptionGroupHandler.promises.removeUserFromGroup,
              ctx.subscriptionId,
              memberUserIdToremove,
              {
                initiatorId: ctx.req.session.user._id,
                ipAddress: ctx.req.ip,
              }
            )
            resolve()
          },
        }
        ctx.Controller.removeSelfFromGroup(ctx.req, res, resolve)
      })
    })

    it('should log that the user has left the subscription', async function (ctx) {
      await new Promise(resolve => {
        ctx.req.query = { subscriptionId: ctx.subscriptionId }
        const memberUserIdToremove = '123456789'
        ctx.req.session.user._id = memberUserIdToremove

        const res = {
          sendStatus: () => {
            sinon.assert.calledWith(
              ctx.UserAuditLogHandler.promises.addEntry,
              memberUserIdToremove,
              'remove-from-group-subscription',
              memberUserIdToremove,
              ctx.req.ip,
              { subscriptionId: ctx.subscriptionId }
            )
            resolve()
          },
        }
        ctx.Controller.removeSelfFromGroup(ctx.req, res, resolve)
      })
    })

    it('should call the group SSO hooks with group SSO enabled', async function (ctx) {
      await new Promise(resolve => {
        ctx.req.query = { subscriptionId: ctx.subscriptionId }
        const memberUserIdToremove = '123456789'
        ctx.req.session.user._id = memberUserIdToremove

        ctx.Modules.promises.hooks.fire
          .withArgs('hasGroupSSOEnabled', ctx.subscription)
          .resolves([true])

        const res = {
          sendStatus: () => {
            ctx.Modules.promises.hooks.fire
              .calledWith('hasGroupSSOEnabled', ctx.subscription)
              .should.equal(true)
            ctx.Modules.promises.hooks.fire
              .calledWith(
                'unlinkUserFromGroupSSO',
                memberUserIdToremove,
                ctx.subscriptionId
              )
              .should.equal(true)
            sinon.assert.calledTwice(ctx.Modules.promises.hooks.fire)
            resolve()
          },
        }
        ctx.Controller.removeSelfFromGroup(ctx.req, res, resolve)
      })
    })

    it('should call the group SSO hooks with group SSO disabled', async function (ctx) {
      await new Promise(resolve => {
        const userIdToRemove = '31231'
        ctx.req.session.user._id = userIdToRemove
        ctx.req.params = { user_id: userIdToRemove }
        ctx.req.entity = ctx.subscription
        ctx.Modules.promises.hooks.fire
          .withArgs('hasGroupSSOEnabled', ctx.subscription)
          .resolves([false])

        const res = {
          sendStatus: () => {
            ctx.Modules.promises.hooks.fire
              .calledWith('hasGroupSSOEnabled', ctx.subscription)
              .should.equal(true)
            sinon.assert.calledOnce(ctx.Modules.promises.hooks.fire)
            resolve()
          },
        }
        ctx.Controller.removeSelfFromGroup(ctx.req, res, resolve)
      })
    })
  })

  describe('addSeatsToGroupSubscription', function () {
    it('should render the "add seats" page', async function (ctx) {
      await new Promise((resolve, reject) => {
        const res = {
          render: (page, props) => {
            ctx.SubscriptionGroupHandler.promises.getUsersGroupSubscriptionDetails
              .calledWith(ctx.req.session.user._id)
              .should.equal(true)
            ctx.SubscriptionGroupHandler.promises.ensureFlexibleLicensingEnabled
              .calledWith(ctx.plan)
              .should.equal(true)
            ctx.SubscriptionGroupHandler.promises.ensureSubscriptionIsActive
              .calledWith(ctx.subscription)
              .should.equal(true)
            ctx.SubscriptionGroupHandler.promises.ensureSubscriptionHasNoPastDueInvoice
              .calledWith(ctx.subscription)
              .should.equal(true)
            ctx.SubscriptionGroupHandler.promises.checkBillingInfoExistence
              .calledWith(ctx.recurlySubscription, ctx.adminUserId)
              .should.equal(true)
            ctx.SubscriptionGroupHandler.promises.ensureSubscriptionHasAdditionalLicenseAddOnWhenCollectionMethodIsManual
              .calledWith(ctx.recurlySubscription)
              .should.equal(true)
            page.should.equal('subscriptions/add-seats')
            props.subscriptionId.should.equal(ctx.subscriptionId)
            props.groupName.should.equal(ctx.subscription.teamName)
            props.totalLicenses.should.equal(ctx.subscription.membersLimit)
            props.isProfessional.should.equal(false)
            props.isCollectionMethodManual.should.equal(true)
            resolve()
          },
        }

        ctx.Controller.addSeatsToGroupSubscription(ctx.req, res)
      })
    })

    it('should redirect to subscription page when getting subscription details fails', async function (ctx) {
      await new Promise(resolve => {
        ctx.SubscriptionGroupHandler.promises.getUsersGroupSubscriptionDetails =
          sinon.stub().rejects()

        const res = {
          redirect: url => {
            url.should.equal('/user/subscription')
            resolve()
          },
        }

        ctx.Controller.addSeatsToGroupSubscription(ctx.req, res)
      })
    })

    it('should redirect to subscription page when flexible licensing is not enabled', async function (ctx) {
      await new Promise(resolve => {
        ctx.SubscriptionGroupHandler.promises.ensureFlexibleLicensingEnabled =
          sinon.stub().rejects()

        const res = {
          redirect: url => {
            url.should.equal('/user/subscription')
            resolve()
          },
        }

        ctx.Controller.addSeatsToGroupSubscription(ctx.req, res)
      })
    })

    it('should redirect to missing billing information page when billing information is missing', async function (ctx) {
      await new Promise(resolve => {
        ctx.SubscriptionGroupHandler.promises.checkBillingInfoExistence = sinon
          .stub()
          .throws(new ctx.Errors.MissingBillingInfoError())

        const res = {
          redirect: url => {
            url.should.equal(
              '/user/subscription/group/missing-billing-information'
            )
            resolve()
          },
        }

        ctx.Controller.addSeatsToGroupSubscription(ctx.req, res)
      })
    })

    it('should redirect to manually collected subscription error page when collection method is manual and has no additional license add-on', async function (ctx) {
      await new Promise(resolve => {
        ctx.SubscriptionGroupHandler.promises.ensureSubscriptionHasAdditionalLicenseAddOnWhenCollectionMethodIsManual =
          sinon
            .stub()
            .throws(
              new ctx.Errors.HasNoAdditionalLicenseWhenManuallyCollectedError()
            )

        const res = {
          redirect: url => {
            url.should.equal(
              '/user/subscription/group/manually-collected-subscription?error_type=no-additional-license'
            )
            resolve()
          },
        }

        ctx.Controller.addSeatsToGroupSubscription(ctx.req, res)
      })
    })

    it('should redirect to subscription page when subscription is not active', async function (ctx) {
      await new Promise(resolve => {
        ctx.SubscriptionGroupHandler.promises.ensureSubscriptionIsActive = sinon
          .stub()
          .rejects()

        const res = {
          redirect: url => {
            url.should.equal('/user/subscription')
            resolve()
          },
        }

        ctx.Controller.addSeatsToGroupSubscription(ctx.req, res)
      })
    })

    it('should redirect to subscription page when subscription has pending invoice', async function (ctx) {
      ctx.SubscriptionGroupHandler.promises.ensureSubscriptionHasNoPastDueInvoice =
        sinon.stub().rejects()

      await new Promise(resolve => {
        const res = {
          redirect: url => {
            url.should.equal('/user/subscription')
            resolve()
          },
        }

        ctx.Controller.addSeatsToGroupSubscription(ctx.req, res)
      })
    })
  })

  describe('previewAddSeatsSubscriptionChange', function () {
    it('should preview "add seats" change', async function (ctx) {
      await new Promise(resolve => {
        ctx.req.body = { adding: 2 }

        const res = {
          json: data => {
            ctx.SubscriptionGroupHandler.promises.previewAddSeatsSubscriptionChange
              .calledWith(ctx.req.session.user._id, ctx.req.body.adding)
              .should.equal(true)
            data.should.deep.equal(ctx.previewSubscriptionChangeData)
            resolve()
          },
        }

        ctx.Controller.previewAddSeatsSubscriptionChange(ctx.req, res)
      })
    })

    it('should fail previewing "add seats" change', async function (ctx) {
      ctx.SubscriptionGroupHandler.promises.previewAddSeatsSubscriptionChange =
        sinon.stub().rejects()
      ctx.req.body = { adding: 2 }

      await new Promise(resolve => {
        const res = {
          status: statusCode => {
            statusCode.should.equal(500)
            return {
              end: () => {
                resolve()
              },
            }
          },
        }

        ctx.Controller.previewAddSeatsSubscriptionChange(ctx.req, res)
      })
    })

    it('should fail previewing "add seats" change with SubtotalLimitExceededError', async function (ctx) {
      await new Promise(resolve => {
        ctx.req.body = { adding: 2 }
        ctx.SubscriptionGroupHandler.promises.previewAddSeatsSubscriptionChange =
          sinon.stub().throws(new ctx.Errors.SubtotalLimitExceededError())

        const res = {
          status: statusCode => {
            statusCode.should.equal(422)

            return {
              json: data => {
                data.should.deep.equal({
                  code: 'subtotal_limit_exceeded',
                  adding: ctx.req.body.adding,
                })
                resolve()
              },
            }
          },
        }

        ctx.Controller.previewAddSeatsSubscriptionChange(ctx.req, res)
      })
    })
  })

  describe('createAddSeatsSubscriptionChange', function () {
    it('should apply "add seats" change', async function (ctx) {
      await new Promise(resolve => {
        ctx.req.body = { adding: 2 }

        const res = {
          json: data => {
            ctx.SubscriptionGroupHandler.promises.createAddSeatsSubscriptionChange
              .calledWith(ctx.req.session.user._id, ctx.req.body.adding)
              .should.equal(true)
            data.should.deep.equal(ctx.createSubscriptionChangeData)
            resolve()
          },
        }

        ctx.Controller.createAddSeatsSubscriptionChange(ctx.req, res)
      })
    })

    it('should fail applying "add seats" change', async function (ctx) {
      await new Promise(resolve => {
        ctx.SubscriptionGroupHandler.promises.createAddSeatsSubscriptionChange =
          sinon.stub().rejects()

        const res = {
          status: statusCode => {
            statusCode.should.equal(500)

            return {
              end: () => {
                resolve()
              },
            }
          },
        }

        ctx.Controller.createAddSeatsSubscriptionChange(ctx.req, res)
      })
    })

    it('should fail applying "add seats" change with SubtotalLimitExceededError', async function (ctx) {
      await new Promise(resolve => {
        ctx.req.body = { adding: 2 }
        ctx.SubscriptionGroupHandler.promises.createAddSeatsSubscriptionChange =
          sinon.stub().throws(new ctx.Errors.SubtotalLimitExceededError())

        const res = {
          status: statusCode => {
            statusCode.should.equal(422)

            return {
              json: data => {
                data.should.deep.equal({
                  code: 'subtotal_limit_exceeded',
                  adding: ctx.req.body.adding,
                })
                resolve()
              },
            }
          },
        }

        ctx.Controller.createAddSeatsSubscriptionChange(ctx.req, res)
      })
    })

    it('should send 402 response with PaymentActionRequiredError', async function (ctx) {
      await new Promise(resolve => {
        const adding = 2
        ctx.req.body = { adding }
        const error = new ctx.Errors.PaymentActionRequiredError({
          clientSecret: 'secret',
          publicKey: 'key',
        })
        ctx.SubscriptionGroupHandler.promises.createAddSeatsSubscriptionChange =
          sinon.stub().throws(error)

        const res = {
          status: statusCode => {
            statusCode.should.equal(402)

            return {
              json: data => {
                data.should.deep.equal({
                  message: 'Payment action required',
                  clientSecret: error.info.clientSecret,
                  publicKey: error.info.publicKey,
                })
                resolve()
              },
            }
          },
        }

        ctx.Controller.createAddSeatsSubscriptionChange(ctx.req, res)
      })
    })

    it('should return 422 with MultiplePendingChangesError', async function (ctx) {
      await new Promise(resolve => {
        ctx.req.body = { adding: 2 }
        ctx.SubscriptionGroupHandler.promises.createAddSeatsSubscriptionChange =
          sinon.stub().throws(new ctx.Errors.MultiplePendingChangesError())

        const res = {
          status: statusCode => {
            statusCode.should.equal(422)

            return {
              end: () => {
                resolve()
              },
            }
          },
        }

        ctx.Controller.createAddSeatsSubscriptionChange(ctx.req, res)
      })
    })
  })

  describe('submitForm', function () {
    it('should build and pass the request body to the sales submit handler', async function (ctx) {
      await new Promise(resolve => {
        const adding = 100
        const poNumber = 'PO123456'
        ctx.req.body = { adding, poNumber }

        const res = {
          sendStatus: code => {
            ctx.SubscriptionGroupHandler.promises.updateSubscriptionPaymentTerms(
              ctx.adminUserId,
              ctx.recurlySubscription,
              poNumber
            )
            ctx.Modules.promises.hooks.fire
              .calledWith('sendSupportRequest', {
                email: ctx.user.email,
                subject: 'Sales Contact Form',
                message:
                  '\n' +
                  '**Overleaf Sales Contact Form:**\n' +
                  '\n' +
                  '**Subject:** Self-Serve Group User Increase Request\n' +
                  '\n' +
                  `**Estimated Number of Users:** ${adding}\n` +
                  '\n' +
                  `**Subscription:** [${ctx.subscriptionId}](https://admin.overleaf.com/admin/subscription/${ctx.subscriptionId})\n` +
                  '\n' +
                  `**Current Number of Seats:** ${ctx.subscription.membersLimit}\n` +
                  '\n' +
                  `**Plan Code:** ${ctx.subscription.planCode}\n` +
                  '\n' +
                  `**PO Number:** ${poNumber}\n` +
                  '\n' +
                  `**Message:** This email has been generated on behalf of user with email **${ctx.user.email}** to request an increase in the total number of users for their subscription.`,
                inbox: 'sales',
              })
              .should.equal(true)
            sinon.assert.calledOnce(ctx.Modules.promises.hooks.fire)
            code.should.equal(204)
            resolve()
          },
        }
        ctx.Controller.submitForm(ctx.req, res, resolve)
      })
    })

    it('should include the Salesforce ID line when the subscription has a salesforce_id', async function (ctx) {
      await new Promise(resolve => {
        const adding = 100
        const salesforceId = '0061x000ABCDEFG'
        ctx.subscription.salesforce_id = salesforceId
        ctx.req.body = { adding }

        const res = {
          sendStatus: () => {
            const { message } =
              ctx.Modules.promises.hooks.fire.getCall(0).args[1]
            message.should.include(
              `**Salesforce ID:** [${salesforceId}](https://digitalscience.lightning.force.com/lightning/r/Opportunity/${salesforceId}/view)\n`
            )
            resolve()
          },
        }
        ctx.Controller.submitForm(ctx.req, res, resolve)
      })
    })
  })

  describe('subscriptionUpgradePage', function () {
    it('should render "subscription upgrade" page', async function (ctx) {
      await new Promise(resolve => {
        const olSubscription = { membersLimit: 1, teamName: 'test team' }
        ctx.SubscriptionModel.Subscription.findOne = () => {
          return {
            exec: () => olSubscription,
          }
        }

        const res = {
          render: (page, data) => {
            ctx.SubscriptionGroupHandler.promises.getGroupPlanUpgradePreview
              .calledWith(ctx.req.session.user._id)
              .should.equal(true)
            page.should.equal('subscriptions/upgrade-group-subscription-react')
            data.totalLicenses.should.equal(olSubscription.membersLimit)
            data.groupName.should.equal(olSubscription.teamName)
            data.changePreview.should.equal(ctx.previewSubscriptionChangeData)
            resolve()
          },
        }

        ctx.Controller.subscriptionUpgradePage(ctx.req, res)
      })
    })

    it('should redirect if failed to generate preview', async function (ctx) {
      await new Promise(resolve => {
        ctx.SubscriptionGroupHandler.promises.getGroupPlanUpgradePreview = sinon
          .stub()
          .rejects()

        const res = {
          redirect: url => {
            url.should.equal('/user/subscription')
            resolve()
          },
        }

        ctx.Controller.subscriptionUpgradePage(ctx.req, res)
      })
    })

    it('should redirect to missing billing information page when billing information is missing', async function (ctx) {
      await new Promise(resolve => {
        ctx.SubscriptionGroupHandler.promises.getGroupPlanUpgradePreview = sinon
          .stub()
          .throws(new ctx.Errors.MissingBillingInfoError())

        const res = {
          redirect: url => {
            url.should.equal(
              '/user/subscription/group/missing-billing-information'
            )
            resolve()
          },
        }

        ctx.Controller.subscriptionUpgradePage(ctx.req, res)
      })
    })

    it('should redirect to manually collected subscription error page when collection method is manual', async function (ctx) {
      await new Promise(resolve => {
        ctx.SubscriptionGroupHandler.promises.getGroupPlanUpgradePreview = sinon
          .stub()
          .throws(new ctx.Errors.ManuallyCollectedError())

        const res = {
          redirect: url => {
            url.should.equal(
              '/user/subscription/group/manually-collected-subscription?error_type=plan-upgrade'
            )
            resolve()
          },
        }

        ctx.Controller.subscriptionUpgradePage(ctx.req, res)
      })
    })

    it('should redirect to subtotal limit exceeded page', async function (ctx) {
      await new Promise(resolve => {
        ctx.SubscriptionGroupHandler.promises.getGroupPlanUpgradePreview = sinon
          .stub()
          .throws(new ctx.Errors.SubtotalLimitExceededError())

        const res = {
          redirect: url => {
            url.should.equal('/user/subscription/group/subtotal-limit-exceeded')
            resolve()
          },
        }

        ctx.Controller.subscriptionUpgradePage(ctx.req, res)
      })
    })
  })

  describe('upgradeSubscription', function () {
    it('should send 200 response', async function (ctx) {
      await new Promise(resolve => {
        ctx.SubscriptionGroupHandler.promises.upgradeGroupPlan = sinon
          .stub()
          .resolves()

        const res = {
          sendStatus: code => {
            code.should.equal(200)
            resolve()
          },
        }

        ctx.Controller.upgradeSubscription(ctx.req, res)
      })
    })

    it('should send 500 response', async function (ctx) {
      await new Promise(resolve => {
        ctx.SubscriptionGroupHandler.promises.upgradeGroupPlan = sinon
          .stub()
          .rejects()

        const res = {
          sendStatus: code => {
            code.should.equal(500)
            resolve()
          },
        }

        ctx.Controller.upgradeSubscription(ctx.req, res)
      })
    })

    it('should send 402 response with PaymentActionRequiredError', async function (ctx) {
      await new Promise(resolve => {
        const error = new ctx.Errors.PaymentActionRequiredError({
          clientSecret: 'secret',
          publicKey: 'public',
        })
        ctx.SubscriptionGroupHandler.promises.upgradeGroupPlan = sinon
          .stub()
          .rejects(error)
        const res = {
          status: code => {
            code.should.equal(402)
            return {
              json: data => {
                data.should.deep.equal({
                  message: 'Payment action required',
                  clientSecret: error.info.clientSecret,
                  publicKey: error.info.publicKey,
                })
                resolve()
              },
            }
          },
        }

        ctx.Controller.upgradeSubscription(ctx.req, res)
      })
    })

    it('should send 422 response with MultiplePendingChangesError', async function (ctx) {
      await new Promise(resolve => {
        ctx.SubscriptionGroupHandler.promises.upgradeGroupPlan = sinon
          .stub()
          .rejects(new ctx.Errors.MultiplePendingChangesError())
        const res = {
          status: code => {
            code.should.equal(422)
            return {
              json: data => {
                data.should.deep.equal({
                  code: 'multiple_pending_changes',
                  message:
                    'Cannot upgrade subscription while there are multiple pending subscription changes. Please contact support.',
                })
                resolve()
              },
            }
          },
        }

        ctx.Controller.upgradeSubscription(ctx.req, res)
      })
    })
  })
})
