import sinon from 'sinon'
import { expect } from 'chai'
import { strict as esmock } from 'esmock'
import Core from 'overleaf-editor-core'

const MODULE_PATH = '../../../../app/js/UpdateTranslator.js'

describe('UpdateTranslator', function () {
  beforeEach(async function () {
    this.UpdateTranslator = await esmock(MODULE_PATH, {
      'overleaf-editor-core': Core,
    })
    this.callback = sinon.stub()

    this.project_id = '59bfd450e3028c4d40a1e9aa'
    this.doc_id = '59bfd450e3028c4d40a1e9ab'
    this.file_id = '59bfd450e3028c4d40a1easd'
    this.user_id = '59bb9051abf6e8682a269b64'
    this.version = 0
    this.timestamp = new Date().toJSON()
    this.mockBlobHash = '12345abc12345abc12345abc12345abc12345abc'
  })

  describe('convertToChanges', function () {
    it('can translate doc additions', function (done) {
      const updates = [
        {
          update: {
            doc: this.doc_id,
            pathname: '/main.tex',
            docLines: 'a\nb',
            meta: {
              user_id: this.user_id,
              ts: this.timestamp,
            },
          },
          blobHash: this.mockBlobHash,
        },
      ]
      const assertion = (error, changes) => {
        changes = changes.map(change => change.toRaw())
        expect(error).to.be.null
        expect(changes).to.deep.equal([
          {
            authors: [],
            operations: [
              {
                pathname: 'main.tex',
                file: {
                  hash: this.mockBlobHash,
                },
              },
            ],
            v2Authors: [this.user_id],
            timestamp: this.timestamp,
          },
        ])
        done()
      }

      this.UpdateTranslator.convertToChanges(
        this.project_id,
        updates,
        assertion
      )
    })

    it('can translate file additions', function (done) {
      const updates = [
        {
          update: {
            file: this.file_id,
            pathname: '/test.png',
            url: 'filestore.example.com/test.png',
            meta: {
              user_id: this.user_id,
              ts: this.timestamp,
            },
          },
          blobHash: this.mockBlobHash,
        },
      ]
      const assertion = (error, changes) => {
        changes = changes.map(change => change.toRaw())
        expect(error).to.be.null
        expect(changes).to.deep.equal([
          {
            authors: [],
            operations: [
              {
                pathname: 'test.png',
                file: {
                  hash: this.mockBlobHash,
                },
              },
            ],
            v2Authors: [this.user_id],
            timestamp: this.timestamp,
          },
        ])
        done()
      }

      this.UpdateTranslator.convertToChanges(
        this.project_id,
        updates,
        assertion
      )
    })

    it('can translate doc renames', function (done) {
      const updates = [
        {
          update: {
            doc: this.doc_id,
            pathname: '/main.tex',
            new_pathname: '/new_main.tex',
            meta: {
              user_id: this.user_id,
              ts: this.timestamp,
            },
          },
        },
      ]
      const assertion = (error, changes) => {
        changes = changes.map(change => change.toRaw())
        expect(error).to.be.null
        expect(changes).to.deep.equal([
          {
            authors: [],
            operations: [
              {
                pathname: 'main.tex',
                newPathname: 'new_main.tex',
              },
            ],
            v2Authors: [this.user_id],
            timestamp: this.timestamp,
          },
        ])
        done()
      }

      this.UpdateTranslator.convertToChanges(
        this.project_id,
        updates,
        assertion
      )
    })

    it('can translate file renames', function (done) {
      const updates = [
        {
          update: {
            file: this.file_id,
            pathname: '/test.png',
            new_pathname: '/new_test.png',
            meta: {
              user_id: this.user_id,
              ts: this.timestamp,
            },
          },
        },
      ]
      const assertion = (error, changes) => {
        changes = changes.map(change => change.toRaw())
        expect(error).to.be.null
        expect(changes).to.deep.equal([
          {
            authors: [],
            operations: [
              {
                pathname: 'test.png',
                newPathname: 'new_test.png',
              },
            ],
            v2Authors: [this.user_id],
            timestamp: this.timestamp,
          },
        ])
        done()
      }

      this.UpdateTranslator.convertToChanges(
        this.project_id,
        updates,
        assertion
      )
    })

    it('can translate multiple updates with the correct versions', function (done) {
      const updates = [
        {
          update: {
            doc: this.doc_id,
            pathname: '/main.tex',
            docLines: 'a\nb',
            meta: {
              user_id: this.user_id,
              ts: this.timestamp,
            },
          },
          blobHash: this.mockBlobHash,
        },
        {
          update: {
            file: this.file_id,
            pathname: '/test.png',
            url: 'filestore.example.com/test.png',
            meta: {
              user_id: this.user_id,
              ts: this.timestamp,
            },
          },
          blobHash: this.mockBlobHash,
        },
      ]
      const assertion = (error, changes) => {
        changes = changes.map(change => change.toRaw())
        expect(error).to.be.null
        expect(changes).to.deep.equal([
          {
            authors: [],
            operations: [
              {
                pathname: 'main.tex',
                file: {
                  hash: this.mockBlobHash,
                },
              },
            ],
            v2Authors: [this.user_id],
            timestamp: this.timestamp,
          },
          {
            authors: [],
            operations: [
              {
                pathname: 'test.png',
                file: {
                  hash: this.mockBlobHash,
                },
              },
            ],
            v2Authors: [this.user_id],
            timestamp: this.timestamp,
          },
        ])
        done()
      }

      this.UpdateTranslator.convertToChanges(
        this.project_id,
        updates,
        assertion
      )
    })

    it('returns an error if the update has an unknown format', function (done) {
      const updates = [
        {
          update: {
            foo: 'bar',
          },
        },
      ]
      const assertion = (error, changes) => {
        expect(error)
          .to.exist.and.be.instanceof(Error)
          .and.have.property('message', 'update with unknown format')
        done()
      }

      this.UpdateTranslator.convertToChanges(
        this.project_id,
        updates,
        assertion
      )
    })

    it('replaces backslashes with underscores in pathnames', function (done) {
      const updates = [
        {
          update: {
            doc: this.doc_id,
            pathname: '/\\main\\foo.tex',
            new_pathname: '/\\new_main\\foo\\bar.tex',
            meta: {
              user_id: this.user_id,
              ts: this.timestamp,
            },
          },
        },
      ]
      const assertion = (error, changes) => {
        changes = changes.map(change => change.toRaw())
        expect(error).to.be.null
        expect(changes).to.deep.equal([
          {
            authors: [],
            operations: [
              {
                pathname: '_main_foo.tex',
                newPathname: '_new_main_foo_bar.tex',
              },
            ],
            v2Authors: [this.user_id],
            timestamp: this.timestamp,
          },
        ])
        done()
      }

      this.UpdateTranslator.convertToChanges(
        this.project_id,
        updates,
        assertion
      )
    })

    it('replaces leading asterisks with __ASTERISK__ in pathnames', function (done) {
      const updates = [
        {
          update: {
            file: this.file_id,
            pathname: '/test*test.png',
            meta: {
              user_id: this.user_id,
              ts: this.timestamp,
            },
            url: 'filestore.example.com/test*test.png',
          },
          blobHash: this.mockBlobHash,
        },
      ]
      const assertion = (error, changes) => {
        changes = changes.map(change => change.toRaw())
        expect(error).to.be.null
        expect(changes).to.deep.equal([
          {
            authors: [],
            operations: [
              {
                pathname: 'test__ASTERISK__test.png',
                file: {
                  hash: this.mockBlobHash,
                },
              },
            ],
            v2Authors: [this.user_id],
            timestamp: this.timestamp,
          },
        ])
        done()
      }

      this.UpdateTranslator.convertToChanges(
        this.project_id,
        updates,
        assertion
      )
    })

    it('replaces a leading space for top-level files with __SPACE__', function (done) {
      const updates = [
        {
          update: {
            file: this.file_id,
            pathname: '/ test.png',
            meta: {
              user_id: this.user_id,
              ts: this.timestamp,
            },
            url: 'filestore.example.com/test.png',
          },
          blobHash: this.mockBlobHash,
        },
      ]
      const assertion = (error, changes) => {
        changes = changes.map(change => change.toRaw())
        expect(error).to.be.null
        expect(changes).to.deep.equal([
          {
            authors: [],
            operations: [
              {
                pathname: '__SPACE__test.png',
                file: {
                  hash: this.mockBlobHash,
                },
              },
            ],
            v2Authors: [this.user_id],
            timestamp: this.timestamp,
          },
        ])
        done()
      }

      this.UpdateTranslator.convertToChanges(
        this.project_id,
        updates,
        assertion
      )
    })

    it('replaces leading spaces of files in subfolders with __SPACE__', function (done) {
      const updates = [
        {
          update: {
            file: this.file_id,
            pathname: '/folder/ test.png',
            meta: {
              user_id: this.user_id,
              ts: this.timestamp,
            },
            url: 'filestore.example.com/folder/test.png',
          },
          blobHash: this.mockBlobHash,
        },
      ]
      const assertion = (error, changes) => {
        changes = changes.map(change => change.toRaw())
        expect(error).to.be.null
        expect(changes).to.deep.equal([
          {
            authors: [],
            operations: [
              {
                pathname: 'folder/__SPACE__test.png',
                file: {
                  hash: this.mockBlobHash,
                },
              },
            ],
            v2Authors: [this.user_id],
            timestamp: this.timestamp,
          },
        ])
        done()
      }

      this.UpdateTranslator.convertToChanges(
        this.project_id,
        updates,
        assertion
      )
    })

    describe('text updates', function () {
      it('can translate insertions', function (done) {
        const updates = [
          {
            update: {
              doc: this.doc_id,
              op: [
                { p: 3, i: 'foo' },
                { p: 15, i: 'bar' },
              ],
              v: this.version,
              meta: {
                user_id: this.user_id,
                ts: new Date(this.timestamp).getTime(),
                pathname: '/main.tex',
                doc_length: 20,
                source: 'some-editor-id',
              },
            },
          },
        ]
        const assertion = (error, changes) => {
          changes = changes.map(change => change.toRaw())
          expect(error).to.be.null
          expect(changes).to.deep.equal([
            {
              authors: [],
              operations: [
                {
                  pathname: 'main.tex',
                  textOperation: [3, 'foo', 9, 'bar', 8],
                },
              ],
              v2Authors: [this.user_id],
              timestamp: this.timestamp,
              v2DocVersions: {
                '59bfd450e3028c4d40a1e9ab': {
                  pathname: 'main.tex',
                  v: 0,
                },
              },
            },
          ])
          done()
        }

        this.UpdateTranslator.convertToChanges(
          this.project_id,
          updates,
          assertion
        )
      })

      it('can translate deletions', function (done) {
        const updates = [
          {
            update: {
              doc: this.doc_id,
              op: [
                { p: 3, d: 'lo' },
                { p: 10, d: 'bar' },
              ],
              v: this.version,
              meta: {
                user_id: this.user_id,
                ts: new Date(this.timestamp).getTime(),
                pathname: '/main.tex',
                doc_length: 20,
              },
            },
          },
        ]
        const assertion = (error, changes) => {
          changes = changes.map(change => change.toRaw())
          expect(error).to.be.null
          expect(changes).to.deep.equal([
            {
              authors: [],
              operations: [
                {
                  pathname: 'main.tex',
                  textOperation: [3, -2, 7, -3, 5],
                },
              ],
              v2Authors: [this.user_id],
              timestamp: this.timestamp,
              v2DocVersions: {
                '59bfd450e3028c4d40a1e9ab': {
                  pathname: 'main.tex',
                  v: 0,
                },
              },
            },
          ])
          done()
        }

        this.UpdateTranslator.convertToChanges(
          this.project_id,
          updates,
          assertion
        )
      })

      it('can translate insertions at the start and end (with zero retained)', function (done) {
        const updates = [
          {
            update: {
              doc: this.doc_id,
              op: [
                { p: 0, i: 'foo' },
                { p: 23, i: 'bar' },
                { p: 0, d: 'foo' },
                { p: 20, d: 'bar' },
              ],
              v: this.version,
              meta: {
                user_id: this.user_id,
                ts: new Date(this.timestamp).getTime(),
                pathname: '/main.tex',
                doc_length: 20,
              },
            },
          },
        ]
        const assertion = (error, changes) => {
          changes = changes.map(change => change.toRaw())
          expect(error).to.be.null
          expect(changes).to.deep.equal([
            {
              authors: [],
              operations: [
                {
                  pathname: 'main.tex',
                  textOperation: [20],
                },
              ],
              v2Authors: [this.user_id],
              timestamp: this.timestamp,
              v2DocVersions: {
                '59bfd450e3028c4d40a1e9ab': {
                  pathname: 'main.tex',
                  v: 0,
                },
              },
            },
          ])
          done()
        }

        this.UpdateTranslator.convertToChanges(
          this.project_id,
          updates,
          assertion
        )
      })

      it('can handle operations in non-linear offset order', function (done) {
        const updates = [
          {
            update: {
              doc: this.doc_id,
              op: [
                { p: 15, i: 'foo' },
                { p: 3, i: 'bar' },
              ],
              v: this.version,
              meta: {
                user_id: this.user_id,
                ts: this.timestamp,
                pathname: '/main.tex',
                doc_length: 20,
              },
            },
          },
        ]
        const assertion = (error, changes) => {
          changes = changes.map(change => change.toRaw())
          expect(error).to.be.null
          expect(changes).to.deep.equal([
            {
              authors: [],
              operations: [
                {
                  pathname: 'main.tex',
                  textOperation: [3, 'bar', 12, 'foo', 5],
                },
              ],
              v2Authors: [this.user_id],
              timestamp: this.timestamp,
              v2DocVersions: {
                '59bfd450e3028c4d40a1e9ab': {
                  pathname: 'main.tex',
                  v: 0,
                },
              },
            },
          ])
          done()
        }

        this.UpdateTranslator.convertToChanges(
          this.project_id,
          updates,
          assertion
        )
      })

      it('can ignore comment ops', function (done) {
        const updates = [
          {
            update: {
              doc: this.doc_id,
              op: [
                { p: 0, i: 'foo' },
                { p: 5, c: 'bar' },
                { p: 10, i: 'baz' },
              ],
              v: this.version,
              meta: {
                user_id: this.user_id,
                ts: new Date(this.timestamp).getTime(),
                pathname: '/main.tex',
                doc_length: 20,
              },
            },
          },
        ]
        const assertion = (error, changes) => {
          changes = changes.map(change => change.toRaw())
          expect(error).to.be.null
          expect(changes).to.deep.equal([
            {
              authors: [],
              operations: [
                {
                  pathname: 'main.tex',
                  textOperation: ['foo', 7, 'baz', 13],
                },
              ],
              v2Authors: [this.user_id],
              timestamp: this.timestamp,
              v2DocVersions: {
                '59bfd450e3028c4d40a1e9ab': {
                  pathname: 'main.tex',
                  v: 0,
                },
              },
            },
          ])
          done()
        }

        this.UpdateTranslator.convertToChanges(
          this.project_id,
          updates,
          assertion
        )
      })

      it('handles insertions after the end of the document', function (done) {
        const updates = [
          {
            update: {
              doc: this.doc_id,
              op: [{ p: 3, i: '\\' }],
              v: this.version,
              meta: {
                user_id: this.user_id,
                ts: new Date(this.timestamp).getTime(),
                pathname: '/main.tex',
                doc_length: 2,
              },
            },
          },
        ]
        const assertion = (error, changes) => {
          changes = changes.map(change => change.toRaw())
          expect(error).to.be.null
          expect(changes).to.deep.equal([
            {
              authors: [],
              operations: [
                {
                  pathname: 'main.tex',
                  textOperation: [2, '\\'],
                },
              ],
              v2Authors: [this.user_id],
              timestamp: this.timestamp,
              v2DocVersions: {
                '59bfd450e3028c4d40a1e9ab': {
                  pathname: 'main.tex',
                  v: 0,
                },
              },
            },
          ])
          done()
        }

        this.UpdateTranslator.convertToChanges(
          this.project_id,
          updates,
          assertion
        )
      })

      it('translates external source metadata into an origin', function (done) {
        const updates = [
          {
            update: {
              doc: this.doc_id,
              op: [{ p: 3, i: 'foo' }],
              v: this.version,
              meta: {
                user_id: this.user_id,
                ts: new Date(this.timestamp).getTime(),
                pathname: '/main.tex',
                doc_length: 20,
                type: 'external',
                source: 'dropbox',
              },
            },
          },
        ]
        const assertion = (error, changes) => {
          changes = changes.map(change => change.toRaw())
          expect(error).to.be.null
          expect(changes).to.deep.equal([
            {
              authors: [],
              operations: [
                {
                  pathname: 'main.tex',
                  textOperation: [3, 'foo', 17],
                },
              ],
              v2Authors: [this.user_id],
              timestamp: this.timestamp,
              v2DocVersions: {
                '59bfd450e3028c4d40a1e9ab': {
                  pathname: 'main.tex',
                  v: 0,
                },
              },
              origin: { kind: 'dropbox' },
            },
          ])
          done()
        }

        this.UpdateTranslator.convertToChanges(
          this.project_id,
          updates,
          assertion
        )
      })

      it('errors on unexpected ops', function (done) {
        const updates = [
          {
            update: {
              doc: this.doc_id,
              op: [{ p: 5, z: 'bar' }],
              v: this.version,
              meta: {
                user_id: this.user_id,
                ts: new Date(this.timestamp).getTime(),
                pathname: '/main.tex',
                doc_length: 20,
              },
            },
          },
        ]
        const assertion = (error, changes) => {
          expect(error.message).to.equal('unexpected op type')
          done()
        }

        this.UpdateTranslator.convertToChanges(
          this.project_id,
          updates,
          assertion
        )
      })
    })
  })
})
