import { vi, expect } from 'vitest'
import path from 'node:path'
import sinon from 'sinon'
import mongodb from 'mongodb-legacy'
import Errors from '../../../../app/src/Features/Errors/Errors.js'

const modulePath = path.join(
  import.meta.dirname,
  '../../../../app/src/Features/Institutions/InstitutionsAPI'
)
const { ObjectId } = mongodb
vi.mock('../../../../app/src/Features/Errors/Errors.js', () =>
  vi.importActual('../../../../app/src/Features/Errors/Errors.js')
)

describe('InstitutionsAPI', function () {
  beforeEach(async function (ctx) {
    ctx.settings = {
      apis: { v1: { url: 'v1.url', user: '', pass: '', timeout: 5000 } },
    }
    ctx.request = sinon.stub()
    ctx.fetchNothing = sinon.stub()
    ctx.ipMatcherNotification = {
      read: (ctx.markAsReadIpMatcher = sinon.stub().resolves()),
    }

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

    vi.doMock('requestretry', () => ({
      default: ctx.request,
    }))

    vi.doMock('@overleaf/fetch-utils', () => ({
      fetchNothing: ctx.fetchNothing,
      fetchJson: (ctx.fetchJson = sinon.stub()),
    }))

    vi.doMock(
      '../../../../app/src/Features/Notifications/NotificationsBuilder',
      () => ({
        default: {
          promises: {
            ipMatcherAffiliation: sinon
              .stub()
              .returns(ctx.ipMatcherNotification),
          },
        },
      })
    )

    vi.doMock('../../../../app/src/infrastructure/Modules', () => ({
      default: (ctx.Modules = {
        promises: {
          hooks: {
            fire: sinon.stub(),
          },
        },
      }),
    }))

    ctx.InstitutionsAPI = (await import(modulePath)).default

    ctx.stubbedUser = {
      _id: '3131231',
      name: 'bob',
      email: 'hello@world.com',
    }
    ctx.newEmail = 'bob@bob.com'
  })

  describe('getInstitutionAffiliations', function () {
    it('get affiliations', async function (ctx) {
      ctx.institutionId = 123
      const responseBody = ['123abc', '456def']
      ctx.request.yields(null, { statusCode: 200 }, responseBody)
      const body =
        await ctx.InstitutionsAPI.promises.getInstitutionAffiliations(
          ctx.institutionId
        )

      ctx.request.calledOnce.should.equal(true)
      const requestOptions = ctx.request.lastCall.args[0]
      const expectedUrl = `v1.url/api/v2/institutions/${ctx.institutionId}/affiliations`
      requestOptions.url.should.equal(expectedUrl)
      requestOptions.method.should.equal('GET')
      requestOptions.maxAttempts.should.exist
      requestOptions.maxAttempts.should.not.equal(0)
      requestOptions.retryDelay.should.exist
      expect(requestOptions.body).not.to.exist
      body.should.equal(responseBody)
    })

    it('handle empty response', async function (ctx) {
      ctx.institutionId = 123
      ctx.settings.apis.v1.url = ''

      const body =
        await ctx.InstitutionsAPI.promises.getInstitutionAffiliations(
          ctx.institutionId
        )
      expect(body).to.be.a('Array')
      body.length.should.equal(0)
    })
  })

  describe('getLicencesForAnalytics', function () {
    const lag = 'daily'
    const queryDate = '2017-01-07:00:00.000Z'
    it('should send the request to v1', async function (ctx) {
      const v1Result = {
        lag: 'daily',
        date: queryDate,
        data: {
          user_counts: { total: [], new: [] },
          max_confirmation_months: [],
        },
      }
      ctx.request.callsArgWith(1, null, { statusCode: 201 }, v1Result)
      await ctx.InstitutionsAPI.promises.getLicencesForAnalytics(lag, queryDate)
      const requestOptions = ctx.request.lastCall.args[0]
      expect(requestOptions.body.query_date).to.equal(queryDate)
      expect(requestOptions.body.lag).to.equal(lag)
      requestOptions.method.should.equal('GET')
    })
    it('should handle errors', async function (ctx) {
      ctx.request.callsArgWith(1, null, { statusCode: 500 })
      let error

      try {
        await ctx.InstitutionsAPI.promises.getLicencesForAnalytics(
          lag,
          queryDate
        )
      } catch (err) {
        error = err
      }

      expect(error).to.be.instanceOf(Errors.V1ConnectionError)
    })
  })

  describe('getUserAffiliations', function () {
    it('get affiliations with commons', async function (ctx) {
      const responseBody = [
        {
          foo: 'bar',
          institution: {
            commonsAccount: true,
            confirmed: true,
          },
        },
      ]
      ctx.request.callsArgWith(1, null, { statusCode: 201 }, responseBody)
      const body = await ctx.InstitutionsAPI.promises.getUserAffiliations(
        ctx.stubbedUser._id
      )
      ctx.request.calledOnce.should.equal(true)
      const requestOptions = ctx.request.lastCall.args[0]
      const expectedUrl = `v1.url/api/v2/users/${ctx.stubbedUser._id}/affiliations`
      requestOptions.url.should.equal(expectedUrl)
      requestOptions.method.should.equal('GET')
      requestOptions.maxAttempts.should.equal(3)
      ctx.Modules.promises.hooks.fire.should.have.been.called
      expect(requestOptions.body).not.to.exist
      expect(body).to.deep.equal(responseBody)
    })

    it('get affiliations with domain capture for groups', async function (ctx) {
      const responseBody = [
        {
          id: '123abc',
          foo: 'bar',
          institution: {
            commonsAccount: false,
            confirmed: true,
          },
        },
      ]
      ctx.request.callsArgWith(1, null, { statusCode: 201 }, responseBody)
      const groupResponse = {
        _id: new ObjectId(),
        managedUsersEnabled: false,
        domainCaptureEnabled: true,
      }
      ctx.Modules.promises.hooks.fire
        .withArgs(
          'getGroupWithDomainCaptureByV1Id',
          responseBody[0].institution.id
        )
        .resolves([groupResponse])
      const body = await ctx.InstitutionsAPI.promises.getUserAffiliations(
        ctx.stubbedUser._id
      )
      ctx.request.calledOnce.should.equal(true)
      const requestOptions = ctx.request.lastCall.args[0]
      const expectedUrl = `v1.url/api/v2/users/${ctx.stubbedUser._id}/affiliations`
      requestOptions.url.should.equal(expectedUrl)
      requestOptions.method.should.equal('GET')
      requestOptions.maxAttempts.should.equal(3)
      ctx.Modules.promises.hooks.fire.should.have.been.calledWith(
        'getGroupWithDomainCaptureByV1Id',
        responseBody[0].institution.id
      )
      expect(requestOptions.body).not.to.exist
      expect(body).to.deep.equal([
        {
          ...responseBody[0],
          group: {
            ...groupResponse,
          },
        },
      ])
    })

    it('does not query for group with domain capture if domain is not confirmed', async function (ctx) {
      const responseBody = [
        {
          id: '123abc',
          foo: 'bar',
          institution: {
            commonsAccount: false,
            confirmed: false,
          },
        },
      ]
      ctx.request.callsArgWith(1, null, { statusCode: 201 }, responseBody)

      const body = await ctx.InstitutionsAPI.promises.getUserAffiliations(
        ctx.stubbedUser._id
      )
      ctx.request.calledOnce.should.equal(true)
      const requestOptions = ctx.request.lastCall.args[0]
      const expectedUrl = `v1.url/api/v2/users/${ctx.stubbedUser._id}/affiliations`
      requestOptions.url.should.equal(expectedUrl)
      requestOptions.method.should.equal('GET')
      requestOptions.maxAttempts.should.equal(3)
      ctx.Modules.promises.hooks.fire.should.not.have.been.called
      expect(requestOptions.body).not.to.exist
      expect(body).to.deep.equal([
        {
          ...responseBody[0],
        },
      ])
    })

    it('handle error', async function (ctx) {
      const body = { errors: 'affiliation error message' }
      ctx.request.callsArgWith(1, null, { statusCode: 503 }, body)
      let error

      try {
        await ctx.InstitutionsAPI.promises.getUserAffiliations(
          ctx.stubbedUser._id
        )
      } catch (err) {
        error = err
      }

      expect(error).to.be.instanceOf(Errors.V1ConnectionError)
    })

    it('handle empty response', async function (ctx) {
      ctx.settings.apis.v1.url = ''
      const body = await ctx.InstitutionsAPI.promises.getUserAffiliations(
        ctx.stubbedUser._id
      )
      expect(body).to.be.a('Array')
      body.length.should.equal(0)
    })
  })

  describe('getUsersNeedingReconfirmationsLapsedProcessed', function () {
    it('get the list of users', async function (ctx) {
      ctx.fetchJson.resolves({ statusCode: 200 })
      await ctx.InstitutionsAPI.promises.getUsersNeedingReconfirmationsLapsedProcessed()
      ctx.fetchJson.calledOnce.should.equal(true)
      const requestOptions = ctx.fetchJson.lastCall.args[1]
      const expectedUrl = `v1.url/api/v2/institutions/need_reconfirmation_lapsed_processed`
      ctx.fetchJson.lastCall.args[0].should.equal(expectedUrl)
      requestOptions.method.should.equal('GET')
    })

    it('handle error', async function (ctx) {
      ctx.fetchJson.throws({ info: { statusCode: 500 } })
      await expect(
        ctx.InstitutionsAPI.promises.getUsersNeedingReconfirmationsLapsedProcessed()
      ).to.be.rejected
    })
  })

  describe('addAffiliation', function () {
    beforeEach(function (ctx) {
      ctx.fetchNothing.resolves({ status: 201 })
    })

    it('add affiliation', async function (ctx) {
      const affiliationOptions = {
        university: { id: 1 },
        department: 'Math',
        role: 'Prof',
        confirmedAt: new Date(),
        entitlement: true,
      }
      await ctx.InstitutionsAPI.promises.addAffiliation(
        ctx.stubbedUser._id,
        ctx.newEmail,
        affiliationOptions
      )
      ctx.fetchNothing.calledOnce.should.equal(true)
      const requestOptions = ctx.fetchNothing.lastCall.args[1]
      const expectedUrl = `v1.url/api/v2/users/${ctx.stubbedUser._id}/affiliations`
      expect(ctx.fetchNothing.lastCall.args[0]).to.equal(expectedUrl)
      requestOptions.method.should.equal('POST')

      const { json } = requestOptions
      Object.keys(json).length.should.equal(7)
      expect(json).to.deep.equal(
        Object.assign(
          { email: ctx.newEmail, rejectIfBlocklisted: undefined },
          affiliationOptions
        )
      )
      ctx.markAsReadIpMatcher.calledOnce.should.equal(true)
    })

    it('handles 422 error', async function (ctx) {
      const messageFromApi = 'affiliation error message'
      const body = JSON.stringify({ errors: messageFromApi })
      ctx.fetchNothing.throws({ response: { status: 422 }, body })
      let error

      try {
        await ctx.InstitutionsAPI.promises.addAffiliation(
          ctx.stubbedUser._id,
          ctx.newEmail,
          {}
        )
      } catch (err) {
        error = err
      }

      expect(error).to.be.instanceOf(Errors.InvalidInstitutionalEmailError)
      expect(error).to.have.property('message', `422: ${messageFromApi}`)
    })

    it('handles 500 error', async function (ctx) {
      const body = { errors: 'affiliation error message' }
      ctx.fetchNothing.throws({ response: { status: 500 }, body })
      let error

      try {
        await ctx.InstitutionsAPI.promises.addAffiliation(
          ctx.stubbedUser._id,
          ctx.newEmail,
          {}
        )
      } catch (err) {
        error = err
      }

      expect(error).to.be.instanceOf(Errors.V1ConnectionError)
      expect(error.message).to.equal('error getting affiliations from v1')
      expect(error.info).to.eql({
        status: 500,
        body: { errors: 'affiliation error message' },
      })
    })

    it('uses default error message when no error body in response', async function (ctx) {
      ctx.fetchNothing.throws({ response: { status: 429 } })
      let error

      try {
        await ctx.InstitutionsAPI.promises.addAffiliation(
          ctx.stubbedUser._id,
          ctx.newEmail,
          {}
        )
      } catch (err) {
        error = err
      }

      expect(error).to.be.instanceOf(Error)
      expect(error).to.have.property(
        'message',
        "Couldn't create affiliation: 429"
      )
    })

    it('does not try to mark IP matcher notifications as read if no university passed', async function (ctx) {
      const affiliationOptions = {
        confirmedAt: new Date(),
      }

      await ctx.InstitutionsAPI.promises.addAffiliation(
        ctx.stubbedUser._id,
        ctx.newEmail,
        affiliationOptions
      )

      expect(ctx.markAsReadIpMatcher.callCount).to.equal(0)
    })
  })

  describe('removeAffiliation', function () {
    beforeEach(function (ctx) {
      ctx.fetchNothing.throws({ response: { status: 404 } })
    })

    it('remove affiliation', async function (ctx) {
      await ctx.InstitutionsAPI.promises.removeAffiliation(
        ctx.stubbedUser._id,
        ctx.newEmail
      )
      ctx.fetchNothing.calledOnce.should.equal(true)
      const requestOptions = ctx.fetchNothing.lastCall.args[1]
      const expectedUrl = `v1.url/api/v2/users/${ctx.stubbedUser._id}/affiliations/remove`
      ctx.fetchNothing.lastCall.args[0].should.equal(expectedUrl)
      requestOptions.method.should.equal('POST')
      expect(requestOptions.json).to.deep.equal({ email: ctx.newEmail })
    })

    it('handle error', async function (ctx) {
      ctx.fetchNothing.throws({ response: { status: 500 } })
      let error

      try {
        await ctx.InstitutionsAPI.promises.removeAffiliation(
          ctx.stubbedUser._id,
          ctx.newEmail
        )
      } catch (err) {
        error = err
      }

      expect(error).to.exist
      expect(error).to.have.property('message')
    })
  })

  describe('deleteAffiliations', function () {
    it('delete affiliations', async function (ctx) {
      ctx.request.callsArgWith(1, null, { statusCode: 200 })
      await ctx.InstitutionsAPI.promises.deleteAffiliations(ctx.stubbedUser._id)
      ctx.request.calledOnce.should.equal(true)
      const requestOptions = ctx.request.lastCall.args[0]
      const expectedUrl = `v1.url/api/v2/users/${ctx.stubbedUser._id}/affiliations`
      requestOptions.url.should.equal(expectedUrl)
      requestOptions.method.should.equal('DELETE')
    })

    it('handle error', async function (ctx) {
      const body = { errors: 'affiliation error message' }
      ctx.request.callsArgWith(1, null, { statusCode: 518 }, body)
      let error

      try {
        await ctx.InstitutionsAPI.promises.deleteAffiliations(
          ctx.stubbedUser._id
        )
      } catch (err) {
        error = err
      }

      expect(error).to.be.instanceOf(Errors.V1ConnectionError)
    })
  })

  describe('endorseAffiliation', function () {
    beforeEach(function (ctx) {
      ctx.request.callsArgWith(1, null, { statusCode: 204 })
    })

    it('endorse affiliation', async function (ctx) {
      await ctx.InstitutionsAPI.promises.endorseAffiliation(
        ctx.stubbedUser._id,
        ctx.newEmail,
        'Student',
        'Physics'
      )
      ctx.request.calledOnce.should.equal(true)
      const requestOptions = ctx.request.lastCall.args[0]
      const expectedUrl = `v1.url/api/v2/users/${ctx.stubbedUser._id}/affiliations/endorse`
      requestOptions.url.should.equal(expectedUrl)
      requestOptions.method.should.equal('POST')

      const { body } = requestOptions
      Object.keys(body).length.should.equal(3)
      body.email.should.equal(ctx.newEmail)
      body.role.should.equal('Student')
      body.department.should.equal('Physics')
    })
  })

  describe('sendUsersWithReconfirmationsLapsedProcessed', function () {
    const users = ['abc123', 'def456']

    it('sends the list of users', async function (ctx) {
      ctx.request.callsArgWith(1, null, { statusCode: 200 })
      await ctx.InstitutionsAPI.promises.sendUsersWithReconfirmationsLapsedProcessed(
        users
      )
      ctx.request.calledOnce.should.equal(true)
      const requestOptions = ctx.request.lastCall.args[0]
      const expectedUrl =
        'v1.url/api/v2/institutions/reconfirmation_lapsed_processed'
      requestOptions.url.should.equal(expectedUrl)
      requestOptions.method.should.equal('POST')
      expect(requestOptions.body).to.deep.equal({ users })
    })

    it('handle error', async function (ctx) {
      ctx.request.callsArgWith(1, null, { statusCode: 500 })
      let error

      try {
        await ctx.InstitutionsAPI.promises.sendUsersWithReconfirmationsLapsedProcessed(
          users
        )
      } catch (err) {
        error = err
      }

      expect(error).to.exist
    })
  })
})
