import { vi, expect } from 'vitest'
import sinon from 'sinon'
import mongodb from 'mongodb-legacy'

const modulePath =
  '../../../../app/src/Features/Project/ProjectOptionsHandler.mjs'

const { ObjectId } = mongodb

describe('ProjectOptionsHandler', function () {
  const projectId = '4eecaffcbffa66588e000008'

  beforeEach(async function (ctx) {
    ctx.projectModel = class Project {}
    ctx.projectModel.updateOne = sinon.stub().resolves()

    ctx.db = {
      projects: {
        updateOne: sinon.stub().resolves(),
      },
    }

    vi.doMock('../../../../app/src/models/Project', () => ({
      Project: ctx.projectModel,
    }))

    vi.doMock('@overleaf/settings', () => ({
      default: {
        languages: [
          { name: 'English', code: 'en' },
          { name: 'French', code: 'fr' },
        ],
        imageRoot: 'docker-repo/subdir',
        allowedImageNames: [
          { imageName: 'texlive-0000.0', imageDesc: 'test image 0' },
          { imageName: 'texlive-1234.5', imageDesc: 'test image 1' },
        ],
        safeCompilers: ['pdflatex', 'latex', 'xelatex', 'lualatex'],
      },
    }))

    vi.doMock('../../../../app/src/infrastructure/mongodb', () => ({
      db: ctx.db,
      ObjectId,
    }))

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

  describe('Setting the compiler', function () {
    it('should perform and update on mongo', async function (ctx) {
      await ctx.handler.promises.setCompiler(projectId, 'xeLaTeX')
      const args = ctx.projectModel.updateOne.args[0]
      args[0]._id.should.equal(projectId)
      args[1].compiler.should.equal('xelatex')
    })

    it('should not perform and update on mongo if it is not a recognised compiler', async function (ctx) {
      await expect(
        ctx.handler.promises.setCompiler(projectId, 'something')
      ).to.be.rejectedWith('invalid compiler')

      ctx.projectModel.updateOne.called.should.equal(false)
    })

    describe('when called without arg', function () {
      it('should callback with null', async function (ctx) {
        await ctx.handler.promises.setCompiler(projectId, null)
        ctx.projectModel.updateOne.callCount.should.equal(0)
      })
    })

    describe('when mongo update error occurs', function () {
      beforeEach(function (ctx) {
        ctx.projectModel.updateOne = sinon.stub().yields('error')
      })

      it('should be rejected', async function (ctx) {
        await expect(ctx.handler.promises.setCompiler(projectId, 'xeLaTeX')).to
          .be.rejected
      })
    })
  })

  describe('Setting the imageName', function () {
    it('should perform and update on mongo', async function (ctx) {
      await ctx.handler.promises.setImageName(projectId, 'texlive-1234.5')
      const args = ctx.projectModel.updateOne.args[0]
      args[0]._id.should.equal(projectId)
      args[1].imageName.should.equal('docker-repo/subdir/texlive-1234.5')
    })

    it('should not perform and update on mongo if it is not a reconised image name', async function (ctx) {
      const fakeImageName = 'something'
      await expect(
        ctx.handler.promises.setImageName(projectId, fakeImageName)
      ).to.be.rejectedWith('invalid imageName')

      ctx.projectModel.updateOne.called.should.equal(false)
    })

    describe('when called without arg', function () {
      it('should callback with null', async function (ctx) {
        await ctx.handler.promises.setImageName(projectId, null)
        ctx.projectModel.updateOne.callCount.should.equal(0)
      })
    })

    describe('when mongo update error occurs', function () {
      beforeEach(function (ctx) {
        ctx.projectModel.updateOne = sinon.stub().yields('error')
      })

      it('should be rejected', async function (ctx) {
        await expect(
          ctx.handler.promises.setImageName(projectId, 'texlive-1234.5')
        ).to.be.rejected
      })
    })
  })

  describe('setting the spellCheckLanguage', function () {
    it('should perform and update on mongo', async function (ctx) {
      await ctx.handler.promises.setSpellCheckLanguage(projectId, 'fr')
      const args = ctx.projectModel.updateOne.args[0]
      args[0]._id.should.equal(projectId)
      args[1].spellCheckLanguage.should.equal('fr')
    })

    it('should not perform and update on mongo if it is not a reconised langauge', async function (ctx) {
      const fakeLanguageCode = 'not a lang'
      await expect(
        ctx.handler.promises.setSpellCheckLanguage(projectId, fakeLanguageCode)
      ).to.be.rejectedWith('invalid languageCode')
      ctx.projectModel.updateOne.called.should.equal(false)
    })

    it('should perform and update on mongo if the language is blank (means turn it off)', async function (ctx) {
      await ctx.handler.promises.setSpellCheckLanguage(projectId, '')
      ctx.projectModel.updateOne.called.should.equal(true)
    })

    describe('when mongo update error occurs', function () {
      beforeEach(function (ctx) {
        ctx.projectModel.updateOne = sinon.stub().yields('error')
      })

      it('should be rejected', async function (ctx) {
        await expect(ctx.handler.promises.setSpellCheckLanguage(projectId)).to
          .be.rejected
      })
    })
  })

  describe('setting the brandVariationId', function () {
    it('should perform and update on mongo', async function (ctx) {
      await ctx.handler.promises.setBrandVariationId(projectId, '123')
      const args = ctx.projectModel.updateOne.args[0]
      args[0]._id.should.equal(projectId)
      args[1].brandVariationId.should.equal('123')
    })

    it('should not perform and update on mongo if there is no brand variation', async function (ctx) {
      await ctx.handler.promises.setBrandVariationId(projectId, null)
      ctx.projectModel.updateOne.called.should.equal(false)
    })

    it('should not perform and update on mongo if brand variation is an empty string', async function (ctx) {
      await ctx.handler.promises.setBrandVariationId(projectId, '')
      ctx.projectModel.updateOne.called.should.equal(false)
    })

    describe('when mongo update error occurs', function () {
      beforeEach(function (ctx) {
        ctx.projectModel.updateOne = sinon.stub().yields('error')
      })

      it('should be rejected', async function (ctx) {
        await expect(ctx.handler.promises.setBrandVariationId(projectId, '123'))
          .to.be.rejected
      })
    })
  })

  describe('setting the rangesSupportEnabled', function () {
    it('should perform and update on mongo', async function (ctx) {
      await ctx.handler.promises.setHistoryRangesSupport(projectId, true)
      sinon.assert.calledWith(
        ctx.db.projects.updateOne,
        { _id: new ObjectId(projectId) },
        { $set: { 'overleaf.history.rangesSupportEnabled': true } }
      )
    })

    describe('when mongo update error occurs', function () {
      beforeEach(function (ctx) {
        ctx.db.projects.updateOne = sinon.stub().yields('error')
      })

      it('should be rejected', async function (ctx) {
        await expect(
          ctx.handler.promises.setHistoryRangesSupport(projectId, true)
        ).to.be.rejected
      })
    })
  })

  describe('unsetting the brandVariationId', function () {
    it('should perform and update on mongo', async function (ctx) {
      await ctx.handler.promises.unsetBrandVariationId(projectId)
      const args = ctx.projectModel.updateOne.args[0]
      args[0]._id.should.equal(projectId)
      expect(args[1]).to.deep.equal({ $unset: { brandVariationId: 1 } })
    })

    describe('when mongo update error occurs', function () {
      beforeEach(function (ctx) {
        ctx.projectModel.updateOne = sinon.stub().yields('error')
      })

      it('should be rejected', async function (ctx) {
        expect(ctx.handler.promises.unsetBrandVariationId(projectId)).to.be
          .rejected
      })
    })
  })
})
