const { expect } = require('chai')
const sinon = require('sinon')
const tk = require('timekeeper')
const Errors = require('../../../../app/src/Features/Errors/Errors')
const { ObjectId } = require('mongoose').Types
const SandboxedModule = require('sandboxed-module')
const { Project } = require('../helpers/models/Project')

const MODULE_PATH =
  '../../../../app/src/Features/Project/ProjectEntityMongoUpdateHandler'

describe('ProjectEntityMongoUpdateHandler', function() {
  beforeEach(function() {
    this.doc = {
      _id: ObjectId(),
      name: 'test-doc.txt',
      lines: ['hello', 'world'],
      rev: 1234
    }
    this.docPath = {
      mongo: 'rootFolder.0.docs.0',
      fileSystem: '/test-doc.txt'
    }
    this.file = {
      _id: ObjectId(),
      name: 'something.jpg',
      linkedFileData: { provider: 'url' },
      hash: 'some-hash'
    }
    this.filePath = {
      fileSystem: '/something.png',
      mongo: 'rootFolder.0.fileRefs.0'
    }
    this.subfolder = { _id: ObjectId(), name: 'test-subfolder' }
    this.subfolderPath = {
      fileSystem: '/test-folder/test-subfolder',
      mongo: 'rootFolder.0.folders.0.folders.0'
    }
    this.folder = {
      _id: ObjectId(),
      name: 'test-folder',
      folders: [this.subfolder]
    }
    this.folderPath = {
      fileSystem: '/test-folder',
      mongo: 'rootFolder.0.folders.0'
    }
    this.rootFolder = {
      _id: ObjectId(),
      folders: [this.folder],
      docs: [this.doc],
      fileRefs: [this.file]
    }
    this.rootFolderPath = {
      fileSystem: '/',
      mongo: 'rootFolder.0'
    }
    this.project = {
      _id: ObjectId(),
      name: 'project name',
      rootFolder: [this.rootFolder]
    }

    this.logger = {
      log: sinon.stub(),
      warn: sinon.stub(),
      error: sinon.stub(),
      err() {}
    }
    this.Settings = { maxEntitiesPerProject: 100 }
    this.CooldownManager = {}
    this.LockManager = {
      promises: {
        runWithLock: sinon.spy((namespace, id, runner) => runner())
      }
    }

    this.FolderModel = sinon.stub()
    this.ProjectMock = sinon.mock(Project)
    this.ProjectEntityHandler = {
      promises: {
        getAllEntitiesFromProject: sinon.stub()
      }
    }
    this.ProjectLocator = {
      promises: {
        findElement: sinon.stub().rejects(new Error('not found')),
        findElementByPath: sinon.stub().rejects(new Error('not found'))
      }
    }
    this.ProjectLocator.promises.findElement
      .withArgs({
        project: this.project,
        element_id: this.rootFolder._id,
        type: 'folder'
      })
      .resolves({
        element: this.rootFolder,
        path: this.rootFolderPath
      })
    this.ProjectLocator.promises.findElement
      .withArgs({
        project: this.project,
        element_id: this.folder._id,
        type: 'folder'
      })
      .resolves({
        element: this.folder,
        path: this.folderPath,
        folder: this.rootFolder
      })
    this.ProjectLocator.promises.findElement
      .withArgs({
        project: this.project,
        element_id: this.subfolder._id,
        type: 'folder'
      })
      .resolves({
        element: this.subfolder,
        path: this.subfolderPath,
        folder: this.folder
      })
    this.ProjectLocator.promises.findElement
      .withArgs({
        project: this.project,
        element_id: this.file._id,
        type: 'file'
      })
      .resolves({
        element: this.file,
        path: this.filePath,
        folder: this.rootFolder
      })
    this.ProjectLocator.promises.findElement
      .withArgs({
        project: this.project,
        element_id: this.doc._id,
        type: 'doc'
      })
      .resolves({
        element: this.doc,
        path: this.docPath,
        folder: this.rootFolder
      })
    this.ProjectLocator.promises.findElementByPath
      .withArgs(
        sinon.match({
          project: this.project,
          path: '/'
        })
      )
      .resolves({ element: this.rootFolder, type: 'folder' })
    this.ProjectLocator.promises.findElementByPath
      .withArgs(
        sinon.match({
          project: this.project,
          path: '/test-folder'
        })
      )
      .resolves({ element: this.folder, type: 'folder' })
    this.ProjectLocator.promises.findElementByPath
      .withArgs(
        sinon.match({
          project: this.project,
          path: '/test-folder/test-subfolder'
        })
      )
      .resolves({ element: this.subfolder, type: 'folder' })

    this.ProjectGetter = {
      promises: {
        getProjectWithoutLock: sinon
          .stub()
          .withArgs(this.project._id)
          .resolves(this.project),
        getProjectWithOnlyFolders: sinon.stub().resolves(this.project)
      }
    }

    this.FolderStructureBuilder = {
      buildFolderStructure: sinon.stub()
    }

    this.subject = SandboxedModule.require(MODULE_PATH, {
      globals: {
        console: console
      },
      requires: {
        'logger-sharelatex': this.logger,
        'settings-sharelatex': this.Settings,
        '../Cooldown/CooldownManager': this.CooldownManager,
        '../../models/Folder': { Folder: this.FolderModel },
        '../../infrastructure/LockManager': this.LockManager,
        '../../models/Project': { Project },
        './ProjectEntityHandler': this.ProjectEntityHandler,
        './ProjectLocator': this.ProjectLocator,
        './ProjectGetter': this.ProjectGetter,
        './FolderStructureBuilder': this.FolderStructureBuilder,
        // We need to provide Errors here to make instance check work
        '../Errors/Errors': Errors
      }
    })
  })

  afterEach(function() {
    this.ProjectMock.restore()
  })

  beforeEach(function() {
    tk.freeze(Date.now())
  })

  afterEach(function() {
    tk.reset()
  })

  describe('addDoc', function() {
    beforeEach(async function() {
      const doc = { _id: ObjectId(), name: 'other.txt' }
      this.ProjectMock.expects('findOneAndUpdate')
        .withArgs(
          { _id: this.project._id },
          {
            $push: { 'rootFolder.0.folders.0.docs': doc },
            $inc: { version: 1 }
          }
        )
        .chain('exec')
        .resolves(this.project)
      this.result = await this.subject.promises.addDoc(
        this.project._id,
        this.folder._id,
        doc
      )
    })

    it('adds the document in Mongo', function() {
      this.ProjectMock.verify()
    })

    it('returns path info and the project', function() {
      expect(this.result).to.deep.equal({
        result: {
          path: {
            mongo: 'rootFolder.0.folders.0',
            fileSystem: '/test-folder/other.txt'
          }
        },
        project: this.project
      })
    })
  })

  describe('addFile', function() {
    beforeEach(function() {
      this.newFile = { _id: ObjectId(), name: 'picture.jpg' }
      this.ProjectMock.expects('findOneAndUpdate')
        .withArgs(
          { _id: this.project._id },
          {
            $push: { 'rootFolder.0.folders.0.fileRefs': this.newFile },
            $inc: { version: 1 }
          }
        )
        .chain('exec')
        .resolves(this.project)
    })

    describe('happy path', function() {
      beforeEach(async function() {
        this.result = await this.subject.promises.addFile(
          this.project._id,
          this.folder._id,
          this.newFile
        )
      })

      it('adds the file in Mongo', function() {
        this.ProjectMock.verify()
      })

      it('returns path info and the project', function() {
        expect(this.result).to.deep.equal({
          result: {
            path: {
              mongo: 'rootFolder.0.folders.0',
              fileSystem: '/test-folder/picture.jpg'
            }
          },
          project: this.project
        })
      })
    })

    describe('when entity limit is reached', function() {
      beforeEach(function() {
        this.savedMaxEntities = this.Settings.maxEntitiesPerProject
        this.Settings.maxEntitiesPerProject = 3
      })

      afterEach(function() {
        this.Settings.maxEntitiesPerProject = this.savedMaxEntities
      })

      it('should throw an error', async function() {
        await expect(
          this.subject.promises.addFile(
            this.project._id,
            this.folder._id,
            this.newFile
          )
        ).to.be.rejected
      })
    })
  })

  describe('addFolder', function() {
    beforeEach(async function() {
      const folderName = 'New folder'
      this.FolderModel.withArgs({ name: folderName }).returns({
        _id: ObjectId(),
        name: folderName
      })
      this.ProjectMock.expects('findOneAndUpdate')
        .withArgs(
          { _id: this.project._id },
          {
            $push: {
              'rootFolder.0.folders.0.folders': sinon.match({
                name: folderName
              })
            },
            $inc: { version: 1 }
          }
        )
        .chain('exec')
        .resolves(this.project)
      await this.subject.promises.addFolder(
        this.project._id,
        this.folder._id,
        folderName
      )
    })

    it('adds the folder in Mongo', function() {
      this.ProjectMock.verify()
    })
  })

  describe('replaceFileWithNew', function() {
    beforeEach(async function() {
      const newFile = {
        _id: ObjectId(),
        name: 'some-other-file.png',
        linkedFileData: { some: 'data' },
        hash: 'some-hash'
      }
      // Add a deleted file record
      this.ProjectMock.expects('updateOne')
        .withArgs(
          { _id: this.project._id },
          {
            $push: {
              deletedFiles: {
                _id: this.file._id,
                name: this.file.name,
                linkedFileData: this.file.linkedFileData,
                hash: this.file.hash,
                deletedAt: sinon.match.date
              }
            }
          }
        )
        .chain('exec')
        .resolves()
      // Update the file in place
      this.ProjectMock.expects('findOneAndUpdate')
        .withArgs(
          { _id: this.project._id },
          {
            $set: {
              'rootFolder.0.fileRefs.0._id': newFile._id,
              'rootFolder.0.fileRefs.0.created': sinon.match.date,
              'rootFolder.0.fileRefs.0.linkedFileData': newFile.linkedFileData,
              'rootFolder.0.fileRefs.0.hash': newFile.hash
            },
            $inc: {
              version: 1,
              'rootFolder.0.fileRefs.0.rev': 1
            }
          }
        )
        .chain('exec')
        .resolves(this.project)
      await this.subject.promises.replaceFileWithNew(
        this.project._id,
        this.file._id,
        newFile
      )
    })

    it('updates the database', function() {
      this.ProjectMock.verify()
    })
  })

  describe('mkdirp', function() {
    describe('when the path is just a slash', function() {
      beforeEach(async function() {
        this.result = await this.subject.promises.mkdirp(this.project._id, '/')
      })

      it('should return the root folder', function() {
        expect(this.result.folder).to.deep.equal(this.rootFolder)
      })

      it('should not report a parent folder', function() {
        expect(this.result.folder.parentFolder_id).not.to.exist
      })

      it('should not return new folders', function() {
        expect(this.result.newFolders).to.have.length(0)
      })
    })

    describe('when the folder already exists', function() {
      beforeEach(async function() {
        this.result = await this.subject.promises.mkdirp(
          this.project._id,
          '/test-folder'
        )
      })

      it('should return the existing folder', function() {
        expect(this.result.folder).to.deep.equal(this.folder)
      })

      it('should report the parent folder', function() {
        expect(this.result.folder.parentFolder_id).not.equal(
          this.rootFolder._id
        )
      })

      it('should not return new folders', function() {
        expect(this.result.newFolders).to.have.length(0)
      })
    })

    describe('when the path is a new folder at the top level', function() {
      beforeEach(async function() {
        this.newFolder = { _id: ObjectId(), name: 'new-folder' }
        this.FolderModel.returns(this.newFolder)
        this.exactCaseMatch = false
        this.ProjectMock.expects('findOneAndUpdate')
          .withArgs(
            { _id: this.project._id },
            {
              $push: { 'rootFolder.0.folders': this.newFolder },
              $inc: { version: 1 }
            }
          )
          .chain('exec')
          .resolves(this.project)
        this.result = await this.subject.promises.mkdirp(
          this.project._id,
          '/new-folder/',
          { exactCaseMatch: this.exactCaseMatch }
        )
      })
      it('should update the database', function() {
        this.ProjectMock.verify()
      })

      it('should make just one folder', function() {
        expect(this.result.newFolders).to.have.length(1)
      })

      it('should return the new folder', function() {
        expect(this.result.folder.name).to.equal('new-folder')
      })

      it('should return the parent folder', function() {
        expect(this.result.folder.parentFolder_id).to.equal(this.rootFolder._id)
      })

      it('should pass the exactCaseMatch option to ProjectLocator', function() {
        expect(
          this.ProjectLocator.promises.findElementByPath
        ).to.have.been.calledWithMatch({ exactCaseMatch: this.exactCaseMatch })
      })
    })

    describe('adding a subfolder', function() {
      beforeEach(async function() {
        this.newFolder = { _id: ObjectId(), name: 'new-folder' }
        this.FolderModel.returns(this.newFolder)
        this.ProjectMock.expects('findOneAndUpdate')
          .withArgs(
            { _id: this.project._id },
            {
              $push: {
                'rootFolder.0.folders.0.folders': sinon.match({
                  name: 'new-folder'
                })
              },
              $inc: { version: 1 }
            }
          )
          .chain('exec')
          .resolves(this.project)
        this.result = await this.subject.promises.mkdirp(
          this.project._id,
          '/test-folder/new-folder'
        )
      })

      it('should update the database', function() {
        this.ProjectMock.verify()
      })

      it('should create one folder', function() {
        expect(this.result.newFolders).to.have.length(1)
      })

      it('should return the new folder', function() {
        expect(this.result.folder.name).to.equal('new-folder')
      })

      it('should return the parent folder', function() {
        expect(this.result.folder.parentFolder_id).to.equal(this.folder._id)
      })
    })

    describe('when mutliple folders are missing', async function() {
      beforeEach(function() {
        this.folder1 = { _id: ObjectId(), name: 'folder1' }
        this.folder1Path = {
          fileSystem: '/test-folder/folder1',
          mongo: 'rootFolder.0.folders.0.folders.0'
        }
        this.folder2 = { _id: ObjectId(), name: 'folder2' }
        this.folder2Path = {
          fileSystem: '/test-folder/folder1/folder2',
          mongo: 'rootFolder.0.folders.0.folders.0.folders.0'
        }
        this.FolderModel.onFirstCall().returns(this.folder1)
        this.FolderModel.onSecondCall().returns(this.folder2)
        this.ProjectLocator.promises.findElement
          .withArgs({
            project: this.project,
            element_id: this.folder1._id,
            type: 'folder'
          })
          .resolves({
            element: this.folder1,
            path: this.folder1Path
          })
        this.ProjectLocator.promises.findElement
          .withArgs({
            project: this.project,
            element_id: this.folder2._id,
            type: 'folder'
          })
          .resolves({
            element: this.folder2,
            path: this.folder2Path
          })
        this.ProjectMock.expects('findOneAndUpdate')
          .withArgs(
            { _id: this.project._id },
            {
              $push: {
                'rootFolder.0.folders.0.folders': sinon.match({
                  name: 'folder1'
                })
              },
              $inc: { version: 1 }
            }
          )
          .chain('exec')
          .resolves(this.project)
        this.ProjectMock.expects('findOneAndUpdate')
          .withArgs(
            { _id: this.project._id },
            {
              $push: {
                'rootFolder.0.folders.0.folders.0.folders': sinon.match({
                  name: 'folder2'
                })
              },
              $inc: { version: 1 }
            }
          )
          .chain('exec')
          .resolves(this.project)
      })
      ;[
        {
          description: 'without a trailing slash',
          path: '/test-folder/folder1/folder2'
        },
        {
          description: 'with a trailing slash',
          path: '/test-folder/folder1/folder2/'
        }
      ].forEach(({ description, path }) => {
        describe(description, function() {
          beforeEach(async function() {
            this.result = await this.subject.promises.mkdirp(
              this.project._id,
              path
            )
          })

          it('should update the database', function() {
            this.ProjectMock.verify()
          })

          it('should add multiple folders', function() {
            const newFolders = this.result.newFolders
            expect(newFolders).to.have.length(2)
            expect(newFolders[0].name).to.equal('folder1')
            expect(newFolders[1].name).to.equal('folder2')
          })

          it('should return the last folder', function() {
            expect(this.result.folder.name).to.equal('folder2')
          })

          it('should return the parent folder', function() {
            expect(this.result.folder.parentFolder_id).to.equal(
              this.folder1._id
            )
          })
        })
      })
    })
  })

  describe('moveEntity', function() {
    describe('moving a doc into a different folder', function() {
      beforeEach(async function() {
        this.pathAfterMove = {
          fileSystem: '/somewhere/else.txt'
        }
        this.oldDocs = ['old-doc']
        this.oldFiles = ['old-file']
        this.newDocs = ['new-doc']
        this.newFiles = ['new-file']

        this.ProjectEntityHandler.promises.getAllEntitiesFromProject
          .onFirstCall()
          .resolves({ docs: this.oldDocs, files: this.oldFiles })
        this.ProjectEntityHandler.promises.getAllEntitiesFromProject
          .onSecondCall()
          .resolves({ docs: this.newDocs, files: this.newFiles })

        this.ProjectMock.expects('findOneAndUpdate')
          .withArgs(
            { _id: this.project._id },
            {
              $push: { 'rootFolder.0.folders.0.docs': this.doc },
              $inc: { version: 1 }
            }
          )
          .chain('exec')
          .resolves(this.project)
        this.ProjectMock.expects('findOneAndUpdate')
          .withArgs(
            { _id: this.project._id },
            {
              $pull: { 'rootFolder.0.docs': { _id: this.doc._id } },
              $inc: { version: 1 }
            }
          )
          .chain('exec')
          .resolves(this.project)
        this.result = await this.subject.promises.moveEntity(
          this.project._id,
          this.doc._id,
          this.folder._id,
          'doc'
        )
      })

      it('should update the database', function() {
        this.ProjectMock.verify()
      })

      it('should report what changed', function() {
        expect(this.result).to.deep.equal({
          project: this.project,
          startPath: '/test-doc.txt',
          endPath: '/test-folder/test-doc.txt',
          rev: this.doc.rev,
          changes: {
            oldDocs: this.oldDocs,
            newDocs: this.newDocs,
            oldFiles: this.oldFiles,
            newFiles: this.newFiles,
            newProject: this.project
          }
        })
      })
    })

    describe('when moving a folder inside itself', function() {
      it('throws an error', async function() {
        await expect(
          this.subject.promises.moveEntity(
            this.project._id,
            this.folder._id,
            this.folder._id,
            'folder'
          )
        ).to.be.rejectedWith(Errors.InvalidNameError)
      })
    })

    describe('when moving a folder to a subfolder of itself', function() {
      it('throws an error', async function() {
        await expect(
          this.subject.promises.moveEntity(
            this.project._id,
            this.folder._id,
            this.subfolder._id,
            'folder'
          )
        ).to.be.rejectedWith(Errors.InvalidNameError)
      })
    })
  })

  describe('deleteEntity', function() {
    beforeEach(async function() {
      this.ProjectMock.expects('findOneAndUpdate')
        .withArgs(
          { _id: this.project._id },
          {
            $pull: { 'rootFolder.0.docs': { _id: this.doc._id } },
            $inc: { version: 1 }
          }
        )
        .chain('exec')
        .resolves(this.project)
      await this.subject.promises.deleteEntity(
        this.project._id,
        this.doc._id,
        'doc'
      )
    })

    it('should update the database', function() {
      this.ProjectMock.verify()
    })
  })

  describe('renameEntity', function() {
    describe('happy path', function() {
      beforeEach(async function() {
        this.newName = 'new.tex'
        this.oldDocs = ['old-doc']
        this.oldFiles = ['old-file']
        this.newDocs = ['new-doc']
        this.newFiles = ['new-file']

        this.ProjectEntityHandler.promises.getAllEntitiesFromProject
          .onFirstCall()
          .resolves({ docs: this.oldDocs, files: this.oldFiles })
        this.ProjectEntityHandler.promises.getAllEntitiesFromProject
          .onSecondCall()
          .resolves({ docs: this.newDocs, files: this.newFiles })

        this.ProjectMock.expects('findOneAndUpdate')
          .withArgs(
            { _id: this.project._id },
            {
              $set: { 'rootFolder.0.docs.0.name': this.newName },
              $inc: { version: 1 }
            }
          )
          .chain('exec')
          .resolves(this.project)
        this.result = await this.subject.promises.renameEntity(
          this.project._id,
          this.doc._id,
          'doc',
          this.newName
        )
      })

      it('should update the database', function() {
        this.ProjectMock.verify()
      })

      it('returns info', function() {
        expect(this.result).to.deep.equal({
          project: this.project,
          startPath: '/test-doc.txt',
          endPath: '/new.tex',
          rev: this.doc.rev,
          changes: {
            oldDocs: this.oldDocs,
            newDocs: this.newDocs,
            oldFiles: this.oldFiles,
            newFiles: this.newFiles,
            newProject: this.project
          }
        })
      })
    })

    describe('name already exists', function() {
      it('should throw an error', async function() {
        await expect(
          this.subject.promises.renameEntity(
            this.project._id,
            this.doc._id,
            'doc',
            this.folder.name
          )
        ).to.be.rejectedWith(Errors.InvalidNameError)
      })
    })
  })

  describe('_putElement', function() {
    describe('updating the project', function() {
      describe('when the parent folder is given', function() {
        beforeEach(function() {
          this.newFile = { _id: ObjectId(), name: 'new file.png' }
          this.ProjectMock.expects('findOneAndUpdate')
            .withArgs(
              { _id: this.project._id },
              {
                $push: { 'rootFolder.0.folders.0.fileRefs': this.newFile },
                $inc: { version: 1 }
              }
            )
            .chain('exec')
            .resolves(this.project)
        })

        it('should update the database', async function() {
          await this.subject.promises._putElement(
            this.project,
            this.folder._id,
            this.newFile,
            'files'
          )
          this.ProjectMock.verify()
        })

        it('should add an s onto the type if not included', async function() {
          await this.subject.promises._putElement(
            this.project,
            this.folder._id,
            this.newFile,
            'file'
          )
          this.ProjectMock.verify()
        })
      })

      describe('error cases', function() {
        it('should throw an error if element is null', async function() {
          await expect(
            this.subject.promises._putElement(
              this.project,
              this.folder._id,
              null,
              'file'
            )
          ).to.be.rejected
        })

        it('should error if the element has no _id', async function() {
          const file = { name: 'something' }
          await expect(
            this.subject.promises._putElement(
              this.project,
              this.folder._id,
              file,
              'file'
            )
          ).to.be.rejected
        })

        it('should error if element name contains invalid characters', async function() {
          const file = { _id: ObjectId(), name: 'something*bad' }
          await expect(
            this.subject.promises._putElement(
              this.project,
              this.folder._id,
              file,
              'file'
            )
          ).to.be.rejected
        })

        it('should error if element name is too long', async function() {
          const file = {
            _id: ObjectId(),
            name: 'long-'.repeat(1000) + 'something'
          }
          await expect(
            this.subject.promises._putElement(
              this.project,
              this.folder._id,
              file,
              'file'
            )
          ).to.be.rejectedWith(Errors.InvalidNameError)
        })

        it('should error if the folder name is too long', async function() {
          const file = {
            _id: ObjectId(),
            name: 'something'
          }
          this.ProjectLocator.promises.findElement
            .withArgs({
              project: this.project,
              element_id: this.folder._id,
              type: 'folder'
            })
            .resolves({
              element: this.folder,
              path: { fileSystem: 'subdir/'.repeat(1000) + 'foo' }
            })
          await expect(
            this.subject.promises._putElement(
              this.project,
              this.folder._id,
              file,
              'file'
            )
          ).to.be.rejectedWith(Errors.InvalidNameError)
        })
        ;['file', 'doc', 'folder'].forEach(entityType => {
          it(`should error if a ${entityType} already exists with the same name`, async function() {
            const file = {
              _id: ObjectId(),
              name: this[entityType].name
            }
            await expect(
              this.subject.promises._putElement(
                this.project,
                null,
                file,
                'file'
              )
            ).to.be.rejectedWith(Errors.InvalidNameError)
          })
        })
      })
    })

    describe('when the parent folder is not given', function() {
      it('should default to root folder insert', async function() {
        this.newFile = { _id: ObjectId(), name: 'new file.png' }
        this.ProjectMock.expects('findOneAndUpdate')
          .withArgs(
            { _id: this.project._id },
            {
              $push: { 'rootFolder.0.fileRefs': this.newFile },
              $inc: { version: 1 }
            }
          )
          .chain('exec')
          .resolves(this.project)
        await this.subject.promises._putElement(
          this.project,
          this.rootFolder._id,
          this.newFile,
          'file'
        )
      })
    })
  })

  describe('_insertDeletedDocReference', function() {
    beforeEach(async function() {
      this.ProjectMock.expects('updateOne')
        .withArgs(
          { _id: this.project._id },
          {
            $push: {
              deletedDocs: {
                _id: this.doc._id,
                name: this.doc.name,
                deletedAt: sinon.match.date
              }
            }
          }
        )
        .chain('exec')
        .resolves()
      await this.subject.promises._insertDeletedDocReference(
        this.project._id,
        this.doc
      )
    })

    it('should update the database', function() {
      this.ProjectMock.verify()
    })
  })

  describe('_insertDeletedFileReference', function() {
    beforeEach(async function() {
      this.ProjectMock.expects('updateOne')
        .withArgs(
          { _id: this.project._id },
          {
            $push: {
              deletedFiles: {
                _id: this.file._id,
                name: this.file.name,
                linkedFileData: this.file.linkedFileData,
                hash: this.file.hash,
                deletedAt: sinon.match.date
              }
            }
          }
        )
        .chain('exec')
        .resolves()
      await this.subject.promises._insertDeletedFileReference(
        this.project._id,
        this.file
      )
    })

    it('should update the database', function() {
      this.ProjectMock.verify()
    })
  })

  describe('createNewFolderStructure', function() {
    beforeEach(function() {
      this.mockRootFolder = 'MOCK_ROOT_FOLDER'
      this.docUploads = ['MOCK_DOC_UPLOAD']
      this.fileUploads = ['MOCK_FILE_UPLOAD']
      this.FolderStructureBuilder.buildFolderStructure
        .withArgs(this.docUploads, this.fileUploads)
        .returns(this.mockRootFolder)
      this.updateExpectation = this.ProjectMock.expects('updateOne')
        .withArgs(
          {
            _id: this.project._id,
            'rootFolder.0.folders.0': { $exists: false },
            'rootFolder.0.docs.0': { $exists: false },
            'rootFolder.0.files.0': { $exists: false }
          },
          { $set: { rootFolder: [this.mockRootFolder] }, $inc: { version: 1 } }
        )
        .chain('exec')
    })

    describe('happy path', function() {
      beforeEach(async function() {
        this.updateExpectation.resolves({ n: 1 })
        await this.subject.promises.createNewFolderStructure(
          this.project._id,
          this.docUploads,
          this.fileUploads
        )
      })

      it('updates the database', function() {
        this.ProjectMock.verify()
      })
    })

    describe("when the update doesn't find a matching document", function() {
      beforeEach(async function() {
        this.updateExpectation.resolves({ n: 0 })
      })

      it('throws an error', async function() {
        await expect(
          this.subject.promises.createNewFolderStructure(
            this.project._id,
            this.docUploads,
            this.fileUploads
          )
        ).to.be.rejected
      })
    })
  })
})
