const sinon = require('sinon')
const modulePath =
  '../../../../app/src/Features/Documents/DocumentController.js'
const SandboxedModule = require('sandboxed-module')
const MockRequest = require('../helpers/MockRequest')
const MockResponse = require('../helpers/MockResponse')
const Errors = require('../../../../app/src/Features/Errors/Errors')

describe('DocumentController', function () {
  beforeEach(function () {
    this.DocumentController = SandboxedModule.require(modulePath, {
      requires: {
        '../Project/ProjectGetter': (this.ProjectGetter = {}),
        '../Project/ProjectLocator': (this.ProjectLocator = {}),
        '../Project/ProjectEntityHandler': (this.ProjectEntityHandler = {}),
        '../Project/ProjectEntityUpdateHandler': (this.ProjectEntityUpdateHandler = {}),
      },
    })
    this.res = new MockResponse()
    this.req = new MockRequest()
    this.next = sinon.stub()
    this.project_id = 'project-id-123'
    this.doc_id = 'doc-id-123'
    this.doc_lines = ['one', 'two', 'three']
    this.version = 42
    this.ranges = { mock: 'ranges' }
    this.pathname = '/a/b/c/file.tex'
    this.lastUpdatedAt = new Date().getTime()
    this.lastUpdatedBy = 'fake-last-updater-id'
    this.rev = 5
  })

  describe('getDocument', function () {
    beforeEach(function () {
      this.req.params = {
        Project_id: this.project_id,
        doc_id: this.doc_id,
      }
    })

    describe('when the project exists without project history enabled', function () {
      beforeEach(function () {
        this.project = { _id: this.project_id }
        this.ProjectGetter.getProject = sinon
          .stub()
          .callsArgWith(2, null, this.project)
      })

      describe('when the document exists', function () {
        beforeEach(function () {
          this.doc = { _id: this.doc_id }
          this.ProjectLocator.findElement = sinon
            .stub()
            .callsArgWith(1, null, this.doc, { fileSystem: this.pathname })
          this.ProjectEntityHandler.getDoc = sinon
            .stub()
            .yields(null, this.doc_lines, this.rev, this.version, this.ranges)
          this.DocumentController.getDocument(this.req, this.res, this.next)
        })

        it('should get the project', function () {
          this.ProjectGetter.getProject
            .calledWith(this.project_id, { rootFolder: true, overleaf: true })
            .should.equal(true)
        })

        it('should get the pathname of the document', function () {
          this.ProjectLocator.findElement
            .calledWith({
              project: this.project,
              element_id: this.doc_id,
              type: 'doc',
            })
            .should.equal(true)
        })

        it('should get the document content', function () {
          this.ProjectEntityHandler.getDoc
            .calledWith(this.project_id, this.doc_id)
            .should.equal(true)
        })

        it('should return the document data to the client as JSON', function () {
          this.res.type.should.equal('application/json')
          this.res.body.should.equal(
            JSON.stringify({
              lines: this.doc_lines,
              version: this.version,
              ranges: this.ranges,
              pathname: this.pathname,
            })
          )
        })
      })

      describe("when the document doesn't exist", function () {
        beforeEach(function () {
          this.ProjectLocator.findElement = sinon
            .stub()
            .callsArgWith(1, new Errors.NotFoundError('not found'))
          this.DocumentController.getDocument(this.req, this.res, this.next)
        })

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

    describe('when project exists with project history enabled', function () {
      beforeEach(function () {
        this.doc = { _id: this.doc_id }
        this.projectHistoryId = 1234
        this.projectHistoryDisplay = true
        this.projectHistoryType = 'project-history'
        this.project = {
          _id: this.project_id,
          overleaf: {
            history: {
              id: this.projectHistoryId,
              display: this.projectHistoryDisplay,
            },
          },
        }
        this.ProjectGetter.getProject = sinon
          .stub()
          .callsArgWith(2, null, this.project)
        this.ProjectLocator.findElement = sinon
          .stub()
          .callsArgWith(1, null, this.doc, { fileSystem: this.pathname })
        this.ProjectEntityHandler.getDoc = sinon
          .stub()
          .yields(null, this.doc_lines, this.rev, this.version, this.ranges)
        this.DocumentController.getDocument(this.req, this.res, this.next)
      })

      it('should return the history id and display setting to the client as JSON', function () {
        this.res.type.should.equal('application/json')
        this.res.body.should.equal(
          JSON.stringify({
            lines: this.doc_lines,
            version: this.version,
            ranges: this.ranges,
            pathname: this.pathname,
            projectHistoryId: this.projectHistoryId,
            projectHistoryType: this.projectHistoryType,
          })
        )
      })
    })

    describe('when project exists that was migrated with downgrades allowed', function () {
      beforeEach(function () {
        this.doc = { _id: this.doc_id }
        this.projectHistoryId = 1234
        this.projectHistoryDisplay = true
        this.projectHistoryType = undefined
        this.project = {
          _id: this.project_id,
          overleaf: {
            history: {
              id: this.projectHistoryId,
              display: this.projectHistoryDisplay,
              allowDowngrade: true,
            },
          },
        }
        this.ProjectGetter.getProject = sinon
          .stub()
          .callsArgWith(2, null, this.project)
        this.ProjectLocator.findElement = sinon
          .stub()
          .callsArgWith(1, null, this.doc, { fileSystem: this.pathname })
        this.ProjectEntityHandler.getDoc = sinon
          .stub()
          .yields(null, this.doc_lines, this.rev, this.version, this.ranges)
        return this.DocumentController.getDocument(
          this.req,
          this.res,
          this.next
        )
      })

      it('should return the history id in the JSON but not history type, sending history to both services', function () {
        this.res.type.should.equal('application/json')
        return this.res.body.should.equal(
          JSON.stringify({
            lines: this.doc_lines,
            version: this.version,
            ranges: this.ranges,
            pathname: this.pathname,
            projectHistoryId: this.projectHistoryId,
            projectHistoryType: this.projectHistoryType,
          })
        )
      })
    })

    describe('when the project does not exist', function () {
      beforeEach(function () {
        this.ProjectGetter.getProject = sinon.stub().callsArgWith(2, null, null)
        this.DocumentController.getDocument(this.req, this.res, this.next)
      })

      it('returns a 404', function () {
        this.res.statusCode.should.equal(404)
      })
    })
  })

  describe('setDocument', function () {
    beforeEach(function () {
      this.req.params = {
        Project_id: this.project_id,
        doc_id: this.doc_id,
      }
    })

    describe('when the document exists', function () {
      beforeEach(function () {
        this.ProjectEntityUpdateHandler.updateDocLines = sinon.stub().yields()
        this.req.body = {
          lines: this.doc_lines,
          version: this.version,
          ranges: this.ranges,
          lastUpdatedAt: this.lastUpdatedAt,
          lastUpdatedBy: this.lastUpdatedBy,
        }
        this.DocumentController.setDocument(this.req, this.res, this.next)
      })

      it('should update the document in Mongo', function () {
        sinon.assert.calledWith(
          this.ProjectEntityUpdateHandler.updateDocLines,
          this.project_id,
          this.doc_id,
          this.doc_lines,
          this.version,
          this.ranges,
          this.lastUpdatedAt,
          this.lastUpdatedBy
        )
      })

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

    describe("when the document doesn't exist", function () {
      beforeEach(function () {
        this.ProjectEntityUpdateHandler.updateDocLines = sinon
          .stub()
          .yields(new Errors.NotFoundError('document does not exist'))
        this.req.body = { lines: this.doc_lines }
        this.DocumentController.setDocument(this.req, this.res, this.next)
      })

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