const sinon = require('sinon')
const modulePath = '../../../../app/js/HttpController.js'
const SandboxedModule = require('sandboxed-module')
const Errors = require('../../../../app/js/Errors.js')

describe('HttpController', function () {
  beforeEach(function () {
    this.HttpController = SandboxedModule.require(modulePath, {
      requires: {
        './DocumentManager': (this.DocumentManager = {}),
        './HistoryManager': (this.HistoryManager = {
          flushProjectChangesAsync: sinon.stub(),
        }),
        './ProjectHistoryRedisManager': (this.ProjectHistoryRedisManager = {}),
        './ProjectManager': (this.ProjectManager = {}),
        './DeleteQueueManager': (this.DeleteQueueManager = {}),
        './RedisManager': (this.RedisManager = {
          DOC_OPS_TTL: 42,
        }),
        './Metrics': (this.Metrics = {}),
        './Errors': Errors,
        '@overleaf/settings': { max_doc_length: 2 * 1024 * 1024 },
      },
    })
    this.Metrics.Timer = class Timer {}
    this.Metrics.Timer.prototype.done = sinon.stub()

    this.project_id = 'project-id-123'
    this.doc_id = 'doc-id-123'
    this.source = 'editor'
    this.next = sinon.stub()
    this.res = {
      send: sinon.stub(),
      sendStatus: sinon.stub(),
      json: sinon.stub(),
    }
  })

  describe('getDoc', function () {
    beforeEach(function () {
      this.lines = ['one', 'two', 'three']
      this.ops = ['mock-op-1', 'mock-op-2']
      this.version = 42
      this.fromVersion = 42
      this.ranges = { changes: 'mock', comments: 'mock' }
      this.pathname = '/a/b/c'
      this.req = {
        params: {
          project_id: this.project_id,
          doc_id: this.doc_id,
        },
        query: {},
        body: {},
      }
    })

    describe('when the document exists and no recent ops are requested', function () {
      beforeEach(function () {
        this.DocumentManager.getDocAndRecentOpsWithLock = sinon
          .stub()
          .callsArgWith(
            3,
            null,
            this.lines,
            this.version,
            [],
            this.ranges,
            this.pathname
          )
        this.HttpController.getDoc(this.req, this.res, this.next)
      })

      it('should get the doc', function () {
        this.DocumentManager.getDocAndRecentOpsWithLock
          .calledWith(this.project_id, this.doc_id, -1)
          .should.equal(true)
      })

      it('should return the doc as JSON', function () {
        this.res.json
          .calledWith({
            id: this.doc_id,
            lines: this.lines,
            version: this.version,
            ops: [],
            ranges: this.ranges,
            pathname: this.pathname,
            ttlInS: 42,
          })
          .should.equal(true)
      })

      it('should log the request', function () {
        this.logger.debug
          .calledWith(
            { docId: this.doc_id, projectId: this.project_id },
            'getting doc via http'
          )
          .should.equal(true)
      })

      it('should time the request', function () {
        this.Metrics.Timer.prototype.done.called.should.equal(true)
      })
    })

    describe('when recent ops are requested', function () {
      beforeEach(function () {
        this.DocumentManager.getDocAndRecentOpsWithLock = sinon
          .stub()
          .callsArgWith(
            3,
            null,
            this.lines,
            this.version,
            this.ops,
            this.ranges,
            this.pathname
          )
        this.req.query = { fromVersion: `${this.fromVersion}` }
        this.HttpController.getDoc(this.req, this.res, this.next)
      })

      it('should get the doc', function () {
        this.DocumentManager.getDocAndRecentOpsWithLock
          .calledWith(this.project_id, this.doc_id, this.fromVersion)
          .should.equal(true)
      })

      it('should return the doc as JSON', function () {
        this.res.json
          .calledWith({
            id: this.doc_id,
            lines: this.lines,
            version: this.version,
            ops: this.ops,
            ranges: this.ranges,
            pathname: this.pathname,
            ttlInS: 42,
          })
          .should.equal(true)
      })

      it('should log the request', function () {
        this.logger.debug
          .calledWith(
            { docId: this.doc_id, projectId: this.project_id },
            'getting doc via http'
          )
          .should.equal(true)
      })

      it('should time the request', function () {
        this.Metrics.Timer.prototype.done.called.should.equal(true)
      })
    })

    describe('when the document does not exist', function () {
      beforeEach(function () {
        this.DocumentManager.getDocAndRecentOpsWithLock = sinon
          .stub()
          .callsArgWith(3, null, null, null)
        this.HttpController.getDoc(this.req, this.res, this.next)
      })

      it('should call next with NotFoundError', function () {
        this.next
          .calledWith(sinon.match.instanceOf(Errors.NotFoundError))
          .should.equal(true)
      })
    })

    describe('when an errors occurs', function () {
      beforeEach(function () {
        this.DocumentManager.getDocAndRecentOpsWithLock = sinon
          .stub()
          .callsArgWith(3, new Error('oops'), null, null)
        this.HttpController.getDoc(this.req, this.res, this.next)
      })

      it('should call next with the error', function () {
        this.next.calledWith(sinon.match.instanceOf(Error)).should.equal(true)
      })
    })
  })

  describe('getComment', function () {
    beforeEach(function () {
      this.ranges = {
        changes: 'mock',
        comments: [
          {
            id: 'comment-id-1',
          },
          {
            id: 'comment-id-2',
          },
        ],
      }
      this.req = {
        params: {
          project_id: this.project_id,
          doc_id: this.doc_id,
          comment_id: this.comment_id,
        },
        query: {},
        body: {},
      }
    })

    beforeEach(function () {
      this.DocumentManager.getCommentWithLock = sinon
        .stub()
        .callsArgWith(3, null, this.ranges.comments[0])
      this.HttpController.getComment(this.req, this.res, this.next)
    })

    it('should get the comment', function () {
      this.DocumentManager.getCommentWithLock
        .calledWith(this.project_id, this.doc_id, this.comment_id)
        .should.equal(true)
    })

    it('should return the comment as JSON', function () {
      this.res.json
        .calledWith({
          id: 'comment-id-1',
        })
        .should.equal(true)
    })

    it('should log the request', function () {
      this.logger.debug
        .calledWith(
          {
            projectId: this.project_id,
            docId: this.doc_id,
            commentId: this.comment_id,
          },
          'getting comment via http'
        )
        .should.equal(true)
    })
  })

  describe('setDoc', function () {
    beforeEach(function () {
      this.lines = ['one', 'two', 'three']
      this.source = 'dropbox'
      this.user_id = 'user-id-123'
      this.req = {
        headers: {},
        params: {
          project_id: this.project_id,
          doc_id: this.doc_id,
        },
        query: {},
        body: {
          lines: this.lines,
          source: this.source,
          user_id: this.user_id,
          undoing: (this.undoing = true),
        },
      }
    })

    describe('successfully', function () {
      beforeEach(function () {
        this.DocumentManager.setDocWithLock = sinon
          .stub()
          .callsArgWith(7, null, { rev: '123' })
        this.HttpController.setDoc(this.req, this.res, this.next)
      })

      it('should set the doc', function () {
        this.DocumentManager.setDocWithLock
          .calledWith(
            this.project_id,
            this.doc_id,
            this.lines,
            this.source,
            this.user_id,
            this.undoing,
            true
          )
          .should.equal(true)
      })

      it('should return a json response with the document rev from web', function () {
        this.res.json.calledWithMatch({ rev: '123' }).should.equal(true)
      })

      it('should log the request', function () {
        this.logger.debug
          .calledWith(
            {
              docId: this.doc_id,
              projectId: this.project_id,
              lines: this.lines,
              source: this.source,
              userId: this.user_id,
              undoing: this.undoing,
            },
            'setting doc via http'
          )
          .should.equal(true)
      })

      it('should time the request', function () {
        this.Metrics.Timer.prototype.done.called.should.equal(true)
      })
    })

    describe('when an errors occurs', function () {
      beforeEach(function () {
        this.DocumentManager.setDocWithLock = sinon
          .stub()
          .callsArgWith(7, new Error('oops'))
        this.HttpController.setDoc(this.req, this.res, this.next)
      })

      it('should call next with the error', function () {
        this.next.calledWith(sinon.match.instanceOf(Error)).should.equal(true)
      })
    })

    describe('when the payload is too large', function () {
      beforeEach(function () {
        const lines = []
        for (let _ = 0; _ <= 200000; _++) {
          lines.push('test test test')
        }
        this.req.body.lines = lines
        this.DocumentManager.setDocWithLock = sinon.stub().callsArgWith(6)
        this.HttpController.setDoc(this.req, this.res, this.next)
      })

      it('should send back a 406 response', function () {
        this.res.sendStatus.calledWith(406).should.equal(true)
      })

      it('should not call setDocWithLock', function () {
        this.DocumentManager.setDocWithLock.callCount.should.equal(0)
      })
    })
  })

  describe('flushProject', function () {
    beforeEach(function () {
      this.req = {
        params: {
          project_id: this.project_id,
        },
        query: {},
        body: {},
      }
    })

    describe('successfully', function () {
      beforeEach(function () {
        this.ProjectManager.flushProjectWithLocks = sinon.stub().callsArgWith(1)
        this.HttpController.flushProject(this.req, this.res, this.next)
      })

      it('should flush the project', function () {
        this.ProjectManager.flushProjectWithLocks
          .calledWith(this.project_id)
          .should.equal(true)
      })

      it('should return a successful No Content response', function () {
        this.res.sendStatus.calledWith(204).should.equal(true)
      })

      it('should log the request', function () {
        this.logger.debug
          .calledWith(
            { projectId: this.project_id },
            'flushing project via http'
          )
          .should.equal(true)
      })

      it('should time the request', function () {
        this.Metrics.Timer.prototype.done.called.should.equal(true)
      })
    })

    describe('when an errors occurs', function () {
      beforeEach(function () {
        this.ProjectManager.flushProjectWithLocks = sinon
          .stub()
          .callsArgWith(1, new Error('oops'))
        this.HttpController.flushProject(this.req, this.res, this.next)
      })

      it('should call next with the error', function () {
        this.next.calledWith(sinon.match.instanceOf(Error)).should.equal(true)
      })
    })
  })

  describe('flushDocIfLoaded', function () {
    beforeEach(function () {
      this.lines = ['one', 'two', 'three']
      this.version = 42
      this.req = {
        params: {
          project_id: this.project_id,
          doc_id: this.doc_id,
        },
        query: {},
        body: {},
      }
    })

    describe('successfully', function () {
      beforeEach(function () {
        this.DocumentManager.flushDocIfLoadedWithLock = sinon
          .stub()
          .callsArgWith(2)
        this.HttpController.flushDocIfLoaded(this.req, this.res, this.next)
      })

      it('should flush the doc', function () {
        this.DocumentManager.flushDocIfLoadedWithLock
          .calledWith(this.project_id, this.doc_id)
          .should.equal(true)
      })

      it('should return a successful No Content response', function () {
        this.res.sendStatus.calledWith(204).should.equal(true)
      })

      it('should log the request', function () {
        this.logger.debug
          .calledWith(
            { docId: this.doc_id, projectId: this.project_id },
            'flushing doc via http'
          )
          .should.equal(true)
      })

      it('should time the request', function () {
        this.Metrics.Timer.prototype.done.called.should.equal(true)
      })
    })

    describe('when an errors occurs', function () {
      beforeEach(function () {
        this.DocumentManager.flushDocIfLoadedWithLock = sinon
          .stub()
          .callsArgWith(2, new Error('oops'))
        this.HttpController.flushDocIfLoaded(this.req, this.res, this.next)
      })

      it('should call next with the error', function () {
        this.next.calledWith(sinon.match.instanceOf(Error)).should.equal(true)
      })
    })
  })

  describe('deleteDoc', function () {
    beforeEach(function () {
      this.req = {
        params: {
          project_id: this.project_id,
          doc_id: this.doc_id,
        },
        query: {},
        body: {},
      }
    })

    describe('successfully', function () {
      beforeEach(function () {
        this.DocumentManager.flushAndDeleteDocWithLock = sinon
          .stub()
          .callsArgWith(3)
        this.HttpController.deleteDoc(this.req, this.res, this.next)
      })

      it('should flush and delete the doc', function () {
        this.DocumentManager.flushAndDeleteDocWithLock
          .calledWith(this.project_id, this.doc_id, {
            ignoreFlushErrors: false,
          })
          .should.equal(true)
      })

      it('should flush project history', function () {
        this.HistoryManager.flushProjectChangesAsync
          .calledWithExactly(this.project_id)
          .should.equal(true)
      })

      it('should return a successful No Content response', function () {
        this.res.sendStatus.calledWith(204).should.equal(true)
      })

      it('should log the request', function () {
        this.logger.debug
          .calledWith(
            { docId: this.doc_id, projectId: this.project_id },
            'deleting doc via http'
          )
          .should.equal(true)
      })

      it('should time the request', function () {
        this.Metrics.Timer.prototype.done.called.should.equal(true)
      })
    })

    describe('ignoring errors', function () {
      beforeEach(function () {
        this.req.query.ignore_flush_errors = 'true'
        this.DocumentManager.flushAndDeleteDocWithLock = sinon.stub().yields()
        this.HttpController.deleteDoc(this.req, this.res, this.next)
      })

      it('should delete the doc', function () {
        this.DocumentManager.flushAndDeleteDocWithLock
          .calledWith(this.project_id, this.doc_id, { ignoreFlushErrors: true })
          .should.equal(true)
      })

      it('should return a successful No Content response', function () {
        this.res.sendStatus.calledWith(204).should.equal(true)
      })
    })

    describe('when an errors occurs', function () {
      beforeEach(function () {
        this.DocumentManager.flushAndDeleteDocWithLock = sinon
          .stub()
          .callsArgWith(3, new Error('oops'))
        this.HttpController.deleteDoc(this.req, this.res, this.next)
      })

      it('should flush project history', function () {
        this.HistoryManager.flushProjectChangesAsync
          .calledWithExactly(this.project_id)
          .should.equal(true)
      })

      it('should call next with the error', function () {
        this.next.calledWith(sinon.match.instanceOf(Error)).should.equal(true)
      })
    })
  })

  describe('deleteProject', function () {
    beforeEach(function () {
      this.req = {
        params: {
          project_id: this.project_id,
        },
        query: {},
        body: {},
      }
    })

    describe('successfully', function () {
      beforeEach(function () {
        this.ProjectManager.flushAndDeleteProjectWithLocks = sinon
          .stub()
          .callsArgWith(2)
        this.HttpController.deleteProject(this.req, this.res, this.next)
      })

      it('should delete the project', function () {
        this.ProjectManager.flushAndDeleteProjectWithLocks
          .calledWith(this.project_id)
          .should.equal(true)
      })

      it('should return a successful No Content response', function () {
        this.res.sendStatus.calledWith(204).should.equal(true)
      })

      it('should log the request', function () {
        this.logger.debug
          .calledWith(
            { projectId: this.project_id },
            'deleting project via http'
          )
          .should.equal(true)
      })

      it('should time the request', function () {
        this.Metrics.Timer.prototype.done.called.should.equal(true)
      })
    })

    describe('with the background=true option from realtime', function () {
      beforeEach(function () {
        this.ProjectManager.queueFlushAndDeleteProject = sinon
          .stub()
          .callsArgWith(1)
        this.req.query = { background: true, shutdown: true }
        this.HttpController.deleteProject(this.req, this.res, this.next)
      })

      it('should queue the flush and delete', function () {
        this.ProjectManager.queueFlushAndDeleteProject
          .calledWith(this.project_id)
          .should.equal(true)
      })
    })

    describe('when an errors occurs', function () {
      beforeEach(function () {
        this.ProjectManager.flushAndDeleteProjectWithLocks = sinon
          .stub()
          .callsArgWith(2, new Error('oops'))
        this.HttpController.deleteProject(this.req, this.res, this.next)
      })

      it('should call next with the error', function () {
        this.next.calledWith(sinon.match.instanceOf(Error)).should.equal(true)
      })
    })
  })

  describe('acceptChanges', function () {
    beforeEach(function () {
      this.req = {
        params: {
          project_id: this.project_id,
          doc_id: this.doc_id,
          change_id: (this.change_id = 'mock-change-od-1'),
        },
        query: {},
        body: {},
      }
    })

    describe('successfully with a single change', function () {
      beforeEach(function () {
        this.DocumentManager.acceptChangesWithLock = sinon
          .stub()
          .callsArgWith(3)
        this.HttpController.acceptChanges(this.req, this.res, this.next)
      })

      it('should accept the change', function () {
        this.DocumentManager.acceptChangesWithLock
          .calledWith(this.project_id, this.doc_id, [this.change_id])
          .should.equal(true)
      })

      it('should return a successful No Content response', function () {
        this.res.sendStatus.calledWith(204).should.equal(true)
      })

      it('should log the request', function () {
        this.logger.debug
          .calledWith(
            { projectId: this.project_id, docId: this.doc_id },
            'accepting 1 changes via http'
          )
          .should.equal(true)
      })

      it('should time the request', function () {
        this.Metrics.Timer.prototype.done.called.should.equal(true)
      })
    })

    describe('succesfully with with multiple changes', function () {
      beforeEach(function () {
        this.change_ids = [
          'mock-change-od-1',
          'mock-change-od-2',
          'mock-change-od-3',
          'mock-change-od-4',
        ]
        this.req.body = { change_ids: this.change_ids }
        this.DocumentManager.acceptChangesWithLock = sinon
          .stub()
          .callsArgWith(3)
        this.HttpController.acceptChanges(this.req, this.res, this.next)
      })

      it('should accept the changes in the body payload', function () {
        this.DocumentManager.acceptChangesWithLock
          .calledWith(this.project_id, this.doc_id, this.change_ids)
          .should.equal(true)
      })

      it('should log the request with the correct number of changes', function () {
        this.logger.debug
          .calledWith(
            { projectId: this.project_id, docId: this.doc_id },
            `accepting ${this.change_ids.length} changes via http`
          )
          .should.equal(true)
      })
    })

    describe('when an errors occurs', function () {
      beforeEach(function () {
        this.DocumentManager.acceptChangesWithLock = sinon
          .stub()
          .callsArgWith(3, new Error('oops'))
        this.HttpController.acceptChanges(this.req, this.res, this.next)
      })

      it('should call next with the error', function () {
        this.next.calledWith(sinon.match.instanceOf(Error)).should.equal(true)
      })
    })
  })

  describe('resolveComment', function () {
    beforeEach(function () {
      this.user_id = 'user-id-123'
      this.req = {
        params: {
          project_id: this.project_id,
          doc_id: this.doc_id,
          comment_id: (this.comment_id = 'mock-comment-id'),
        },
        query: {},
        body: {
          user_id: this.user_id,
        },
      }
      this.resolved = true
    })

    describe('successfully', function () {
      beforeEach(function (done) {
        this.DocumentManager.updateCommentStateWithLock = sinon
          .stub()
          .callsArgWith(5)

        this.ProjectHistoryRedisManager.queueOps = sinon.stub()
        this.res.sendStatus.callsFake(() => done())
        this.HttpController.resolveComment(this.req, this.res, this.next)
      })

      it('should accept the change', function () {
        this.DocumentManager.updateCommentStateWithLock
          .calledWith(
            this.project_id,
            this.doc_id,
            this.comment_id,
            this.user_id,
            this.resolved
          )
          .should.equal(true)
      })

      it('should return a successful No Content response', function () {
        this.res.sendStatus.calledWith(204).should.equal(true)
      })

      it('should log the request', function () {
        this.logger.debug
          .calledWith(
            {
              projectId: this.project_id,
              docId: this.doc_id,
              commentId: this.comment_id,
            },
            'resolving comment via http'
          )
          .should.equal(true)
      })
    })

    describe('when an errors occurs', function () {
      beforeEach(function () {
        this.DocumentManager.updateCommentStateWithLock = sinon
          .stub()
          .callsArgWith(5, new Error('oops'))
        this.HttpController.resolveComment(this.req, this.res, this.next)
      })

      it('should call next with the error', function () {
        this.next.calledWith(sinon.match.instanceOf(Error)).should.equal(true)
      })
    })
  })

  describe('reopenComment', function () {
    beforeEach(function () {
      this.user_id = 'user-id-123'
      this.req = {
        params: {
          project_id: this.project_id,
          doc_id: this.doc_id,
          comment_id: (this.comment_id = 'mock-comment-id'),
        },
        query: {},
        body: {
          user_id: this.user_id,
        },
      }
      this.resolved = false
    })

    describe('successfully', function () {
      beforeEach(function () {
        this.DocumentManager.updateCommentStateWithLock = sinon
          .stub()
          .callsArgWith(5)

        this.ProjectHistoryRedisManager.queueOps = sinon.stub()
        this.HttpController.reopenComment(this.req, this.res, this.next)
      })

      it('should accept the change', function () {
        this.DocumentManager.updateCommentStateWithLock
          .calledWith(
            this.project_id,
            this.doc_id,
            this.comment_id,
            this.user_id,
            this.resolved
          )
          .should.equal(true)
      })

      it('should return a successful No Content response', function () {
        this.res.sendStatus.calledWith(204).should.equal(true)
      })

      it('should log the request', function () {
        this.logger.debug
          .calledWith(
            {
              projectId: this.project_id,
              docId: this.doc_id,
              commentId: this.comment_id,
            },
            'reopening comment via http'
          )
          .should.equal(true)
      })
    })

    describe('when an errors occurs', function () {
      beforeEach(function () {
        this.DocumentManager.updateCommentStateWithLock = sinon
          .stub()
          .callsArgWith(5, new Error('oops'))
        this.HttpController.reopenComment(this.req, this.res, this.next)
      })

      it('should call next with the error', function () {
        this.next.calledWith(sinon.match.instanceOf(Error)).should.equal(true)
      })
    })
  })

  describe('deleteComment', function () {
    beforeEach(function () {
      this.user_id = 'user-id-123'
      this.req = {
        params: {
          project_id: this.project_id,
          doc_id: this.doc_id,
          comment_id: (this.comment_id = 'mock-comment-id'),
        },
        query: {},
        body: {
          user_id: this.user_id,
        },
      }
    })

    describe('successfully', function () {
      beforeEach(function () {
        this.DocumentManager.deleteCommentWithLock = sinon
          .stub()
          .callsArgWith(4)

        this.ProjectHistoryRedisManager.queueOps = sinon.stub()
        this.HttpController.deleteComment(this.req, this.res, this.next)
      })

      it('should accept the change', function () {
        this.DocumentManager.deleteCommentWithLock
          .calledWith(
            this.project_id,
            this.doc_id,
            this.comment_id,
            this.user_id
          )
          .should.equal(true)
      })

      it('should return a successful No Content response', function () {
        this.res.sendStatus.calledWith(204).should.equal(true)
      })

      it('should log the request', function () {
        this.logger.debug
          .calledWith(
            {
              projectId: this.project_id,
              docId: this.doc_id,
              commentId: this.comment_id,
            },
            'deleting comment via http'
          )
          .should.equal(true)
      })

      it('should time the request', function () {
        this.Metrics.Timer.prototype.done.called.should.equal(true)
      })
    })

    describe('when an errors occurs', function () {
      beforeEach(function () {
        this.DocumentManager.deleteCommentWithLock = sinon
          .stub()
          .callsArgWith(4, new Error('oops'))
        this.HttpController.deleteComment(this.req, this.res, this.next)
      })

      it('should call next with the error', function () {
        this.next.calledWith(sinon.match.instanceOf(Error)).should.equal(true)
      })
    })
  })

  describe('getProjectDocsAndFlushIfOld', function () {
    beforeEach(function () {
      this.state = '01234567890abcdef'
      this.docs = [
        { _id: '1234', lines: 'hello', v: 23 },
        { _id: '4567', lines: 'world', v: 45 },
      ]
      this.req = {
        params: {
          project_id: this.project_id,
        },
        query: {
          state: this.state,
        },
        body: {},
      }
    })

    describe('successfully', function () {
      beforeEach(function () {
        this.ProjectManager.getProjectDocsAndFlushIfOld = sinon
          .stub()
          .callsArgWith(3, null, this.docs)
        this.HttpController.getProjectDocsAndFlushIfOld(
          this.req,
          this.res,
          this.next
        )
      })

      it('should get docs from the project manager', function () {
        this.ProjectManager.getProjectDocsAndFlushIfOld
          .calledWith(this.project_id, this.state, {})
          .should.equal(true)
      })

      it('should return a successful response', function () {
        this.res.send.calledWith(this.docs).should.equal(true)
      })

      it('should log the request', function () {
        this.logger.debug
          .calledWith(
            { projectId: this.project_id, exclude: [] },
            'getting docs via http'
          )
          .should.equal(true)
      })

      it('should log the response', function () {
        this.logger.debug
          .calledWith(
            { projectId: this.project_id, result: ['1234:23', '4567:45'] },
            'got docs via http'
          )
          .should.equal(true)
      })

      it('should time the request', function () {
        this.Metrics.Timer.prototype.done.called.should.equal(true)
      })
    })

    describe('when there is a conflict', function () {
      beforeEach(function () {
        this.ProjectManager.getProjectDocsAndFlushIfOld = sinon
          .stub()
          .callsArgWith(
            3,
            new Errors.ProjectStateChangedError('project state changed')
          )
        this.HttpController.getProjectDocsAndFlushIfOld(
          this.req,
          this.res,
          this.next
        )
      })

      it('should return an HTTP 409 Conflict response', function () {
        this.res.sendStatus.calledWith(409).should.equal(true)
      })
    })

    describe('when an error occurs', function () {
      beforeEach(function () {
        this.ProjectManager.getProjectDocsAndFlushIfOld = sinon
          .stub()
          .callsArgWith(3, new Error('oops'))
        this.HttpController.getProjectDocsAndFlushIfOld(
          this.req,
          this.res,
          this.next
        )
      })

      it('should call next with the error', function () {
        this.next.calledWith(sinon.match.instanceOf(Error)).should.equal(true)
      })
    })
  })

  describe('updateProject', function () {
    beforeEach(function () {
      this.projectHistoryId = 'history-id-123'
      this.userId = 'user-id-123'
      this.updates = [
        {
          type: 'rename-doc',
          id: 1,
          pathname: 'thesis.tex',
          newPathname: 'book.tex',
        },
        { type: 'add-doc', id: 2, pathname: 'article.tex', docLines: 'hello' },
        {
          type: 'rename-file',
          id: 3,
          pathname: 'apple.png',
          newPathname: 'banana.png',
        },
        { type: 'add-file', id: 4, url: 'filestore.example.com/4' },
      ]
      this.version = 1234567
      this.req = {
        query: {},
        body: {
          projectHistoryId: this.projectHistoryId,
          userId: this.userId,
          updates: this.updates,
          version: this.version,
          source: this.source,
        },
        params: {
          project_id: this.project_id,
        },
      }
    })

    describe('successfully', function () {
      beforeEach(function () {
        this.ProjectManager.updateProjectWithLocks = sinon.stub().yields()
        this.HttpController.updateProject(this.req, this.res, this.next)
      })

      it('should accept the change', function () {
        this.ProjectManager.updateProjectWithLocks
          .calledWith(
            this.project_id,
            this.projectHistoryId,
            this.userId,
            this.updates,
            this.version,
            this.source
          )
          .should.equal(true)
      })

      it('should return a successful No Content response', function () {
        this.res.sendStatus.calledWith(204).should.equal(true)
      })

      it('should time the request', function () {
        this.Metrics.Timer.prototype.done.called.should.equal(true)
      })
    })

    describe('when an errors occurs', function () {
      beforeEach(function () {
        this.ProjectManager.updateProjectWithLocks = sinon
          .stub()
          .yields(new Error('oops'))
        this.HttpController.updateProject(this.req, this.res, this.next)
      })

      it('should call next with the error', function () {
        this.next.calledWith(sinon.match.instanceOf(Error)).should.equal(true)
      })
    })
  })

  describe('resyncProjectHistory', function () {
    beforeEach(function () {
      this.projectHistoryId = 'history-id-123'
      this.docs = sinon.stub()
      this.files = sinon.stub()
      this.fileUpdates = sinon.stub()
      this.req = {
        query: {},
        body: {
          projectHistoryId: this.projectHistoryId,
          docs: this.docs,
          files: this.files,
        },
        params: {
          project_id: this.project_id,
        },
      }
    })

    describe('successfully', function () {
      beforeEach(function () {
        this.HistoryManager.resyncProjectHistory = sinon.stub().callsArgWith(5)
        this.HttpController.resyncProjectHistory(this.req, this.res, this.next)
      })

      it('should accept the change', function () {
        this.HistoryManager.resyncProjectHistory
          .calledWith(
            this.project_id,
            this.projectHistoryId,
            this.docs,
            this.files,
            {}
          )
          .should.equal(true)
      })

      it('should return a successful No Content response', function () {
        this.res.sendStatus.should.have.been.calledWith(204)
      })
    })

    describe('when an errors occurs', function () {
      beforeEach(function () {
        this.HistoryManager.resyncProjectHistory = sinon
          .stub()
          .callsArgWith(5, new Error('oops'))
        this.HttpController.resyncProjectHistory(this.req, this.res, this.next)
      })

      it('should call next with the error', function () {
        this.next.calledWith(sinon.match.instanceOf(Error)).should.equal(true)
      })
    })
  })

  describe('appendToDoc', function () {
    beforeEach(function () {
      this.lines = ['one', 'two', 'three']
      this.source = 'dropbox'
      this.user_id = 'user-id-123'
      this.req = {
        headers: {},
        params: {
          project_id: this.project_id,
          doc_id: this.doc_id,
        },
        query: {},
        body: {
          lines: this.lines,
          source: this.source,
          user_id: this.user_id,
          undoing: (this.undoing = true),
        },
      }
    })

    describe('successfully', function () {
      beforeEach(function () {
        this.DocumentManager.appendToDocWithLock = sinon
          .stub()
          .callsArgWith(5, null, { rev: '123' })
        this.HttpController.appendToDoc(this.req, this.res, this.next)
      })

      it('should append to the doc', function () {
        this.DocumentManager.appendToDocWithLock
          .calledWith(
            this.project_id,
            this.doc_id,
            this.lines,
            this.source,
            this.user_id
          )
          .should.equal(true)
      })

      it('should return a json response with the document rev from web', function () {
        this.res.json.calledWithMatch({ rev: '123' }).should.equal(true)
      })

      it('should log the request', function () {
        this.logger.debug
          .calledWith(
            {
              docId: this.doc_id,
              projectId: this.project_id,
              lines: this.lines,
              source: this.source,
              userId: this.user_id,
            },
            'appending to doc via http'
          )
          .should.equal(true)
      })

      it('should time the request', function () {
        this.Metrics.Timer.prototype.done.called.should.equal(true)
      })
    })

    describe('when an errors occurs', function () {
      beforeEach(function () {
        this.DocumentManager.appendToDocWithLock = sinon
          .stub()
          .callsArgWith(5, new Error('oops'))
        this.HttpController.appendToDoc(this.req, this.res, this.next)
      })

      it('should call next with the error', function () {
        this.next.calledWith(sinon.match.instanceOf(Error)).should.equal(true)
      })
    })

    describe('when the payload is too large', function () {
      beforeEach(function () {
        this.DocumentManager.appendToDocWithLock = sinon
          .stub()
          .callsArgWith(5, new Errors.FileTooLargeError())
        this.HttpController.appendToDoc(this.req, this.res, this.next)
      })

      it('should send back a 422 response', function () {
        this.res.sendStatus.calledWith(422).should.equal(true)
      })
    })
  })
})
