import { vi, expect } from 'vitest'
import sinon from 'sinon'
import { Writable } from 'node:stream'
import mongodb from 'mongodb-legacy'

const { ObjectId } = mongodb

const MODULE_PATH =
  '../../../../app/src/Features/ThirdPartyDataStore/UpdateMerger.mjs'

describe('UpdateMerger :', function () {
  beforeEach(async function (ctx) {
    ctx.projectId = 'project_id_here'
    ctx.userId = 'mock-user-id'
    ctx.randomUUID = 'random-uuid'
    ctx.dumpPath = '/dump'

    ctx.docPath = ctx.newDocPath = '/folder/doc.tex'
    ctx.filePath = ctx.newFilePath = '/folder/file.png'

    ctx.existingDocPath = '/folder/other.tex'
    ctx.existingFilePath = '/folder/fig1.pdf'

    ctx.linkedFileData = { provider: 'url' }

    ctx.existingDocs = [{ path: '/main.tex' }, { path: '/folder/other.tex' }]
    ctx.existingFiles = [{ path: '/figure.pdf' }, { path: '/folder/fig1.pdf' }]

    ctx.fsPath = `${ctx.dumpPath}/${ctx.projectId}_${ctx.randomUUID}_update-merger`
    ctx.fileContents = `\\documentclass{article}
\\usepackage[utf8]{inputenc}

\\title{42}
\\author{Jane Doe}
\\date{June 2011}`
    ctx.docLines = ctx.fileContents.split('\n')
    ctx.source = 'dropbox'
    ctx.updateRequest = new Writable()
    ctx.writeStream = new Writable()

    ctx.fsPromises = {
      unlink: sinon.stub().resolves(),
      readFile: sinon.stub().withArgs(ctx.fsPath).resolves(ctx.fileContents),
      mkdir: sinon.stub().resolves(),
    }

    ctx.fs = {
      createWriteStream: sinon.stub().returns(ctx.writeStream),
    }

    ctx.doc = {
      _id: new ObjectId(),
      rev: 2,
    }

    ctx.file = {
      _id: new ObjectId(),
      rev: 6,
    }

    ctx.folder = {
      _id: new ObjectId(),
    }

    ctx.EditorController = {
      promises: {
        deleteEntityWithPath: sinon.stub().resolves(new ObjectId()),
        upsertDocWithPath: sinon
          .stub()
          .resolves({ doc: ctx.doc, folder: ctx.folder }),
        upsertFileWithPath: sinon
          .stub()
          .resolves({ file: ctx.file, folder: ctx.folder }),
      },
    }

    ctx.FileTypeManager = {
      promises: {
        getType: sinon.stub(),
      },
    }

    ctx.crypto = {
      randomUUID: sinon.stub().returns(ctx.randomUUID),
    }

    ctx.ProjectEntityHandler = {
      promises: {
        getAllEntities: sinon.stub().resolves({
          docs: ctx.existingDocs,
          files: ctx.existingFiles,
        }),
      },
    }

    ctx.Settings = { path: { dumpFolder: ctx.dumpPath } }

    ctx.stream = { pipeline: sinon.stub().resolves() }

    vi.doMock('fs/promises', () => ({
      default: ctx.fsPromises,
    }))

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

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

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

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

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

    vi.doMock('stream/promises', () => ({
      pipeline: ctx.stream.pipeline,
    }))

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

    ctx.UpdateMerger = (await import(MODULE_PATH)).default
  })

  describe('mergeUpdate', function () {
    describe('doc updates for a new doc', function () {
      beforeEach(async function (ctx) {
        ctx.FileTypeManager.promises.getType.resolves({
          binary: false,
          encoding: 'utf-8',
        })
        ctx.mergeUpdateResult = await ctx.UpdateMerger.promises.mergeUpdate(
          ctx.userId,
          ctx.projectId,
          ctx.docPath,
          ctx.updateRequest,
          ctx.source
        )
      })

      it('should look at the file contents', function (ctx) {
        expect(ctx.FileTypeManager.promises.getType).to.have.been.called
      })

      it('should process update as doc', function (ctx) {
        expect(
          ctx.EditorController.promises.upsertDocWithPath
        ).to.have.been.calledWith(
          ctx.projectId,
          ctx.docPath,
          ctx.docLines,
          ctx.source,
          ctx.userId
        )
      })

      it('removes the temp file from disk', function (ctx) {
        expect(ctx.fsPromises.unlink).to.have.been.calledWith(ctx.fsPath)
      })

      it('returns the entity id and rev', function (ctx) {
        expect(ctx.mergeUpdateResult.entityId).to.be.instanceOf(ObjectId)
        expect(ctx.mergeUpdateResult.rev).to.equal(2)
      })
    })

    describe('file updates for a new file ', function () {
      beforeEach(async function (ctx) {
        ctx.FileTypeManager.promises.getType.resolves({ binary: true })
        ctx.mergeUpdateResult = await ctx.UpdateMerger.promises.mergeUpdate(
          ctx.userId,
          ctx.projectId,
          ctx.filePath,
          ctx.updateRequest,
          ctx.source
        )
      })

      it('should look at the file contents', function (ctx) {
        expect(ctx.FileTypeManager.promises.getType).to.have.been.called
      })

      it('should process update as file', function (ctx) {
        expect(
          ctx.EditorController.promises.upsertFileWithPath
        ).to.have.been.calledWith(
          ctx.projectId,
          ctx.filePath,
          ctx.fsPath,
          null,
          ctx.source,
          ctx.userId
        )
      })

      it('removes the temp file from disk', function (ctx) {
        expect(ctx.fsPromises.unlink).to.have.been.calledWith(ctx.fsPath)
      })

      it('returns the entity id and rev', function (ctx) {
        expect(ctx.mergeUpdateResult.entityId).to.be.instanceOf(ObjectId)
        expect(ctx.mergeUpdateResult.rev).to.equal(6)
      })
    })

    describe('doc updates for an existing doc', function () {
      beforeEach(async function (ctx) {
        ctx.FileTypeManager.promises.getType.resolves({
          binary: false,
          encoding: 'utf-8',
        })
        ctx.mergeUpdateResult = await ctx.UpdateMerger.promises.mergeUpdate(
          ctx.userId,
          ctx.projectId,
          ctx.existingDocPath,
          ctx.updateRequest,
          ctx.source
        )
      })

      it('should look at the file contents', function (ctx) {
        expect(ctx.FileTypeManager.promises.getType).to.have.been.called
      })

      it('should process update as doc', function (ctx) {
        expect(
          ctx.EditorController.promises.upsertDocWithPath
        ).to.have.been.calledWith(
          ctx.projectId,
          ctx.existingDocPath,
          ctx.docLines,
          ctx.source,
          ctx.userId
        )
      })

      it('removes the temp file from disk', function (ctx) {
        expect(ctx.fsPromises.unlink).to.have.been.calledWith(ctx.fsPath)
      })

      it('returns the entity id and rev', function (ctx) {
        expect(ctx.mergeUpdateResult.entityId).to.be.instanceOf(ObjectId)
        expect(ctx.mergeUpdateResult.rev).to.equal(2)
      })
    })

    describe('file updates for an existing file', function () {
      beforeEach(async function (ctx) {
        ctx.FileTypeManager.promises.getType.resolves({ binary: true })
        ctx.mergeUpdateResult = await ctx.UpdateMerger.promises.mergeUpdate(
          ctx.userId,
          ctx.projectId,
          ctx.existingFilePath,
          ctx.updateRequest,
          ctx.source
        )
      })

      it('should look at the file contents', function (ctx) {
        expect(ctx.FileTypeManager.promises.getType).to.have.been.called
      })

      it('should process update as file', function (ctx) {
        expect(
          ctx.EditorController.promises.upsertFileWithPath
        ).to.have.been.calledWith(
          ctx.projectId,
          ctx.existingFilePath,
          ctx.fsPath,
          null,
          ctx.source,
          ctx.userId
        )
      })

      it('removes the temp file from disk', function (ctx) {
        expect(ctx.fsPromises.unlink).to.have.been.calledWith(ctx.fsPath)
      })

      it('returns the entity id and rev', function (ctx) {
        expect(ctx.mergeUpdateResult.entityId).to.be.instanceOf(ObjectId)
        expect(ctx.mergeUpdateResult.rev).to.equal(6)
      })
    })
  })

  describe('file updates for an existing doc', function () {
    beforeEach(async function (ctx) {
      ctx.FileTypeManager.promises.getType.resolves({ binary: true })
      ctx.mergeUpdateResult = await ctx.UpdateMerger.promises.mergeUpdate(
        ctx.userId,
        ctx.projectId,
        ctx.existingDocPath,
        ctx.updateRequest,
        ctx.source
      )
    })

    it('should look at the file contents', function (ctx) {
      expect(ctx.FileTypeManager.promises.getType).to.have.been.called
    })

    it('should process update as file', function (ctx) {
      expect(
        ctx.EditorController.promises.upsertFileWithPath
      ).to.have.been.calledWith(
        ctx.projectId,
        ctx.existingDocPath,
        ctx.fsPath,
        null,
        ctx.source,
        ctx.userId
      )
    })

    it('removes the temp file from disk', function (ctx) {
      expect(ctx.fsPromises.unlink).to.have.been.calledWith(ctx.fsPath)
    })

    it('returns the entity id and rev', function (ctx) {
      expect(ctx.mergeUpdateResult.entityId).to.be.instanceOf(ObjectId)
      expect(ctx.mergeUpdateResult.rev).to.equal(6)
    })
  })

  describe('doc updates for an existing file', function () {
    beforeEach(async function (ctx) {
      ctx.FileTypeManager.promises.getType.resolves({ binary: true })
      ctx.mergeUpdateResult = await ctx.UpdateMerger.promises.mergeUpdate(
        ctx.userId,
        ctx.projectId,
        ctx.existingFilePath,
        ctx.updateRequest,
        ctx.source
      )
    })

    it('should look at the file contents', function (ctx) {
      expect(ctx.FileTypeManager.promises.getType).to.have.been.called
    })

    it('should not delete the existing file', function (ctx) {
      expect(ctx.EditorController.promises.deleteEntityWithPath).to.not.have
        .been.called
    })

    it('should process update as file', function (ctx) {
      expect(
        ctx.EditorController.promises.upsertFileWithPath
      ).to.have.been.calledWith(
        ctx.projectId,
        ctx.existingFilePath,
        ctx.fsPath,
        null,
        ctx.source,
        ctx.userId
      )
    })

    it('removes the temp file from disk', function (ctx) {
      expect(ctx.fsPromises.unlink).to.have.been.calledWith(ctx.fsPath)
    })

    it('returns the entity id and rev', function (ctx) {
      expect(ctx.mergeUpdateResult.entityId).to.be.instanceOf(ObjectId)
      expect(ctx.mergeUpdateResult.rev).to.equal(6)
    })
  })

  describe('deleteUpdate', function () {
    beforeEach(async function (ctx) {
      ctx.deleteUpdateResult = await ctx.UpdateMerger.promises.deleteUpdate(
        ctx.userId,
        ctx.projectId,
        ctx.docPath,
        ctx.source
      )
    })

    afterEach(function (ctx) {
      delete ctx.deleteUpdateResult
    })

    it('should delete the entity in the editor controller', function (ctx) {
      expect(
        ctx.EditorController.promises.deleteEntityWithPath
      ).to.have.been.calledWith(
        ctx.projectId,
        ctx.docPath,
        ctx.source,
        ctx.userId
      )
    })

    it('returns the entity id', function (ctx) {
      expect(ctx.deleteUpdateResult).to.be.instanceOf(ObjectId)
    })
  })
})
