const sinon = require('sinon')
const modulePath = '../../../../app/js/ProjectManager.js'
const SandboxedModule = require('sandboxed-module')
const _ = require('lodash')

describe('ProjectManager', function () {
  beforeEach(function () {
    this.RedisManager = {}
    this.ProjectHistoryRedisManager = {
      queueRenameEntity: sinon.stub().yields(),
      queueAddEntity: sinon.stub().yields(),
    }
    this.DocumentManager = {
      renameDocWithLock: sinon.stub().yields(),
    }
    this.HistoryManager = {
      flushProjectChangesAsync: sinon.stub(),
      shouldFlushHistoryOps: sinon.stub().returns(false),
    }
    this.LockManager = {
      getLock: sinon.stub().yields(),
      releaseLock: sinon.stub().yields(),
    }
    this.Metrics = {
      Timer: class Timer {},
    }
    this.Metrics.Timer.prototype.done = sinon.stub()

    this.ProjectManager = SandboxedModule.require(modulePath, {
      requires: {
        './RedisManager': this.RedisManager,
        './ProjectHistoryRedisManager': this.ProjectHistoryRedisManager,
        './DocumentManager': this.DocumentManager,
        './HistoryManager': this.HistoryManager,
        './LockManager': this.LockManager,
        './Metrics': this.Metrics,
      },
    })

    this.project_id = 'project-id-123'
    this.projectHistoryId = 'history-id-123'
    this.user_id = 'user-id-123'
    this.version = 1234567
    this.source = 'editor'
    this.callback = sinon.stub()
  })

  describe('updateProjectWithLocks', function () {
    describe('rename operations', function () {
      beforeEach(function () {
        this.firstDocUpdate = {
          type: 'rename-doc',
          id: 1,
          pathname: 'foo',
          newPathname: 'foo',
        }
        this.secondDocUpdate = {
          type: 'rename-doc',
          id: 2,
          pathname: 'bar',
          newPathname: 'bar2',
        }
        this.firstFileUpdate = {
          type: 'rename-file',
          id: 2,
          pathname: 'bar',
          newPathname: 'bar2',
        }
        this.updates = [
          this.firstDocUpdate,
          this.secondDocUpdate,
          this.firstFileUpdate,
        ]
      })

      describe('successfully', function () {
        beforeEach(function () {
          this.ProjectManager.updateProjectWithLocks(
            this.project_id,
            this.projectHistoryId,
            this.user_id,
            this.updates,
            this.version,
            this.source,
            this.callback
          )
        })

        it('should rename the docs in the updates', function () {
          const firstDocUpdateWithVersion = _.extend({}, this.firstDocUpdate, {
            version: `${this.version}.0`,
          })
          const secondDocUpdateWithVersion = _.extend(
            {},
            this.secondDocUpdate,
            { version: `${this.version}.1` }
          )
          this.DocumentManager.renameDocWithLock
            .calledWith(
              this.project_id,
              this.firstDocUpdate.id,
              this.user_id,
              firstDocUpdateWithVersion,
              this.projectHistoryId
            )
            .should.equal(true)
          this.DocumentManager.renameDocWithLock
            .calledWith(
              this.project_id,
              this.secondDocUpdate.id,
              this.user_id,
              secondDocUpdateWithVersion,
              this.projectHistoryId
            )
            .should.equal(true)
        })

        it('should rename the files in the updates', function () {
          const firstFileUpdateWithVersion = _.extend(
            {},
            this.firstFileUpdate,
            { version: `${this.version}.2` }
          )
          this.ProjectHistoryRedisManager.queueRenameEntity
            .calledWith(
              this.project_id,
              this.projectHistoryId,
              'file',
              this.firstFileUpdate.id,
              this.user_id,
              firstFileUpdateWithVersion,
              this.source
            )
            .should.equal(true)
        })

        it('should not flush the history', function () {
          this.HistoryManager.flushProjectChangesAsync
            .calledWith(this.project_id)
            .should.equal(false)
        })

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

      describe('when renaming a doc fails', function () {
        beforeEach(function () {
          this.error = new Error('error')
          this.DocumentManager.renameDocWithLock.yields(this.error)
          this.ProjectManager.updateProjectWithLocks(
            this.project_id,
            this.projectHistoryId,
            this.user_id,
            this.updates,
            this.version,
            this.source,
            this.callback
          )
        })

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

      describe('when renaming a file fails', function () {
        beforeEach(function () {
          this.error = new Error('error')
          this.ProjectHistoryRedisManager.queueRenameEntity.yields(this.error)
          this.ProjectManager.updateProjectWithLocks(
            this.project_id,
            this.projectHistoryId,
            this.user_id,
            this.updates,
            this.version,
            this.source,
            this.callback
          )
        })

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

      describe('with enough ops to flush', function () {
        beforeEach(function () {
          this.HistoryManager.shouldFlushHistoryOps.returns(true)
          this.ProjectManager.updateProjectWithLocks(
            this.project_id,
            this.projectHistoryId,
            this.user_id,
            this.updates,
            this.version,
            this.source,
            this.callback
          )
        })

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

    describe('add operations', function () {
      beforeEach(function () {
        this.firstDocUpdate = {
          type: 'add-doc',
          id: 1,
          docLines: 'a\nb',
        }
        this.secondDocUpdate = {
          type: 'add-doc',
          id: 2,
          docLines: 'a\nb',
        }
        this.firstFileUpdate = {
          type: 'add-file',
          id: 3,
          url: 'filestore.example.com/2',
        }
        this.secondFileUpdate = {
          type: 'add-file',
          id: 4,
          url: 'filestore.example.com/3',
        }
        this.updates = [
          this.firstDocUpdate,
          this.secondDocUpdate,
          this.firstFileUpdate,
          this.secondFileUpdate,
        ]
      })

      describe('successfully', function () {
        beforeEach(function () {
          this.ProjectManager.updateProjectWithLocks(
            this.project_id,
            this.projectHistoryId,
            this.user_id,
            this.updates,
            this.version,
            this.source,
            this.callback
          )
        })

        it('should add the docs in the updates', function () {
          const firstDocUpdateWithVersion = _.extend({}, this.firstDocUpdate, {
            version: `${this.version}.0`,
          })
          const secondDocUpdateWithVersion = _.extend(
            {},
            this.secondDocUpdate,
            { version: `${this.version}.1` }
          )
          this.ProjectHistoryRedisManager.queueAddEntity
            .getCall(0)
            .calledWith(
              this.project_id,
              this.projectHistoryId,
              'doc',
              this.firstDocUpdate.id,
              this.user_id,
              firstDocUpdateWithVersion,
              this.source
            )
            .should.equal(true)
          this.ProjectHistoryRedisManager.queueAddEntity
            .getCall(1)
            .calledWith(
              this.project_id,
              this.projectHistoryId,
              'doc',
              this.secondDocUpdate.id,
              this.user_id,
              secondDocUpdateWithVersion,
              this.source
            )
            .should.equal(true)
        })

        it('should add the files in the updates', function () {
          const firstFileUpdateWithVersion = _.extend(
            {},
            this.firstFileUpdate,
            { version: `${this.version}.2` }
          )
          const secondFileUpdateWithVersion = _.extend(
            {},
            this.secondFileUpdate,
            { version: `${this.version}.3` }
          )
          this.ProjectHistoryRedisManager.queueAddEntity
            .getCall(2)
            .calledWith(
              this.project_id,
              this.projectHistoryId,
              'file',
              this.firstFileUpdate.id,
              this.user_id,
              firstFileUpdateWithVersion,
              this.source
            )
            .should.equal(true)
          this.ProjectHistoryRedisManager.queueAddEntity
            .getCall(3)
            .calledWith(
              this.project_id,
              this.projectHistoryId,
              'file',
              this.secondFileUpdate.id,
              this.user_id,
              secondFileUpdateWithVersion,
              this.source
            )
            .should.equal(true)
        })

        it('should not flush the history', function () {
          this.HistoryManager.flushProjectChangesAsync
            .calledWith(this.project_id)
            .should.equal(false)
        })

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

      describe('when adding a doc fails', function () {
        beforeEach(function () {
          this.error = new Error('error')
          this.ProjectHistoryRedisManager.queueAddEntity.yields(this.error)
          this.ProjectManager.updateProjectWithLocks(
            this.project_id,
            this.projectHistoryId,
            this.user_id,
            this.updates,
            this.version,
            this.source,
            this.callback
          )
        })

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

      describe('when adding a file fails', function () {
        beforeEach(function () {
          this.error = new Error('error')
          this.ProjectHistoryRedisManager.queueAddEntity.yields(this.error)
          this.ProjectManager.updateProjectWithLocks(
            this.project_id,
            this.projectHistoryId,
            this.user_id,
            this.updates,
            this.version,
            this.source,
            this.callback
          )
        })

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

      describe('with enough ops to flush', function () {
        beforeEach(function () {
          this.HistoryManager.shouldFlushHistoryOps.returns(true)
          this.ProjectManager.updateProjectWithLocks(
            this.project_id,
            this.projectHistoryId,
            this.user_id,
            this.updates,
            this.version,
            this.source,
            this.callback
          )
        })

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

    describe('when given an unknown operation type', function () {
      beforeEach(function () {
        this.updates = [{ type: 'brew-coffee' }]
        this.ProjectManager.updateProjectWithLocks(
          this.project_id,
          this.projectHistoryId,
          this.user_id,
          this.updates,
          this.version,
          this.source,
          this.callback
        )
      })

      it('should call back with an error', function () {
        this.callback.calledWith(sinon.match.instanceOf(Error)).should.be.true
      })
    })
  })
})
