const sinon = require('sinon')
const SandboxedModule = require('sandboxed-module')
const path = require('path')
const { ObjectId } = require('mongodb')
const modulePath = path.join(
  __dirname,
  '../../../../app/src/Features/DocumentUpdater/DocumentUpdaterHandler'
)

describe('DocumentUpdaterHandler', function() {
  beforeEach(function() {
    this.project_id = 'project-id-923'
    this.projectHistoryId = 'ol-project-id-1'
    this.doc_id = 'doc-id-394'
    this.lines = ['one', 'two', 'three']
    this.version = 42
    this.user_id = 'mock-user-id-123'
    this.project = { _id: this.project_id }

    this.request = sinon.stub()
    this.projectEntityHandler = {}
    this.settings = {
      apis: {
        documentupdater: {
          url: 'http://document_updater.example.com'
        },
        project_history: {
          url: 'http://project_history.example.com'
        }
      }
    }

    this.callback = sinon.stub()
    this.handler = SandboxedModule.require(modulePath, {
      globals: {
        console: console
      },
      requires: {
        request: {
          defaults: () => {
            return this.request
          }
        },
        'settings-sharelatex': this.settings,
        'logger-sharelatex': { log() {}, error() {}, warn() {} },
        '../Project/ProjectEntityHandler': this.projectEntityHandler,
        '../../models/Project': {
          Project: (this.Project = {})
        },
        '../../Features/Project/ProjectLocator': {},
        '@overleaf/metrics': {
          Timer: class {
            done() {}
          }
        }
      }
    })
  })

  describe('flushProjectToMongo', function() {
    describe('successfully', function() {
      beforeEach(function() {
        this.request.callsArgWith(1, null, { statusCode: 204 }, '')
        this.handler.flushProjectToMongo(this.project_id, this.callback)
      })

      it('should flush the document from the document updater', function() {
        this.request
          .calledWithMatch({
            url: `${this.settings.apis.documentupdater.url}/project/${this.project_id}/flush`,
            method: 'POST'
          })
          .should.equal(true)
      })

      it('should call the callback with no error', function() {
        this.callback.calledWith(null).should.equal(true)
      })
    })

    describe('when the document updater API returns an error', function() {
      beforeEach(function() {
        this.request.callsArgWith(
          1,
          new Error('something went wrong'),
          null,
          null
        )
        this.handler.flushProjectToMongo(this.project_id, this.callback)
      })

      it('should return an error to the callback', function() {
        this.callback
          .calledWith(sinon.match.instanceOf(Error))
          .should.equal(true)
      })
    })

    describe('when the document updater returns a failure error code', function() {
      beforeEach(function() {
        this.request.callsArgWith(1, null, { statusCode: 500 }, '')
        this.handler.flushProjectToMongo(this.project_id, this.callback)
      })

      it('should return the callback with an error', function() {
        this.callback
          .calledWith(
            sinon.match
              .instanceOf(Error)
              .and(
                sinon.match.has(
                  'message',
                  'document updater returned a failure status code: 500'
                )
              )
          )
          .should.equal(true)
      })
    })
  })

  describe('flushProjectToMongoAndDelete', function() {
    describe('successfully', function() {
      beforeEach(function() {
        this.request.callsArgWith(1, null, { statusCode: 204 }, '')
        this.handler.flushProjectToMongoAndDelete(
          this.project_id,
          this.callback
        )
      })

      it('should delete the project from the document updater', function() {
        this.request
          .calledWithMatch({
            url: `${this.settings.apis.documentupdater.url}/project/${this.project_id}`,
            method: 'DELETE'
          })
          .should.equal(true)
      })

      it('should call the callback with no error', function() {
        this.callback.calledWith(null).should.equal(true)
      })
    })

    describe('when the document updater API returns an error', function() {
      beforeEach(function() {
        this.request.callsArgWith(
          1,
          new Error('something went wrong'),
          null,
          null
        )
        this.handler.flushProjectToMongoAndDelete(
          this.project_id,
          this.callback
        )
      })

      it('should return an error to the callback', function() {
        this.callback
          .calledWith(sinon.match.instanceOf(Error))
          .should.equal(true)
      })
    })

    describe('when the document updater returns a failure error code', function() {
      beforeEach(function() {
        this.request.callsArgWith(1, null, { statusCode: 500 }, '')
        this.handler.flushProjectToMongoAndDelete(
          this.project_id,
          this.callback
        )
      })

      it('should return the callback with an error', function() {
        this.callback
          .calledWith(
            sinon.match
              .instanceOf(Error)
              .and(
                sinon.match.has(
                  'message',
                  'document updater returned a failure status code: 500'
                )
              )
          )
          .should.equal(true)
      })
    })
  })

  describe('flushDocToMongo', function() {
    describe('successfully', function() {
      beforeEach(function() {
        this.request.callsArgWith(1, null, { statusCode: 204 }, '')
        this.handler.flushDocToMongo(
          this.project_id,
          this.doc_id,
          this.callback
        )
      })

      it('should flush the document from the document updater', function() {
        this.request
          .calledWithMatch({
            url: `${this.settings.apis.documentupdater.url}/project/${this.project_id}/doc/${this.doc_id}/flush`,
            method: 'POST'
          })
          .should.equal(true)
      })

      it('should call the callback with no error', function() {
        this.callback.calledWith(null).should.equal(true)
      })
    })

    describe('when the document updater API returns an error', function() {
      beforeEach(function() {
        this.request.callsArgWith(
          1,
          new Error('something went wrong'),
          null,
          null
        )
        this.handler.flushDocToMongo(
          this.project_id,
          this.doc_id,
          this.callback
        )
      })

      it('should return an error to the callback', function() {
        this.callback
          .calledWith(sinon.match.instanceOf(Error))
          .should.equal(true)
      })
    })

    describe('when the document updater returns a failure error code', function() {
      beforeEach(function() {
        this.request.callsArgWith(1, null, { statusCode: 500 }, '')
        this.handler.flushDocToMongo(
          this.project_id,
          this.doc_id,
          this.callback
        )
      })

      it('should return the callback with an error', function() {
        this.callback
          .calledWith(
            sinon.match
              .instanceOf(Error)
              .and(
                sinon.match.has(
                  'message',
                  'document updater returned a failure status code: 500'
                )
              )
          )
          .should.equal(true)
      })
    })
  })

  describe('deleteDoc', function() {
    describe('successfully', function() {
      beforeEach(function() {
        this.request.callsArgWith(1, null, { statusCode: 204 }, '')
        this.handler.deleteDoc(this.project_id, this.doc_id, this.callback)
      })

      it('should delete the document from the document updater', function() {
        this.request
          .calledWithMatch({
            url: `${this.settings.apis.documentupdater.url}/project/${this.project_id}/doc/${this.doc_id}`,
            method: 'DELETE'
          })
          .should.equal(true)
      })

      it('should call the callback with no error', function() {
        this.callback.calledWith(null).should.equal(true)
      })
    })

    describe('when the document updater API returns an error', function() {
      beforeEach(function() {
        this.request.callsArgWith(
          1,
          new Error('something went wrong'),
          null,
          null
        )
        this.handler.deleteDoc(this.project_id, this.doc_id, this.callback)
      })

      it('should return an error to the callback', function() {
        this.callback
          .calledWith(sinon.match.instanceOf(Error))
          .should.equal(true)
      })
    })

    describe('when the document updater returns a failure error code', function() {
      beforeEach(function() {
        this.request.callsArgWith(1, null, { statusCode: 500 }, '')
        this.handler.deleteDoc(this.project_id, this.doc_id, this.callback)
      })

      it('should return the callback with an error', function() {
        this.callback
          .calledWith(
            sinon.match
              .instanceOf(Error)
              .and(
                sinon.match.has(
                  'message',
                  'document updater returned a failure status code: 500'
                )
              )
          )
          .should.equal(true)
      })
    })
  })

  describe('setDocument', function() {
    beforeEach(function() {
      this.source = 'dropbox'
    })

    describe('successfully', function() {
      beforeEach(function() {
        this.request.callsArgWith(1, null, { statusCode: 204 }, '')
        this.handler.setDocument(
          this.project_id,
          this.doc_id,
          this.user_id,
          this.lines,
          this.source,
          this.callback
        )
      })

      it('should set the document in the document updater', function() {
        this.request
          .calledWith({
            url: `${this.settings.apis.documentupdater.url}/project/${this.project_id}/doc/${this.doc_id}`,
            json: {
              lines: this.lines,
              source: this.source,
              user_id: this.user_id
            },
            method: 'POST'
          })
          .should.equal(true)
      })

      it('should call the callback with no error', function() {
        this.callback.calledWith(null).should.equal(true)
      })
    })

    describe('when the document updater API returns an error', function() {
      beforeEach(function() {
        this.request.callsArgWith(
          1,
          new Error('something went wrong'),
          null,
          null
        )
        this.handler.setDocument(
          this.project_id,
          this.doc_id,
          this.user_id,
          this.lines,
          this.source,
          this.callback
        )
      })

      it('should return an error to the callback', function() {
        this.callback
          .calledWith(sinon.match.instanceOf(Error))
          .should.equal(true)
      })
    })

    describe('when the document updater returns a failure error code', function() {
      beforeEach(function() {
        this.request.callsArgWith(1, null, { statusCode: 500 }, '')
        this.handler.setDocument(
          this.project_id,
          this.doc_id,
          this.user_id,
          this.lines,
          this.source,
          this.callback
        )
      })

      it('should return the callback with an error', function() {
        this.callback
          .calledWith(
            sinon.match
              .instanceOf(Error)
              .and(
                sinon.match.has(
                  'message',
                  'document updater returned a failure status code: 500'
                )
              )
          )
          .should.equal(true)
      })
    })
  })

  describe('getDocument', function() {
    describe('successfully', function() {
      beforeEach(function() {
        this.body = {
          lines: this.lines,
          version: this.version,
          ops: (this.ops = ['mock-op-1', 'mock-op-2']),
          ranges: (this.ranges = { mock: 'ranges' })
        }
        this.fromVersion = 2
        this.request.callsArgWith(1, null, { statusCode: 200 }, this.body)
        this.handler.getDocument(
          this.project_id,
          this.doc_id,
          this.fromVersion,
          this.callback
        )
      })

      it('should get the document from the document updater', function() {
        this.request
          .calledWith({
            url: `${this.settings.apis.documentupdater.url}/project/${this.project_id}/doc/${this.doc_id}?fromVersion=${this.fromVersion}`,
            method: 'GET',
            json: true
          })
          .should.equal(true)
      })

      it('should call the callback with the lines and version', function() {
        this.callback
          .calledWith(null, this.lines, this.version, this.ranges, this.ops)
          .should.equal(true)
      })
    })

    describe('when the document updater API returns an error', function() {
      beforeEach(function() {
        this.request.callsArgWith(
          1,
          new Error('something went wrong'),
          null,
          null
        )
        this.handler.getDocument(
          this.project_id,
          this.doc_id,
          this.fromVersion,
          this.callback
        )
      })

      it('should return an error to the callback', function() {
        this.callback
          .calledWith(sinon.match.instanceOf(Error))
          .should.equal(true)
      })
    })

    describe('when the document updater returns a failure error code', function() {
      beforeEach(function() {
        this.request.callsArgWith(1, null, { statusCode: 500 }, '')
        this.handler.getDocument(
          this.project_id,
          this.doc_id,
          this.fromVersion,
          this.callback
        )
      })

      it('should return the callback with an error', function() {
        this.callback
          .calledWith(
            sinon.match
              .instanceOf(Error)
              .and(
                sinon.match.has(
                  'message',
                  'document updater returned a failure status code: 500'
                )
              )
          )
          .should.equal(true)
      })
    })
  })

  describe('getProjectDocsIfMatch', function() {
    beforeEach(function() {
      this.project_state_hash = '1234567890abcdef'
    })

    describe('successfully', function() {
      beforeEach(function() {
        this.doc0 = {
          _id: this.doc_id,
          lines: this.lines,
          v: this.version
        }
        this.docs = [this.doc0, this.doc0, this.doc0]
        this.body = JSON.stringify(this.docs)
        this.request.post = sinon
          .stub()
          .callsArgWith(1, null, { statusCode: 200 }, this.body)
        this.handler.getProjectDocsIfMatch(
          this.project_id,
          this.project_state_hash,
          this.callback
        )
      })

      it('should get the documents from the document updater', function() {
        const url = `${this.settings.apis.documentupdater.url}/project/${this.project_id}/get_and_flush_if_old?state=${this.project_state_hash}`
        this.request.post.calledWith(url).should.equal(true)
      })

      it('should call the callback with the documents', function() {
        this.callback.calledWithExactly(null, this.docs).should.equal(true)
      })
    })

    describe('when the document updater API returns an error', function() {
      beforeEach(function() {
        this.request.post = sinon
          .stub()
          .callsArgWith(1, new Error('something went wrong'), null, null)
        this.handler.getProjectDocsIfMatch(
          this.project_id,
          this.project_state_hash,
          this.callback
        )
      })

      it('should return an error to the callback', function() {
        this.callback
          .calledWith(sinon.match.instanceOf(Error))
          .should.equal(true)
      })
    })

    describe('when the document updater returns a conflict error code', function() {
      beforeEach(function() {
        this.request.post = sinon
          .stub()
          .callsArgWith(1, null, { statusCode: 409 }, 'Conflict')
        this.handler.getProjectDocsIfMatch(
          this.project_id,
          this.project_state_hash,
          this.callback
        )
      })

      it('should return the callback with no documents', function() {
        this.callback.alwaysCalledWithExactly().should.equal(true)
      })
    })
  })

  describe('clearProjectState', function() {
    describe('successfully', function() {
      beforeEach(function() {
        this.request.callsArgWith(1, null, { statusCode: 200 })
        this.handler.clearProjectState(this.project_id, this.callback)
      })

      it('should clear the project state from the document updater', function() {
        this.request
          .calledWithMatch({
            url: `${this.settings.apis.documentupdater.url}/project/${this.project_id}/clearState`,
            method: 'POST'
          })
          .should.equal(true)
      })

      it('should call the callback', function() {
        this.callback.calledWith(null).should.equal(true)
      })
    })

    describe('when the document updater API returns an error', function() {
      beforeEach(function() {
        this.request.callsArgWith(
          1,
          new Error('something went wrong'),
          null,
          null
        )
        this.handler.clearProjectState(this.project_id, this.callback)
      })

      it('should return an error to the callback', function() {
        this.callback
          .calledWith(sinon.match.instanceOf(Error))
          .should.equal(true)
      })
    })

    describe('when the document updater returns an error code', function() {
      beforeEach(function() {
        this.request.callsArgWith(1, null, { statusCode: 500 }, null)
        this.handler.clearProjectState(this.project_id, this.callback)
      })

      it('should return the callback with no documents', function() {
        this.callback
          .calledWith(
            sinon.match
              .instanceOf(Error)
              .and(
                sinon.match.has(
                  'message',
                  'document updater returned a failure status code: 500'
                )
              )
          )
          .should.equal(true)
      })
    })
  })

  describe('acceptChanges', function() {
    beforeEach(function() {
      this.change_id = 'mock-change-id-1'
    })

    describe('successfully', function() {
      beforeEach(function() {
        this.request.callsArgWith(1, null, { statusCode: 200 }, this.body)
        this.handler.acceptChanges(
          this.project_id,
          this.doc_id,
          [this.change_id],
          this.callback
        )
      })

      it('should accept the change in the document updater', function() {
        this.request
          .calledWith({
            url: `${this.settings.apis.documentupdater.url}/project/${this.project_id}/doc/${this.doc_id}/change/accept`,
            json: {
              change_ids: [this.change_id]
            },
            method: 'POST'
          })
          .should.equal(true)
      })

      it('should call the callback', function() {
        this.callback.calledWith(null).should.equal(true)
      })
    })

    describe('when the document updater API returns an error', function() {
      beforeEach(function() {
        this.request.callsArgWith(
          1,
          new Error('something went wrong'),
          null,
          null
        )
        this.handler.acceptChanges(
          this.project_id,
          this.doc_id,
          [this.change_id],
          this.callback
        )
      })

      it('should return an error to the callback', function() {
        this.callback
          .calledWith(sinon.match.instanceOf(Error))
          .should.equal(true)
      })
    })

    describe('when the document updater returns a failure error code', function() {
      beforeEach(function() {
        this.request.callsArgWith(1, null, { statusCode: 500 }, '')
        this.handler.acceptChanges(
          this.project_id,
          this.doc_id,
          [this.change_id],
          this.callback
        )
      })

      it('should return the callback with an error', function() {
        this.callback
          .calledWith(
            sinon.match
              .instanceOf(Error)
              .and(
                sinon.match.has(
                  'message',
                  'document updater returned a failure status code: 500'
                )
              )
          )
          .should.equal(true)
      })
    })
  })

  describe('deleteThread', function() {
    beforeEach(function() {
      this.thread_id = 'mock-thread-id-1'
    })

    describe('successfully', function() {
      beforeEach(function() {
        this.request.callsArgWith(1, null, { statusCode: 200 }, this.body)
        this.handler.deleteThread(
          this.project_id,
          this.doc_id,
          this.thread_id,
          this.callback
        )
      })

      it('should delete the thread in the document updater', function() {
        this.request
          .calledWithMatch({
            url: `${this.settings.apis.documentupdater.url}/project/${this.project_id}/doc/${this.doc_id}/comment/${this.thread_id}`,
            method: 'DELETE'
          })
          .should.equal(true)
      })

      it('should call the callback', function() {
        this.callback.calledWith(null).should.equal(true)
      })
    })

    describe('when the document updater API returns an error', function() {
      beforeEach(function() {
        this.request.callsArgWith(
          1,
          new Error('something went wrong'),
          null,
          null
        )
        this.handler.deleteThread(
          this.project_id,
          this.doc_id,
          this.thread_id,
          this.callback
        )
      })

      it('should return an error to the callback', function() {
        this.callback
          .calledWith(sinon.match.instanceOf(Error))
          .should.equal(true)
      })
    })

    describe('when the document updater returns a failure error code', function() {
      beforeEach(function() {
        this.request.callsArgWith(1, null, { statusCode: 500 }, '')
        this.handler.deleteThread(
          this.project_id,
          this.doc_id,
          this.thread_id,
          this.callback
        )
      })

      it('should return the callback with an error', function() {
        this.callback
          .calledWith(
            sinon.match
              .instanceOf(Error)
              .and(
                sinon.match.has(
                  'message',
                  'document updater returned a failure status code: 500'
                )
              )
          )
          .should.equal(true)
      })
    })
  })

  describe('updateProjectStructure ', function() {
    beforeEach(function() {
      this.user_id = 1234
      this.version = 999
    })

    describe('with project history disabled', function() {
      beforeEach(function() {
        this.settings.apis.project_history.sendProjectStructureOps = false
        this.handler.updateProjectStructure(
          this.project_id,
          this.projectHistoryId,
          this.user_id,
          {},
          this.callback
        )
      })

      it('does not make a web request', function() {
        this.request.called.should.equal(false)
      })

      it('calls the callback', function() {
        this.callback.called.should.equal(true)
      })
    })

    describe('with project history enabled', function() {
      beforeEach(function() {
        this.settings.apis.project_history.sendProjectStructureOps = true
        this.url = `${this.settings.apis.documentupdater.url}/project/${this.project_id}`
        this.request.callsArgWith(1, null, { statusCode: 204 }, '')
      })

      describe('when an entity has changed name', function() {
        it('should send the structure update to the document updater', function(done) {
          this.docIdA = new ObjectId()
          this.docIdB = new ObjectId()
          this.changes = {
            oldDocs: [
              { path: '/old_a', doc: { _id: this.docIdA } },
              { path: '/old_b', doc: { _id: this.docIdB } }
            ],
            // create new instances of the same ObjectIds so that == doesn't pass
            newDocs: [
              {
                path: '/old_a',
                doc: { _id: new ObjectId(this.docIdA.toString()) }
              },
              {
                path: '/new_b',
                doc: { _id: new ObjectId(this.docIdB.toString()) }
              }
            ],
            newProject: { version: this.version }
          }

          const updates = [
            {
              type: 'rename-doc',
              id: this.docIdB.toString(),
              pathname: '/old_b',
              newPathname: '/new_b'
            }
          ]

          this.handler.updateProjectStructure(
            this.project_id,
            this.projectHistoryId,
            this.user_id,
            this.changes,
            () => {
              this.request
                .calledWith({
                  url: this.url,
                  method: 'POST',
                  json: {
                    updates,
                    userId: this.user_id,
                    version: this.version,
                    projectHistoryId: this.projectHistoryId
                  }
                })
                .should.equal(true)
              done()
            }
          )
        })
      })

      describe('when a doc has been added', function() {
        it('should send the structure update to the document updater', function(done) {
          this.docId = new ObjectId()
          this.changes = {
            newDocs: [
              { path: '/foo', docLines: 'a\nb', doc: { _id: this.docId } }
            ],
            newProject: { version: this.version }
          }

          const updates = [
            {
              type: 'add-doc',
              id: this.docId.toString(),
              pathname: '/foo',
              docLines: 'a\nb',
              url: undefined,
              hash: undefined
            }
          ]

          this.handler.updateProjectStructure(
            this.project_id,
            this.projectHistoryId,
            this.user_id,
            this.changes,
            () => {
              this.request
                .calledWith({
                  url: this.url,
                  method: 'POST',
                  json: {
                    updates,
                    userId: this.user_id,
                    version: this.version,
                    projectHistoryId: this.projectHistoryId
                  }
                })
                .should.equal(true)
              done()
            }
          )
        })
      })

      describe('when a file has been added', function() {
        it('should send the structure update to the document updater', function(done) {
          this.fileId = new ObjectId()
          this.changes = {
            newFiles: [
              {
                path: '/bar',
                url: 'filestore.example.com/file',
                file: { _id: this.fileId, hash: '12345' }
              }
            ],
            newProject: { version: this.version }
          }

          const updates = [
            {
              type: 'add-file',
              id: this.fileId.toString(),
              pathname: '/bar',
              url: 'filestore.example.com/file',
              docLines: undefined,
              hash: '12345'
            }
          ]

          this.handler.updateProjectStructure(
            this.project_id,
            this.projectHistoryId,
            this.user_id,
            this.changes,
            () => {
              this.request
                .calledWith({
                  url: this.url,
                  method: 'POST',
                  json: {
                    updates,
                    userId: this.user_id,
                    version: this.version,
                    projectHistoryId: this.projectHistoryId
                  }
                })
                .should.equal(true)
              done()
            }
          )
        })
      })

      describe('when an entity has been deleted', function() {
        it('should end the structure update to the document updater', function(done) {
          this.docId = new ObjectId()
          this.changes = {
            oldDocs: [
              { path: '/foo', docLines: 'a\nb', doc: { _id: this.docId } }
            ],
            newProject: { version: this.version }
          }

          const updates = [
            {
              type: 'rename-doc',
              id: this.docId.toString(),
              pathname: '/foo',
              newPathname: ''
            }
          ]

          this.handler.updateProjectStructure(
            this.project_id,
            this.projectHistoryId,
            this.user_id,
            this.changes,
            () => {
              this.request
                .calledWith({
                  url: this.url,
                  method: 'POST',
                  json: {
                    updates,
                    userId: this.user_id,
                    version: this.version,
                    projectHistoryId: this.projectHistoryId
                  }
                })
                .should.equal(true)
              done()
            }
          )
        })
      })

      describe('when a file is converted to a doc', function() {
        it('should send the delete first', function(done) {
          this.docId = new ObjectId()
          this.fileId = new ObjectId()
          this.changes = {
            oldFiles: [
              {
                path: '/foo.doc',
                url: 'filestore.example.com/file',
                file: { _id: this.fileId }
              }
            ],
            newDocs: [
              {
                path: '/foo.doc',
                docLines: 'hello there',
                doc: { _id: this.docId }
              }
            ],
            newProject: { version: this.version }
          }

          const updates = [
            {
              type: 'rename-file',
              id: this.fileId.toString(),
              pathname: '/foo.doc',
              newPathname: ''
            },
            {
              type: 'add-doc',
              id: this.docId.toString(),
              pathname: '/foo.doc',
              docLines: 'hello there',
              url: undefined,
              hash: undefined
            }
          ]

          this.handler.updateProjectStructure(
            this.project_id,
            this.projectHistoryId,
            this.user_id,
            this.changes,
            () => {
              this.request.should.have.been.calledWith({
                url: this.url,
                method: 'POST',
                json: {
                  updates,
                  userId: this.user_id,
                  version: this.version,
                  projectHistoryId: this.projectHistoryId
                }
              })
              done()
            }
          )
        })
      })

      describe('when the project version is missing', function() {
        it('should call the callback with an error', function() {
          this.docId = new ObjectId()
          this.changes = {
            oldDocs: [
              { path: '/foo', docLines: 'a\nb', doc: { _id: this.docId } }
            ]
          }

          this.handler.updateProjectStructure(
            this.project_id,
            this.projectHistoryId,
            this.user_id,
            this.changes,
            this.callback
          )

          this.callback
            .calledWith(sinon.match.instanceOf(Error))
            .should.equal(true)
          const firstCallArgs = this.callback.args[0]
          firstCallArgs[0].message.should.equal(
            'did not receive project version in changes'
          )
        })
      })
    })
  })
})
