import { vi, expect } from 'vitest'
import sinon from 'sinon'
const modulePath =
  '../../../../app/src/Features/Subscription/LimitationsManager'

describe('LimitationsManager', function () {
  beforeEach(async function (ctx) {
    ctx.user = {
      _id: (ctx.userId = 'user-id'),
      features: { collaborators: 1 },
    }
    ctx.project = {
      _id: (ctx.projectId = 'project-id'),
      owner_ref: ctx.userId,
    }
    ctx.UserGetter = {
      promises: {
        getUser: sinon.stub().callsFake(async (userId, filter) => {
          if (userId === ctx.userId) {
            return ctx.user
          } else {
            return null
          }
        }),
      },
    }

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

    ctx.CollaboratorsGetter = {
      promises: {
        getInvitedEditCollaboratorCount: sinon.stub().resolves(0),
        getMemberIdPrivilegeLevel: sinon.stub(),
        getProjectOwnerId: sinon.stub().resolves(ctx.project.owner_ref),
      },
    }

    ctx.CollaboratorsInviteGetter = {
      promises: {
        getEditInviteCount: sinon.stub().resolves(0),
      },
    }

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

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

    vi.doMock('@overleaf/settings', () => ({
      default: (ctx.Settings = {}),
    }))

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

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

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

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

  describe('allowedNumberOfCollaboratorsInProject', function () {
    describe('when the project is owned by a user without a subscription', function () {
      beforeEach(function (ctx) {
        ctx.Settings.defaultFeatures = { collaborators: 23 }
        ctx.project.owner_ref = ctx.userId
        delete ctx.user.features
      })

      it('should return the default number', async function (ctx) {
        const result =
          await ctx.LimitationsManager.promises.allowedNumberOfCollaboratorsInProject(
            ctx.projectId
          )
        expect(result).to.equal(ctx.Settings.defaultFeatures.collaborators)
      })
    })

    describe('when the project is owned by a user with a subscription', function () {
      beforeEach(function (ctx) {
        ctx.project.owner_ref = ctx.userId
        ctx.user.features = { collaborators: 21 }
      })

      it('should return the number of collaborators the user is allowed', async function (ctx) {
        const result =
          await ctx.LimitationsManager.promises.allowedNumberOfCollaboratorsInProject(
            ctx.projectId
          )
        expect(result).to.equal(ctx.user.features.collaborators)
      })
    })
  })

  describe('allowedNumberOfCollaboratorsForUser', function () {
    describe('when the user has no features', function () {
      beforeEach(function (ctx) {
        ctx.Settings.defaultFeatures = { collaborators: 23 }
        delete ctx.user.features
      })

      it('should return the default number', async function (ctx) {
        const result =
          await ctx.LimitationsManager.promises.allowedNumberOfCollaboratorsForUser(
            ctx.userId
          )
        expect(result).to.equal(ctx.Settings.defaultFeatures.collaborators)
      })
    })

    describe('when the user has features', function () {
      beforeEach(async function (ctx) {
        ctx.user.features = { collaborators: 21 }
        ctx.result =
          await ctx.LimitationsManager.promises.allowedNumberOfCollaboratorsForUser(
            ctx.userId
          )
      })

      it('should return the number of collaborators the user is allowed', function (ctx) {
        expect(ctx.result).to.equal(ctx.user.features.collaborators)
      })
    })
  })

  describe('canAcceptEditCollaboratorInvite', function () {
    describe('when the project has fewer collaborators than allowed', function () {
      beforeEach(function (ctx) {
        ctx.current_number = 1
        ctx.user.features.collaborators = 2
        ctx.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount = sinon
          .stub()
          .resolves(ctx.current_number)
      })

      it('should return true', async function (ctx) {
        const result =
          await ctx.LimitationsManager.promises.canAcceptEditCollaboratorInvite(
            ctx.projectId
          )
        expect(result).to.be.true
      })
    })

    describe('when accepting the invite would exceed the collaborator limit', function () {
      beforeEach(function (ctx) {
        ctx.current_number = 2
        ctx.user.features.collaborators = 2
        ctx.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount = sinon
          .stub()
          .resolves(ctx.current_number)
      })

      it('should return false', async function (ctx) {
        const result =
          await ctx.LimitationsManager.promises.canAcceptEditCollaboratorInvite(
            ctx.projectId
          )
        expect(result).to.be.false
      })
    })

    describe('when the project has more collaborators than allowed', function () {
      beforeEach(function (ctx) {
        ctx.current_number = 3
        ctx.user.features.collaborators = 2
        ctx.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount = sinon
          .stub()
          .resolves(ctx.current_number)
      })

      it('should return false', async function (ctx) {
        const result =
          await ctx.LimitationsManager.promises.canAcceptEditCollaboratorInvite(
            ctx.projectId
          )
        expect(result).to.be.false
      })
    })

    describe('when the project has infinite collaborators', function () {
      beforeEach(function (ctx) {
        ctx.current_number = 100
        ctx.user.features.collaborators = -1
        ctx.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount = sinon
          .stub()
          .resolves(ctx.current_number)
      })

      it('should return true', async function (ctx) {
        const result =
          await ctx.LimitationsManager.promises.canAcceptEditCollaboratorInvite(
            ctx.projectId
          )
        expect(result).to.be.true
      })
    })
  })

  describe('canAddXEditCollaborators', function () {
    describe('when the project has fewer collaborators than allowed', function () {
      beforeEach(function (ctx) {
        ctx.current_number = 1
        ctx.user.features.collaborators = 2
        ctx.invite_count = 0
        ctx.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount = sinon
          .stub()
          .resolves(ctx.current_number)
        ctx.CollaboratorsInviteGetter.promises.getEditInviteCount = sinon
          .stub()
          .resolves(ctx.invite_count)
      })

      it('should return true', async function (ctx) {
        const result =
          await ctx.LimitationsManager.promises.canAddXEditCollaborators(
            ctx.projectId,
            1
          )
        expect(result).to.be.true
      })
    })

    describe('when the project has fewer collaborators and invites than allowed', function () {
      beforeEach(function (ctx) {
        ctx.current_number = 1
        ctx.user.features.collaborators = 4
        ctx.invite_count = 1
        ctx.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount = sinon
          .stub()
          .resolves(ctx.current_number)
        ctx.CollaboratorsInviteGetter.promises.getEditInviteCount = sinon
          .stub()
          .resolves(ctx.invite_count)
      })

      it('should return true', async function (ctx) {
        const result =
          await ctx.LimitationsManager.promises.canAddXEditCollaborators(
            ctx.projectId,
            1
          )
        expect(result).to.be.true
      })
    })

    describe('when the project has fewer collaborators than allowed but I want to add more than allowed', function () {
      beforeEach(function (ctx) {
        ctx.current_number = 1
        ctx.user.features.collaborators = 2
        ctx.invite_count = 0
        ctx.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount = sinon
          .stub()
          .resolves(ctx.current_number)
        ctx.CollaboratorsInviteGetter.promises.getEditInviteCount = sinon
          .stub()
          .resolves(ctx.invite_count)
      })

      it('should return false', async function (ctx) {
        const result =
          await ctx.LimitationsManager.promises.canAddXEditCollaborators(
            ctx.projectId,
            2
          )
        expect(result).to.be.false
      })
    })

    describe('when the project has more collaborators than allowed', function () {
      beforeEach(function (ctx) {
        ctx.current_number = 3
        ctx.user.features.collaborators = 2
        ctx.invite_count = 0
        ctx.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount = sinon
          .stub()
          .resolves(ctx.current_number)
        ctx.CollaboratorsInviteGetter.promises.getEditInviteCount = sinon
          .stub()
          .resolves(ctx.invite_count)
      })

      it('should return false', async function (ctx) {
        const result =
          await ctx.LimitationsManager.promises.canAddXEditCollaborators(
            ctx.projectId,
            1
          )
        expect(result).to.be.false
      })
    })

    describe('when the project has infinite collaborators', function () {
      beforeEach(function (ctx) {
        ctx.current_number = 100
        ctx.user.features.collaborators = -1
        ctx.invite_count = 0
        ctx.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount = sinon
          .stub()
          .resolves(ctx.current_number)
        ctx.CollaboratorsInviteGetter.promises.getEditInviteCount = sinon
          .stub()
          .resolves(ctx.invite_count)
      })

      it('should return true', async function (ctx) {
        const result =
          await ctx.LimitationsManager.promises.canAddXEditCollaborators(
            ctx.projectId,
            1
          )
        expect(result).to.be.true
      })
    })

    describe('when the project has more invites than allowed', function () {
      beforeEach(function (ctx) {
        ctx.current_number = 0
        ctx.user.features.collaborators = 2
        ctx.invite_count = 2
        ctx.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount = sinon
          .stub()
          .resolves(ctx.current_number)
        ctx.CollaboratorsInviteGetter.promises.getEditInviteCount = sinon
          .stub()
          .resolves(ctx.invite_count)
      })

      it('should return false', async function (ctx) {
        const result =
          await ctx.LimitationsManager.promises.canAddXEditCollaborators(
            ctx.projectId,
            1
          )
        expect(result).to.be.false
      })
    })

    describe('when the project has more invites and collaborators than allowed', function () {
      beforeEach(function (ctx) {
        ctx.current_number = 1
        ctx.user.features.collaborators = 2
        ctx.invite_count = 1
        ctx.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount = sinon
          .stub()
          .resolves(ctx.current_number)
        ctx.CollaboratorsInviteGetter.promises.getEditInviteCount = sinon
          .stub()
          .resolves(ctx.invite_count)
      })

      it('should return false', async function (ctx) {
        const result =
          await ctx.LimitationsManager.promises.canAddXEditCollaborators(
            ctx.projectId,
            1
          )
        expect(result).to.be.false
      })
    })
  })

  describe('canChangeCollaboratorPrivilegeLevel', function () {
    beforeEach(function (ctx) {
      ctx.collaboratorId = 'collaborator-id'
      ctx.CollaboratorsGetter.promises.getMemberIdPrivilegeLevel.resolves(
        'readOnly'
      )
    })

    describe("when the limit hasn't been reached", function () {
      it('accepts changing a viewer to an editor', async function (ctx) {
        const result =
          await ctx.LimitationsManager.promises.canChangeCollaboratorPrivilegeLevel(
            ctx.projectId,
            ctx.collaboratorId,
            'readAndWrite'
          )
        expect(result).to.be.true
      })
    })

    describe('when the limit has been reached', function () {
      beforeEach(function (ctx) {
        ctx.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount.resolves(
          1
        )
      })

      it('accepts changing a reviewer to an editor', async function (ctx) {
        ctx.CollaboratorsGetter.promises.getMemberIdPrivilegeLevel.resolves(
          'review'
        )
        const result =
          await ctx.LimitationsManager.promises.canChangeCollaboratorPrivilegeLevel(
            ctx.projectId,
            ctx.collaboratorId,
            'readAndWrite'
          )
        expect(result).to.be.true
      })

      it('rejects changing a viewer to a reviewer', async function (ctx) {
        const result =
          await ctx.LimitationsManager.promises.canChangeCollaboratorPrivilegeLevel(
            ctx.projectId,
            ctx.collaboratorId,
            'review'
          )
        expect(result).to.be.false
      })
    })
  })

  describe('userHasSubscription', function () {
    beforeEach(function (ctx) {
      ctx.SubscriptionLocator.promises.getUsersSubscription = sinon
        .stub()
        .resolves()
    })

    it('should return true if the recurly token is set', async function (ctx) {
      ctx.SubscriptionLocator.promises.getUsersSubscription = sinon
        .stub()
        .resolves({
          recurlySubscription_id: '1234',
        })
      const { hasSubscription } =
        await ctx.LimitationsManager.promises.userHasSubscription(ctx.user)
      expect(hasSubscription).to.be.true
    })

    it('should return true if the paymentProvider field is set', async function (ctx) {
      ctx.SubscriptionLocator.promises.getUsersSubscription = sinon
        .stub()
        .resolves({
          paymentProvider: {
            subscriptionId: '1234',
          },
        })
      const { hasSubscription } =
        await ctx.LimitationsManager.promises.userHasSubscription(ctx.user)
      expect(hasSubscription).to.be.true
    })

    it('should return false if the recurly token is not set', async function (ctx) {
      ctx.SubscriptionLocator.promises.getUsersSubscription.resolves({})
      const { hasSubscription } =
        await ctx.LimitationsManager.promises.userHasSubscription(ctx.user)
      expect(hasSubscription).to.be.false
    })

    it('should return false if the subscription is undefined', async function (ctx) {
      ctx.SubscriptionLocator.promises.getUsersSubscription.resolves()
      const { hasSubscription } =
        await ctx.LimitationsManager.promises.userHasSubscription(ctx.user)
      expect(hasSubscription).to.be.false
    })

    it('should return the subscription', async function (ctx) {
      const stubbedSubscription = { freeTrial: {}, token: '' }
      ctx.SubscriptionLocator.promises.getUsersSubscription.resolves(
        stubbedSubscription
      )
      const { subscription } =
        await ctx.LimitationsManager.promises.userHasSubscription(ctx.user)
      expect(subscription).to.deep.equal(stubbedSubscription)
    })

    describe('when user has a custom account', function () {
      beforeEach(function (ctx) {
        ctx.fakeSubscription = { customAccount: true }
        ctx.SubscriptionLocator.promises.getUsersSubscription.resolves(
          ctx.fakeSubscription
        )
      })

      it('should return true', async function (ctx) {
        const { hasSubscription } =
          await ctx.LimitationsManager.promises.userHasSubscription(ctx.user)
        expect(hasSubscription).to.be.true
      })

      it('should return the subscription', async function (ctx) {
        const { subscription } =
          await ctx.LimitationsManager.promises.userHasSubscription(ctx.user)
        expect(subscription).to.deep.equal(ctx.fakeSubscription)
      })
    })
  })

  describe('userIsMemberOfGroupSubscription', function () {
    beforeEach(function (ctx) {
      ctx.SubscriptionLocator.promises.getMemberSubscriptions = sinon
        .stub()
        .resolves()
    })

    it('should return false if there are no groups subcriptions', async function (ctx) {
      ctx.SubscriptionLocator.promises.getMemberSubscriptions.resolves([])
      const { isMember } =
        await ctx.LimitationsManager.promises.userIsMemberOfGroupSubscription(
          ctx.user
        )
      expect(isMember).to.be.false
    })

    it('should return true if there are no groups subcriptions', async function (ctx) {
      const subscriptions = ['mock-subscription']
      ctx.SubscriptionLocator.promises.getMemberSubscriptions.resolves(
        subscriptions
      )
      const { isMember, subscriptions: retSubscriptions } =
        await ctx.LimitationsManager.promises.userIsMemberOfGroupSubscription(
          ctx.user
        )
      expect(isMember).to.be.true
      expect(retSubscriptions).to.deep.equal(subscriptions)
    })
  })

  describe('hasPaidSubscription', function () {
    beforeEach(function (ctx) {
      ctx.SubscriptionLocator.promises.getMemberSubscriptions = sinon
        .stub()
        .resolves([])
      ctx.SubscriptionLocator.promises.getUsersSubscription = sinon
        .stub()
        .resolves(null)
    })

    it('should return true if userIsMemberOfGroupSubscription', async function (ctx) {
      ctx.SubscriptionLocator.promises.getMemberSubscriptions = sinon
        .stub()
        .resolves([{ _id: '123' }])
      const { hasPaidSubscription } =
        await ctx.LimitationsManager.promises.hasPaidSubscription(ctx.user)
      expect(hasPaidSubscription).to.be.true
    })

    it('should return true if userHasSubscription', async function (ctx) {
      ctx.SubscriptionLocator.promises.getUsersSubscription = sinon
        .stub()
        .resolves({ recurlySubscription_id: '123' })
      const { hasPaidSubscription } =
        await ctx.LimitationsManager.promises.hasPaidSubscription(ctx.user)
      expect(hasPaidSubscription).to.be.true
    })

    it('should return false if none are true', async function (ctx) {
      const { hasPaidSubscription } =
        await ctx.LimitationsManager.promises.hasPaidSubscription(ctx.user)
      expect(hasPaidSubscription).to.be.false
    })

    it('should have userHasSubscriptionOrIsGroupMember alias', async function (ctx) {
      const { hasPaidSubscription } =
        await ctx.LimitationsManager.promises.userHasSubscriptionOrIsGroupMember(
          ctx.user
        )
      expect(hasPaidSubscription).to.be.false
    })
  })

  describe('hasGroupMembersLimitReached', function () {
    beforeEach(function (ctx) {
      ctx.subscriptionId = '12312'
      ctx.subscription = {
        membersLimit: 3,
        member_ids: ['', ''],
        teamInvites: [
          { email: 'bob@example.com', sentAt: new Date(), token: 'hey' },
        ],
      }
    })

    it('should return true if the limit is hit (including members and invites)', async function (ctx) {
      ctx.SubscriptionLocator.promises.getSubscription.resolves(
        ctx.subscription
      )
      const { limitReached } =
        await ctx.LimitationsManager.promises.hasGroupMembersLimitReached(
          ctx.subscriptionId
        )
      expect(limitReached).to.be.true
    })

    it('should return false if the limit is not hit (including members and invites)', async function (ctx) {
      ctx.subscription.membersLimit = 4
      ctx.SubscriptionLocator.promises.getSubscription.resolves(
        ctx.subscription
      )
      const { limitReached } =
        await ctx.LimitationsManager.promises.hasGroupMembersLimitReached(
          ctx.subscriptionId
        )
      expect(limitReached).to.be.false
    })

    it('should return true if the limit has been exceded (including members and invites)', async function (ctx) {
      ctx.subscription.membersLimit = 2
      ctx.SubscriptionLocator.promises.getSubscription.resolves(
        ctx.subscription
      )
      const { limitReached } =
        await ctx.LimitationsManager.promises.hasGroupMembersLimitReached(
          ctx.subscriptionId
        )
      expect(limitReached).to.be.true
    })
  })
})
