const { ObjectId } = require('mongodb')
const SandboxedModule = require('sandboxed-module')
const assert = require('assert')
const moment = require('moment')
const path = require('path')
const sinon = require('sinon')
const modulePath = path.join(
  __dirname,
  '../../../../app/src/Features/User/UserGetter'
)
const { expect } = require('chai')
const Errors = require('../../../../app/src/Features/Errors/Errors')
const {
  normalizeQuery,
  normalizeMultiQuery,
} = require('../../../../app/src/Features/Helpers/Mongo')

describe('UserGetter', function () {
  beforeEach(function () {
    this.fakeUser = {
      _id: '12390i',
      email: 'email2@foo.bar',
      emails: [
        {
          email: 'email1@foo.bar',
          reversedHostname: 'rab.oof',
          confirmedAt: new Date(),
        },
        { email: 'email2@foo.bar', reversedHostname: 'rab.oof' },
      ],
    }
    this.findOne = sinon.stub().callsArgWith(2, null, this.fakeUser)
    this.findToArrayStub = sinon.stub().yields(null, [this.fakeUser])
    this.find = sinon.stub().returns({ toArray: this.findToArrayStub })
    this.Mongo = {
      db: {
        users: {
          findOne: this.findOne,
          find: this.find,
        },
      },
      ObjectId,
    }
    this.getUserAffiliations = sinon.stub().resolves([])

    this.UserGetter = SandboxedModule.require(modulePath, {
      requires: {
        '../Helpers/Mongo': { normalizeQuery, normalizeMultiQuery },
        '../../infrastructure/mongodb': this.Mongo,
        '@overleaf/metrics': {
          timeAsyncMethod: sinon.stub(),
        },
        '@overleaf/settings': (this.settings = {
          reconfirmNotificationDays: 14,
        }),
        '../Institutions/InstitutionsAPI': {
          promises: {
            getUserAffiliations: this.getUserAffiliations,
          },
        },
        '../../infrastructure/Features': {
          hasFeature: sinon.stub().returns(true),
        },
        '../../models/User': {
          User: (this.User = {}),
        },
      },
    })
  })

  describe('getSsoUsersAtInstitution', function () {
    it('should throw an error when no projection is passed', function (done) {
      this.UserGetter.getSsoUsersAtInstitution(1, undefined, error => {
        expect(error).to.exist
        expect(error.message).to.equal('missing projection')
        done()
      })
    })
  })

  describe('getUser', function () {
    it('should get user', function (done) {
      const query = { _id: '000000000000000000000000' }
      const projection = { email: 1 }
      this.UserGetter.getUser(query, projection, (error, user) => {
        expect(error).to.not.exist
        this.findOne.called.should.equal(true)
        this.findOne.calledWith(query, { projection }).should.equal(true)
        user.should.deep.equal(this.fakeUser)
        done()
      })
    })

    it('should not allow null query', function (done) {
      this.UserGetter.getUser(null, {}, error => {
        error.should.exist
        error.message.should.equal('no query provided')
        done()
      })
    })
  })

  describe('getUsers', function () {
    it('should get users with array of userIds', function (done) {
      const query = [new ObjectId()]
      const projection = { email: 1 }
      this.UserGetter.getUsers(query, projection, (error, users) => {
        expect(error).to.not.exist
        this.find.should.have.been.calledWithMatch(
          { _id: { $in: query } },
          { projection }
        )
        users.should.deep.equal([this.fakeUser])
        done()
      })
    })

    it('should not allow null query', function (done) {
      this.UserGetter.getUser(null, {}, error => {
        error.should.exist
        error.message.should.equal('no query provided')
        done()
      })
    })
  })

  describe('getUserFullEmails', function () {
    it('should get user', function (done) {
      this.UserGetter.promises.getUser = sinon.stub().resolves(this.fakeUser)
      const projection = { email: 1, emails: 1, samlIdentifiers: 1 }
      this.UserGetter.getUserFullEmails(
        this.fakeUser._id,
        (error, fullEmails) => {
          expect(error).to.not.exist
          this.UserGetter.promises.getUser.called.should.equal(true)
          this.UserGetter.promises.getUser
            .calledWith(this.fakeUser._id, projection)
            .should.equal(true)
          done()
        }
      )
    })

    it('should fetch emails data', function (done) {
      this.UserGetter.promises.getUser = sinon.stub().resolves(this.fakeUser)
      this.UserGetter.getUserFullEmails(
        this.fakeUser._id,
        (error, fullEmails) => {
          expect(error).to.not.exist
          assert.deepEqual(fullEmails, [
            {
              email: 'email1@foo.bar',
              reversedHostname: 'rab.oof',
              confirmedAt: this.fakeUser.emails[0].confirmedAt,
              emailHasInstitutionLicence: false,
              default: false,
            },
            {
              email: 'email2@foo.bar',
              reversedHostname: 'rab.oof',
              emailHasInstitutionLicence: false,
              default: true,
            },
          ])
          done()
        }
      )
    })

    it('should merge affiliation data', function (done) {
      this.UserGetter.promises.getUser = sinon.stub().resolves(this.fakeUser)
      const affiliationsData = [
        {
          email: 'email1@foo.bar',
          role: 'Prof',
          department: 'Maths',
          inferred: false,
          licence: 'pro_plus',
          institution: {
            name: 'University Name',
            isUniversity: true,
            confirmed: true,
          },
          portal: undefined,
        },
      ]
      this.getUserAffiliations.resolves(affiliationsData)
      this.UserGetter.getUserFullEmails(
        this.fakeUser._id,
        (error, fullEmails) => {
          expect(error).to.not.exist
          assert.deepEqual(fullEmails, [
            {
              email: 'email1@foo.bar',
              reversedHostname: 'rab.oof',
              confirmedAt: this.fakeUser.emails[0].confirmedAt,
              default: false,
              emailHasInstitutionLicence: true,
              affiliation: {
                institution: affiliationsData[0].institution,
                inferred: affiliationsData[0].inferred,
                department: affiliationsData[0].department,
                role: affiliationsData[0].role,
                lastDayToReconfirm: undefined,
                licence: affiliationsData[0].licence,
                inReconfirmNotificationPeriod: false,
                pastReconfirmDate: false,
                portal: undefined,
              },
            },
            {
              email: 'email2@foo.bar',
              reversedHostname: 'rab.oof',
              emailHasInstitutionLicence: false,
              default: true,
            },
          ])
          done()
        }
      )
    })

    it('should merge SAML identifier', function (done) {
      const fakeSamlIdentifiers = [
        { providerId: 'saml_id', exteranlUserId: 'whatever' },
      ]
      const fakeUserWithSaml = this.fakeUser
      fakeUserWithSaml.emails[0].samlProviderId = 'saml_id'
      fakeUserWithSaml.samlIdentifiers = fakeSamlIdentifiers
      this.UserGetter.promises.getUser = sinon.stub().resolves(this.fakeUser)
      this.getUserAffiliations.resolves([])
      this.UserGetter.getUserFullEmails(
        this.fakeUser._id,
        (error, fullEmails) => {
          expect(error).to.not.exist
          assert.deepEqual(fullEmails, [
            {
              email: 'email1@foo.bar',
              reversedHostname: 'rab.oof',
              confirmedAt: this.fakeUser.emails[0].confirmedAt,
              default: false,
              emailHasInstitutionLicence: false,
              samlProviderId: 'saml_id',
              samlIdentifier: fakeSamlIdentifiers[0],
            },
            {
              email: 'email2@foo.bar',
              reversedHostname: 'rab.oof',
              emailHasInstitutionLicence: false,
              default: true,
            },
          ])
          done()
        }
      )
    })

    it('should get user when it has no emails field', function (done) {
      this.fakeUser = {
        _id: '12390i',
        email: 'email2@foo.bar',
      }
      this.UserGetter.promises.getUser = sinon.stub().resolves(this.fakeUser)
      const projection = { email: 1, emails: 1, samlIdentifiers: 1 }
      this.UserGetter.getUserFullEmails(
        this.fakeUser._id,
        (error, fullEmails) => {
          expect(error).to.not.exist
          this.UserGetter.promises.getUser.called.should.equal(true)
          this.UserGetter.promises.getUser
            .calledWith(this.fakeUser._id, projection)
            .should.equal(true)
          assert.deepEqual(fullEmails, [])
          done()
        }
      )
    })

    describe('affiliation reconfirmation', function () {
      const institutionNonSSO = {
        id: 1,
        name: 'University Name',
        commonsAccount: true,
        isUniversity: true,
        confirmed: true,
        ssoBeta: false,
        ssoEnabled: false,
        maxConfirmationMonths: 12,
      }
      const institutionSSO = {
        id: 2,
        name: 'SSO University Name',
        isUniversity: true,
        confirmed: true,
        ssoBeta: false,
        ssoEnabled: true,
        maxConfirmationMonths: 12,
      }
      describe('non-SSO institutions', function () {
        const email1 = 'leonard@example-affiliation.com'
        const email2 = 'mccoy@example-affiliation.com'
        const affiliationsData = [
          {
            email: email1,
            role: 'Prof',
            department: 'Medicine',
            inferred: false,
            licence: 'pro_plus',
            institution: institutionNonSSO,
          },
          {
            email: email2,
            role: 'Prof',
            department: 'Medicine',
            inferred: false,
            licence: 'pro_plus',
            institution: institutionNonSSO,
          },
        ]
        it('should flag inReconfirmNotificationPeriod for all affiliations in period', function (done) {
          const user = {
            _id: '12390i',
            email: email1,
            emails: [
              {
                email: email1,
                reversedHostname: 'moc.noitailiffa-elpmaxe',
                confirmedAt: moment()
                  .subtract(
                    institutionNonSSO.maxConfirmationMonths + 2,
                    'months'
                  )
                  .toDate(),
                default: true,
              },
              {
                email: email2,
                reversedHostname: 'moc.noitailiffa-elpmaxe',
                confirmedAt: moment()
                  .subtract(
                    institutionNonSSO.maxConfirmationMonths + 1,
                    'months'
                  )
                  .toDate(),
              },
            ],
          }
          this.getUserAffiliations.resolves(affiliationsData)
          this.UserGetter.promises.getUser = sinon.stub().resolves(user)
          this.UserGetter.getUserFullEmails(
            this.fakeUser._id,
            (error, fullEmails) => {
              expect(error).to.not.exist
              expect(
                fullEmails[0].affiliation.inReconfirmNotificationPeriod
              ).to.equal(true)
              expect(
                fullEmails[1].affiliation.inReconfirmNotificationPeriod
              ).to.equal(true)
              done()
            }
          )
        })
        it('should not flag affiliations outside of notification period', function (done) {
          const aboutToBeWithinPeriod = moment()
            .subtract(institutionNonSSO.maxConfirmationMonths, 'months')
            .add(15, 'days')
            .toDate() // expires in 15 days
          const user = {
            _id: '12390i',
            email: email1,
            emails: [
              {
                email: email1,
                reversedHostname: 'moc.noitailiffa-elpmaxe',
                confirmedAt: new Date(),
                default: true,
              },
              {
                email: email2,
                reversedHostname: 'moc.noitailiffa-elpmaxe',
                confirmedAt: aboutToBeWithinPeriod,
              },
            ],
          }
          this.getUserAffiliations.resolves(affiliationsData)
          this.UserGetter.promises.getUser = sinon.stub().resolves(user)
          this.UserGetter.getUserFullEmails(
            this.fakeUser._id,
            (error, fullEmails) => {
              expect(error).to.not.exist
              expect(
                fullEmails[0].affiliation.inReconfirmNotificationPeriod
              ).to.equal(false)
              expect(
                fullEmails[1].affiliation.inReconfirmNotificationPeriod
              ).to.equal(false)
              done()
            }
          )
        })
      })

      describe('SSO institutions', function () {
        it('should flag only linked email, if in notification period', function (done) {
          const email1 = 'email1@sso.bar'
          const email2 = 'email2@sso.bar'
          const email3 = 'email3@sso.bar'

          const affiliationsData = [
            {
              email: email1,
              role: 'Prof',
              department: 'Maths',
              inferred: false,
              licence: 'pro_plus',
              institution: institutionSSO,
            },
            {
              email: email2,
              role: 'Prof',
              department: 'Maths',
              inferred: false,
              licence: 'pro_plus',
              institution: institutionSSO,
            },
            {
              email: email3,
              role: 'Prof',
              department: 'Maths',
              inferred: false,
              licence: 'pro_plus',
              institution: institutionSSO,
            },
          ]

          const user = {
            _id: '12390i',
            email: email1,
            emails: [
              {
                email: email1,
                reversedHostname: 'rab.oss',
                confirmedAt: new Date('2019-09-24'),
                reconfirmedAt: new Date('2019-09-24'),
                default: true,
              },
              {
                email: email2,
                reversedHostname: 'rab.oss',
                confirmedAt: new Date('2019-09-24'),
                reconfirmedAt: new Date('2019-09-24'),
                samlProviderId: institutionSSO.id,
              },
              {
                email: email3,
                reversedHostname: 'rab.oss',
                confirmedAt: new Date('2019-09-24'),
                reconfirmedAt: new Date('2019-09-24'),
              },
            ],
            samlIdentifiers: [
              {
                providerId: institutionSSO.id,
                externalUserId: 'abc123',
              },
            ],
          }
          this.getUserAffiliations.resolves(affiliationsData)
          this.UserGetter.promises.getUser = sinon.stub().resolves(user)
          this.UserGetter.getUserFullEmails(
            this.fakeUser._id,
            (error, fullEmails) => {
              expect(error).to.not.exist
              expect(
                fullEmails[0].affiliation.inReconfirmNotificationPeriod
              ).to.equal(false)
              expect(
                fullEmails[1].affiliation.inReconfirmNotificationPeriod
              ).to.equal(true)
              expect(
                fullEmails[2].affiliation.inReconfirmNotificationPeriod
              ).to.equal(false)
              done()
            }
          )
        })
      })

      describe('multiple institution affiliations', function () {
        it('should flag each institution', function (done) {
          const email1 = 'email1@sso.bar'
          const email2 = 'email2@sso.bar'
          const email3 = 'email3@foo.bar'
          const email4 = 'email4@foo.bar'

          const affiliationsData = [
            {
              email: email1,
              role: 'Prof',
              department: 'Maths',
              inferred: false,
              licence: 'pro_plus',
              institution: institutionSSO,
            },
            {
              email: email2,
              role: 'Prof',
              department: 'Maths',
              inferred: false,
              licence: 'pro_plus',
              institution: institutionSSO,
            },
            {
              email: email3,
              role: 'Prof',
              department: 'Maths',
              inferred: false,
              licence: 'pro_plus',
              institution: institutionNonSSO,
            },
            {
              email: email4,
              role: 'Prof',
              department: 'Maths',
              inferred: false,
              licence: 'pro_plus',
              institution: institutionNonSSO,
            },
          ]
          const user = {
            _id: '12390i',
            email: email1,
            emails: [
              {
                email: email1,
                reversedHostname: 'rab.oss',
                confirmedAt: '2019-09-24T20:25:08.503Z',
                default: true,
              },
              {
                email: email2,
                reversedHostname: 'rab.oss',
                confirmedAt: new Date('2019-09-24T20:25:08.503Z'),
                samlProviderId: institutionSSO.id,
              },
              {
                email: email3,
                reversedHostname: 'rab.oof',
                confirmedAt: new Date('2019-10-24T20:25:08.503Z'),
              },
              {
                email: email4,
                reversedHostname: 'rab.oof',
                confirmedAt: new Date('2019-09-24T20:25:08.503Z'),
              },
            ],
            samlIdentifiers: [
              {
                providerId: institutionSSO.id,
                externalUserId: 'abc123',
              },
            ],
          }
          this.getUserAffiliations.resolves(affiliationsData)
          this.UserGetter.promises.getUser = sinon.stub().resolves(user)
          this.UserGetter.getUserFullEmails(
            this.fakeUser._id,
            (error, fullEmails) => {
              expect(error).to.not.exist
              expect(
                fullEmails[0].affiliation.inReconfirmNotificationPeriod
              ).to.to.equal(false)
              expect(
                fullEmails[1].affiliation.inReconfirmNotificationPeriod
              ).to.equal(true)
              expect(
                fullEmails[2].affiliation.inReconfirmNotificationPeriod
              ).to.equal(true)
              expect(
                fullEmails[3].affiliation.inReconfirmNotificationPeriod
              ).to.equal(true)
              done()
            }
          )
        })
      })

      describe('reconfirmedAt', function () {
        it('only use confirmedAt when no reconfirmedAt', function (done) {
          const email1 = 'email1@foo.bar'
          const email2 = 'email2@foo.bar'
          const email3 = 'email3@foo.bar'

          const affiliationsData = [
            {
              email: email1,
              role: 'Prof',
              department: 'Maths',
              inferred: false,
              licence: 'pro_plus',
              institution: institutionNonSSO,
            },
            {
              email: email2,
              role: 'Prof',
              department: 'Maths',
              inferred: false,
              licence: 'pro_plus',
              institution: institutionNonSSO,
            },
            {
              email: email3,
              role: 'Prof',
              department: 'Maths',
              inferred: false,
              licence: 'pro_plus',
              institution: institutionNonSSO,
            },
          ]
          const user = {
            _id: '12390i',
            email: email1,
            emails: [
              {
                email: email1,
                reversedHostname: 'rab.oof',
                confirmedAt: moment().subtract(
                  institutionSSO.maxConfirmationMonths * 2,
                  'months'
                ),
                reconfirmedAt: moment().subtract(
                  institutionSSO.maxConfirmationMonths * 3,
                  'months'
                ),
                default: true,
              },
              {
                email: email2,
                reversedHostname: 'rab.oof',
                confirmedAt: moment().subtract(
                  institutionSSO.maxConfirmationMonths * 3,
                  'months'
                ),
                reconfirmedAt: moment().subtract(
                  institutionSSO.maxConfirmationMonths * 2,
                  'months'
                ),
              },
              {
                email: email3,
                reversedHostname: 'rab.oof',
                confirmedAt: moment().subtract(
                  institutionSSO.maxConfirmationMonths * 4,
                  'months'
                ),
                reconfirmedAt: moment().subtract(
                  institutionSSO.maxConfirmationMonths * 4,
                  'months'
                ),
              },
            ],
          }
          this.getUserAffiliations.resolves(affiliationsData)
          this.UserGetter.promises.getUser = sinon.stub().resolves(user)
          this.UserGetter.getUserFullEmails(
            this.fakeUser._id,
            (error, fullEmails) => {
              expect(error).to.not.exist
              expect(
                fullEmails[0].affiliation.inReconfirmNotificationPeriod
              ).to.equal(true)
              expect(
                fullEmails[1].affiliation.inReconfirmNotificationPeriod
              ).to.equal(true)
              expect(
                fullEmails[2].affiliation.inReconfirmNotificationPeriod
              ).to.equal(true)
              done()
            }
          )
        })
      })

      describe('before reconfirmation period expires and within reconfirmation notification period', function () {
        const email = 'leonard@example-affiliation.com'
        it('should flag the email', function (done) {
          const confirmedAt = moment()
            .subtract(institutionNonSSO.maxConfirmationMonths, 'months')
            .subtract(14, 'days')
            .toDate() // expires in 14 days
          const affiliationsData = [
            {
              email,
              role: 'Prof',
              department: 'Medicine',
              inferred: false,
              licence: 'pro_plus',
              institution: institutionNonSSO,
            },
          ]
          const user = {
            _id: '12390i',
            email,
            emails: [
              {
                email,
                confirmedAt,
                default: true,
              },
            ],
          }
          this.getUserAffiliations.resolves(affiliationsData)
          this.UserGetter.promises.getUser = sinon.stub().resolves(user)
          this.UserGetter.getUserFullEmails(
            this.fakeUser._id,
            (error, fullEmails) => {
              expect(error).to.not.exist
              expect(
                fullEmails[0].affiliation.inReconfirmNotificationPeriod
              ).to.equal(true)
              done()
            }
          )
        })
      })

      describe('when no Settings.reconfirmNotificationDays', function () {
        it('should always return inReconfirmNotificationPeriod:false', function (done) {
          const email1 = 'email1@sso.bar'
          const email2 = 'email2@foo.bar'
          const email3 = 'email3@foo.bar'
          const confirmedAtAboutToExpire = moment()
            .subtract(institutionNonSSO.maxConfirmationMonths, 'months')
            .subtract(14, 'days')
            .toDate() // expires in 14 days

          const affiliationsData = [
            {
              email: email1,
              institution: institutionSSO,
            },
            {
              email: email2,
              institution: institutionNonSSO,
            },
            {
              email: email3,
              institution: institutionNonSSO,
            },
          ]
          const user = {
            _id: '12390i',
            email: email1,
            emails: [
              {
                email: email1,
                confirmedAt: confirmedAtAboutToExpire,
                default: true,
                samlProviderId: institutionSSO.id,
              },
              {
                email: email2,
                confirmedAt: new Date('2019-09-24T20:25:08.503Z'),
              },
              {
                email: email3,
                confirmedAt: new Date('2019-10-24T20:25:08.503Z'),
              },
            ],
            samlIdentifiers: [
              {
                providerId: institutionSSO.id,
                externalUserId: 'abc123',
              },
            ],
          }
          this.settings.reconfirmNotificationDays = undefined
          this.getUserAffiliations.resolves(affiliationsData)
          this.UserGetter.promises.getUser = sinon.stub().resolves(user)
          this.UserGetter.getUserFullEmails(
            this.fakeUser._id,
            (error, fullEmails) => {
              expect(error).to.not.exist
              expect(
                fullEmails[0].affiliation.inReconfirmNotificationPeriod
              ).to.to.equal(false)
              expect(
                fullEmails[1].affiliation.inReconfirmNotificationPeriod
              ).to.equal(false)
              expect(
                fullEmails[2].affiliation.inReconfirmNotificationPeriod
              ).to.equal(false)
              done()
            }
          )
        })
      })
    })
  })

  describe('getUserbyMainEmail', function () {
    it('query user by main email', function (done) {
      const email = 'hello@world.com'
      const projection = { emails: 1 }
      this.UserGetter.getUserByMainEmail(email, projection, (error, user) => {
        expect(error).to.not.exist
        this.findOne.called.should.equal(true)
        this.findOne.calledWith({ email }, { projection }).should.equal(true)
        done()
      })
    })

    it('return user if found', function (done) {
      const email = 'hello@world.com'
      this.UserGetter.getUserByMainEmail(email, (error, user) => {
        expect(error).to.not.exist
        user.should.deep.equal(this.fakeUser)
        done()
      })
    })

    it('trim email', function (done) {
      const email = 'hello@world.com'
      this.UserGetter.getUserByMainEmail(` ${email} `, (error, user) => {
        expect(error).to.not.exist
        this.findOne.called.should.equal(true)
        this.findOne.calledWith({ email }).should.equal(true)
        done()
      })
    })
  })

  describe('getUserByAnyEmail', function () {
    it('query user for any email', function (done) {
      const email = 'hello@world.com'
      const expectedQuery = {
        emails: { $exists: true },
        'emails.email': email,
      }
      const projection = { emails: 1 }
      this.UserGetter.getUserByAnyEmail(
        ` ${email} `,
        projection,
        (error, user) => {
          expect(error).to.not.exist
          this.findOne
            .calledWith(expectedQuery, { projection })
            .should.equal(true)
          user.should.deep.equal(this.fakeUser)
          done()
        }
      )
    })

    it('query contains $exists:true so partial index is used', function (done) {
      const expectedQuery = {
        emails: { $exists: true },
        'emails.email': '',
      }
      this.UserGetter.getUserByAnyEmail('', {}, (error, user) => {
        expect(error).to.not.exist
        this.findOne
          .calledWith(expectedQuery, { projection: {} })
          .should.equal(true)
        done()
      })
    })

    it('checks main email as well', function (done) {
      this.findOne.callsArgWith(2, null, null)
      const email = 'hello@world.com'
      const projection = { emails: 1 }
      this.UserGetter.getUserByAnyEmail(
        ` ${email} `,
        projection,
        (error, user) => {
          expect(error).to.not.exist
          this.findOne.calledTwice.should.equal(true)
          this.findOne.calledWith({ email }, { projection }).should.equal(true)
          done()
        }
      )
    })
  })

  describe('getUsersByHostname', function () {
    it('should find user by hostname', function (done) {
      const hostname = 'bar.foo'
      const expectedQuery = {
        emails: { $exists: true },
        'emails.reversedHostname': hostname.split('').reverse().join(''),
      }
      const projection = { emails: 1 }
      this.UserGetter.getUsersByHostname(
        hostname,
        projection,
        (error, users) => {
          expect(error).to.not.exist
          this.find.calledOnce.should.equal(true)
          this.find.calledWith(expectedQuery, { projection }).should.equal(true)
          done()
        }
      )
    })
  })

  describe('getUsersByAnyConfirmedEmail', function () {
    it('should find users by confirmed email', function (done) {
      const emails = ['confirmed@example.com']

      this.UserGetter.getUsersByAnyConfirmedEmail(emails, (error, users) => {
        expect(error).to.not.exist
        expect(this.find).to.be.calledOnceWith(
          {
            'emails.email': { $in: emails }, // use the index on emails.email
            emails: {
              $exists: true,
              $elemMatch: {
                email: { $in: emails },
                confirmedAt: { $exists: true },
              },
            },
          },
          { projection: {} }
        )
        done()
      })
    })
  })

  describe('getUsersByV1Id', function () {
    it('should find users by list of v1 ids', function (done) {
      const v1Ids = [501]
      const expectedQuery = {
        'overleaf.id': { $in: v1Ids },
      }
      const projection = { emails: 1 }
      this.UserGetter.getUsersByV1Ids(v1Ids, projection, (error, users) => {
        expect(error).to.not.exist
        this.find.calledOnce.should.equal(true)
        this.find.calledWith(expectedQuery, { projection }).should.equal(true)
        done()
      })
    })
  })

  describe('ensureUniqueEmailAddress', function () {
    beforeEach(function () {
      this.UserGetter.getUserByAnyEmail = sinon.stub()
    })

    it('should return error if existing user is found', function (done) {
      this.UserGetter.getUserByAnyEmail.callsArgWith(1, null, this.fakeUser)
      this.UserGetter.ensureUniqueEmailAddress(this.newEmail, err => {
        expect(err).to.exist
        expect(err).to.be.an.instanceof(Errors.EmailExistsError)
        done()
      })
    })

    it('should return null if no user is found', function (done) {
      this.UserGetter.getUserByAnyEmail.callsArgWith(1)
      this.UserGetter.ensureUniqueEmailAddress(this.newEmail, err => {
        expect(err).not.to.exist
        done()
      })
    })
  })
})
