const SandboxedModule = require('sandboxed-module')
const sinon = require('sinon')
const modulePath =
  '../../../../app/src/Features/Subscription/SubscriptionUpdater'
const { assert, expect } = require('chai')
const { ObjectId } = require('mongodb')

describe('SubscriptionUpdater', function () {
  beforeEach(function () {
    this.recurlySubscription = {
      uuid: '1238uoijdasjhd',
      plan: {
        plan_code: 'kjhsakjds'
      }
    }
    this.adminUser = { _id: (this.adminuser_id = '5208dd34438843e2db000007') }
    this.otherUserId = '5208dd34438842e2db000005'
    this.allUserIds = ['13213', 'dsadas', 'djsaiud89']
    this.subscription = {
      _id: '111111111111111111111111',
      admin_id: this.adminUser._id,
      manager_ids: [this.adminUser._id],
      member_ids: [],
      save: sinon.stub().callsArgWith(0),
      planCode: 'student_or_something'
    }
    this.user_id = this.adminuser_id

    this.groupSubscription = {
      _id: '222222222222222222222222',
      admin_id: this.adminUser._id,
      manager_ids: [this.adminUser._id],
      member_ids: this.allUserIds,
      save: sinon.stub().callsArgWith(0),
      planCode: 'group_subscription'
    }

    this.updateStub = sinon.stub().callsArgWith(2, null)
    this.updateManyStub = sinon.stub().callsArgWith(2, null)
    this.findOneAndUpdateStub = sinon
      .stub()
      .callsArgWith(2, null, this.subscription)

    let subscription = this.subscription
    this.SubscriptionModel = class {
      constructor(opts) {
        // Always return our mock subscription when creating a new one
        subscription.admin_id = opts.admin_id
        subscription.manager_ids = [opts.admin_id]
        return subscription
      }
    }
    this.SubscriptionModel.deleteOne = sinon.stub().yields()
    this.SubscriptionModel.updateOne = this.updateStub
    this.SubscriptionModel.updateMany = this.updateManyStub
    this.SubscriptionModel.findOneAndUpdate = this.findOneAndUpdateStub

    this.SubscriptionLocator = {
      getUsersSubscription: sinon.stub(),
      getGroupSubscriptionMemberOf: sinon.stub(),
      getMemberSubscriptions: sinon.stub().yields(null, [])
    }

    this.Settings = {
      defaultPlanCode: 'personal',
      defaultFeatures: { default: 'features' }
    }

    this.UserFeaturesUpdater = { updateFeatures: sinon.stub().yields() }

    this.PlansLocator = { findLocalPlanInSettings: sinon.stub().returns({}) }

    this.UserGetter = {
      getUsers(memberIds, projection, callback) {
        const users = memberIds.map(id => ({ _id: id }))
        callback(null, users)
      },
      getUser: sinon.stub()
    }

    this.ReferalFeatures = { getBonusFeatures: sinon.stub().callsArgWith(1) }
    this.Modules = { hooks: { fire: sinon.stub().callsArgWith(2, null, null) } }
    this.FeaturesUpdater = {
      refreshFeatures: sinon.stub().yields()
    }
    this.DeletedSubscription = {
      findOneAndUpdate: sinon.stub().yields()
    }
    this.SubscriptionUpdater = SandboxedModule.require(modulePath, {
      requires: {
        mongodb: { ObjectId },
        '../../models/Subscription': {
          Subscription: this.SubscriptionModel
        },
        './UserFeaturesUpdater': this.UserFeaturesUpdater,
        './SubscriptionLocator': this.SubscriptionLocator,
        '../User/UserGetter': this.UserGetter,
        './PlansLocator': this.PlansLocator,
        'settings-sharelatex': this.Settings,
        '../../infrastructure/mongodb': { db: {}, ObjectId },
        './FeaturesUpdater': this.FeaturesUpdater,
        '../../models/DeletedSubscription': {
          DeletedSubscription: this.DeletedSubscription
        }
      }
    })
  })

  describe('updateAdmin', function () {
    it('should update the subscription admin', function (done) {
      this.subscription.groupPlan = true
      this.SubscriptionUpdater.updateAdmin(
        this.subscription,
        this.otherUserId,
        err => {
          if (err != null) {
            return done(err)
          }
          const query = {
            _id: ObjectId(this.subscription._id),
            customAccount: true
          }
          const update = {
            $set: { admin_id: ObjectId(this.otherUserId) },
            $addToSet: { manager_ids: ObjectId(this.otherUserId) }
          }
          this.SubscriptionModel.updateOne.should.have.been.calledOnce
          this.SubscriptionModel.updateOne.should.have.been.calledWith(
            query,
            update
          )
          done()
        }
      )
    })

    it('should remove the manager for non-group subscriptions', function (done) {
      this.SubscriptionUpdater.updateAdmin(
        this.subscription,
        this.otherUserId,
        err => {
          if (err != null) {
            return done(err)
          }
          const query = {
            _id: ObjectId(this.subscription._id),
            customAccount: true
          }
          const update = {
            $set: {
              admin_id: ObjectId(this.otherUserId),
              manager_ids: [ObjectId(this.otherUserId)]
            }
          }
          this.SubscriptionModel.updateOne.should.have.been.calledOnce
          this.SubscriptionModel.updateOne.should.have.been.calledWith(
            query,
            update
          )
          done()
        }
      )
    })
  })

  describe('syncSubscription', function () {
    beforeEach(function () {
      this.SubscriptionLocator.getUsersSubscription.callsArgWith(
        1,
        null,
        this.subscription
      )
      this.SubscriptionUpdater._updateSubscriptionFromRecurly = sinon
        .stub()
        .yields()
    })

    it('should update the subscription if the user already is admin of one', function (done) {
      this.SubscriptionUpdater._createNewSubscription = sinon.stub()

      this.SubscriptionUpdater.syncSubscription(
        this.recurlySubscription,
        this.adminUser._id,
        err => {
          if (err != null) {
            return done(err)
          }
          this.SubscriptionLocator.getUsersSubscription
            .calledWith(this.adminUser._id)
            .should.equal(true)
          this.SubscriptionUpdater._updateSubscriptionFromRecurly.called.should.equal(
            true
          )
          this.SubscriptionUpdater._updateSubscriptionFromRecurly
            .calledWith(this.recurlySubscription, this.subscription)
            .should.equal(true)
          done()
        }
      )
    })

    it('should not call updateFeatures with group subscription if recurly subscription is not expired', function (done) {
      this.SubscriptionUpdater.syncSubscription(
        this.recurlySubscription,
        this.adminUser._id,
        err => {
          if (err != null) {
            return done(err)
          }
          this.SubscriptionLocator.getUsersSubscription
            .calledWith(this.adminUser._id)
            .should.equal(true)
          this.SubscriptionUpdater._updateSubscriptionFromRecurly.called.should.equal(
            true
          )
          this.SubscriptionUpdater._updateSubscriptionFromRecurly
            .calledWith(this.recurlySubscription, this.subscription)
            .should.equal(true)
          this.UserFeaturesUpdater.updateFeatures.called.should.equal(false)
          done()
        }
      )
    })
  })

  describe('_updateSubscriptionFromRecurly', function () {
    beforeEach(function () {
      this.FeaturesUpdater.refreshFeatures = sinon.stub().callsArgWith(1)
      this.SubscriptionUpdater.deleteSubscription = sinon.stub().yields()
    })

    afterEach(function () {
      this.subscription.member_ids = []
    })

    it('should update the subscription with token etc when not expired', function (done) {
      this.SubscriptionUpdater._updateSubscriptionFromRecurly(
        this.recurlySubscription,
        this.subscription,
        {},
        err => {
          if (err != null) {
            return done(err)
          }
          this.subscription.recurlySubscription_id.should.equal(
            this.recurlySubscription.uuid
          )
          this.subscription.planCode.should.equal(
            this.recurlySubscription.plan.plan_code
          )
          this.subscription.save.called.should.equal(true)
          this.FeaturesUpdater.refreshFeatures
            .calledWith(this.adminUser._id)
            .should.equal(true)
          done()
        }
      )
    })

    it('should remove the subscription when expired', function (done) {
      this.recurlySubscription.state = 'expired'
      this.SubscriptionUpdater._updateSubscriptionFromRecurly(
        this.recurlySubscription,
        this.subscription,
        {},
        err => {
          if (err != null) {
            return done(err)
          }
          this.SubscriptionUpdater.deleteSubscription
            .calledWithMatch(this.subscription)
            .should.equal(true)
          done()
        }
      )
    })

    it('should update all the users features', function (done) {
      this.subscription.member_ids = this.allUserIds
      this.SubscriptionUpdater._updateSubscriptionFromRecurly(
        this.recurlySubscription,
        this.subscription,
        {},
        err => {
          if (err != null) {
            return done(err)
          }
          this.FeaturesUpdater.refreshFeatures
            .calledWith(this.adminUser._id)
            .should.equal(true)
          this.FeaturesUpdater.refreshFeatures
            .calledWith(this.allUserIds[0])
            .should.equal(true)
          this.FeaturesUpdater.refreshFeatures
            .calledWith(this.allUserIds[1])
            .should.equal(true)
          this.FeaturesUpdater.refreshFeatures
            .calledWith(this.allUserIds[2])
            .should.equal(true)
          done()
        }
      )
    })

    it('should set group to true and save how many members can be added to group', function (done) {
      this.PlansLocator.findLocalPlanInSettings
        .withArgs(this.recurlySubscription.plan.plan_code)
        .returns({ groupPlan: true, membersLimit: 5 })
      this.SubscriptionUpdater._updateSubscriptionFromRecurly(
        this.recurlySubscription,
        this.subscription,
        {},
        err => {
          if (err != null) {
            return done(err)
          }
          this.subscription.membersLimit.should.equal(5)
          this.subscription.groupPlan.should.equal(true)
          this.subscription.member_ids.should.deep.equal([
            this.subscription.admin_id
          ])
          done()
        }
      )
    })

    it('should not set group to true or set groupPlan', function (done) {
      this.SubscriptionUpdater._updateSubscriptionFromRecurly(
        this.recurlySubscription,
        this.subscription,
        {},
        err => {
          if (err != null) {
            return done(err)
          }
          assert.notEqual(this.subscription.membersLimit, 5)
          assert.notEqual(this.subscription.groupPlan, true)
          done()
        }
      )
    })

    describe('when the plan allows adding more seats', function () {
      beforeEach(function () {
        this.membersLimitAddOn = 'add_on1'
        this.PlansLocator.findLocalPlanInSettings
          .withArgs(this.recurlySubscription.plan.plan_code)
          .returns({
            groupPlan: true,
            membersLimit: 5,
            membersLimitAddOn: this.membersLimitAddOn
          })
      })

      function expectMembersLimit(limit) {
        it('should set the membersLimit accordingly', function (done) {
          this.SubscriptionUpdater._updateSubscriptionFromRecurly(
            this.recurlySubscription,
            this.subscription,
            {},
            error => {
              if (error) return done(error)

              expect(this.subscription.membersLimit).to.equal(limit)
              done()
            }
          )
        })
      }

      describe('when the recurlySubscription does not have add ons', function () {
        beforeEach(function () {
          delete this.recurlySubscription.subscription_add_ons
        })
        expectMembersLimit(5)
      })

      describe('when the recurlySubscription has non-matching add ons', function () {
        beforeEach(function () {
          this.recurlySubscription.subscription_add_ons = [
            { add_on_code: 'add_on_99', quantity: 3 }
          ]
        })
        expectMembersLimit(5)
      })

      describe('when the recurlySubscription has a matching add on', function () {
        beforeEach(function () {
          this.recurlySubscription.subscription_add_ons = [
            { add_on_code: this.membersLimitAddOn, quantity: 10 }
          ]
        })
        expectMembersLimit(15)
      })

      // NOTE: This is unexpected, but we are going to support it anyways.
      describe('when the recurlySubscription has multiple matching add ons', function () {
        beforeEach(function () {
          this.recurlySubscription.subscription_add_ons = [
            { add_on_code: this.membersLimitAddOn, quantity: 10 },
            { add_on_code: this.membersLimitAddOn, quantity: 3 }
          ]
        })
        expectMembersLimit(18)
      })
    })
  })

  describe('_createNewSubscription', function () {
    it('should create a new subscription then update the subscription', function (done) {
      this.SubscriptionUpdater._createNewSubscription(
        this.adminUser._id,
        () => {
          this.subscription.admin_id.should.equal(this.adminUser._id)
          this.subscription.manager_ids.should.deep.equal([this.adminUser._id])
          this.subscription.save.called.should.equal(true)
          done()
        }
      )
    })
  })

  describe('addUserToGroup', function () {
    beforeEach(function () {
      this.SubscriptionUpdater.addUsersToGroup = sinon.stub().yields(null)
    })

    it('delegates to addUsersToGroup', function (done) {
      this.SubscriptionUpdater.addUserToGroup(
        this.subscription._id,
        this.otherUserId,
        () => {
          this.SubscriptionUpdater.addUsersToGroup
            .calledWith(this.subscription._id, [this.otherUserId])
            .should.equal(true)
          done()
        }
      )
    })
  })

  describe('addUsersToGroup', function () {
    beforeEach(function () {
      this.FeaturesUpdater.refreshFeatures = sinon.stub().callsArgWith(1)
    })

    it('should add the user ids to the group as a set', function (done) {
      this.SubscriptionUpdater.addUsersToGroup(
        this.subscription._id,
        [this.otherUserId],
        () => {
          const searchOps = { _id: this.subscription._id }
          const insertOperation = {
            $addToSet: { member_ids: { $each: [this.otherUserId] } }
          }
          this.updateStub
            .calledWith(searchOps, insertOperation)
            .should.equal(true)
          done()
        }
      )
    })

    it('should update the users features', function (done) {
      this.SubscriptionUpdater.addUserToGroup(
        this.subscription._id,
        this.otherUserId,
        () => {
          this.FeaturesUpdater.refreshFeatures
            .calledWith(this.otherUserId)
            .should.equal(true)
          done()
        }
      )
    })
  })

  describe('removeUserFromGroups', function () {
    beforeEach(function () {
      this.FeaturesUpdater.refreshFeatures = sinon.stub().callsArgWith(1)
      this.UserGetter.getUser.yields(null, {})
      this.fakeSubscriptions = [{ _id: 'fake-id-1' }, { _id: 'fake-id-2' }]
      this.SubscriptionLocator.getMemberSubscriptions.yields(
        null,
        this.fakeSubscriptions
      )
    })

    it('should pull the users id from the group', function (done) {
      this.SubscriptionUpdater.removeUserFromGroup(
        this.subscription._id,
        this.otherUserId,
        () => {
          const searchOps = { _id: this.subscription._id }
          const removeOperation = { $pull: { member_ids: this.otherUserId } }
          this.updateManyStub
            .calledWith(searchOps, removeOperation)
            .should.equal(true)
          done()
        }
      )
    })

    it('should pull the users id from all groups', function (done) {
      this.SubscriptionUpdater.removeUserFromAllGroups(this.otherUserId, () => {
        const filter = { _id: ['fake-id-1', 'fake-id-2'] }
        const removeOperation = { $pull: { member_ids: this.otherUserId } }
        sinon.assert.calledWith(this.updateManyStub, filter, removeOperation)
        done()
      })
    })

    it('should update the users features', function (done) {
      this.SubscriptionUpdater.removeUserFromGroup(
        this.subscription._id,
        this.otherUserId,
        () => {
          this.FeaturesUpdater.refreshFeatures
            .calledWith(this.otherUserId)
            .should.equal(true)
          done()
        }
      )
    })
  })

  describe('deleteSubscription', function () {
    beforeEach(function (done) {
      this.subscription = {
        _id: ObjectId().toString(),
        mock: 'subscription',
        admin_id: ObjectId(),
        member_ids: [ObjectId(), ObjectId(), ObjectId()]
      }
      this.SubscriptionLocator.getSubscription = sinon
        .stub()
        .yields(null, this.subscription)
      this.FeaturesUpdater.refreshFeatures = sinon.stub().yields()
      this.SubscriptionUpdater.deleteSubscription(this.subscription, {}, done)
    })

    it('should remove the subscription', function () {
      this.SubscriptionModel.deleteOne
        .calledWith({ _id: this.subscription._id })
        .should.equal(true)
    })

    it('should downgrade the admin_id', function () {
      this.FeaturesUpdater.refreshFeatures
        .calledWith(this.subscription.admin_id)
        .should.equal(true)
    })

    it('should downgrade all of the members', function () {
      for (const userId of this.subscription.member_ids) {
        this.FeaturesUpdater.refreshFeatures
          .calledWith(userId)
          .should.equal(true)
      }
    })
  })
})
