/* eslint-disable
    mocha/no-identical-title,
    no-return-assign,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
 * decaffeinate suggestions:
 * DS101: Remove unnecessary use of Array.from
 * DS102: Remove unnecessary code created because of implicit returns
 * DS202: Simplify dynamic range loops
 * DS205: Consider reworking code to avoid use of IIFEs
 * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
 */
const text = require('../../../../app/js/sharejs/types/text')
const RangesTracker = require('@overleaf/ranges-tracker')

describe('ShareJS text type', function () {
  beforeEach(function () {
    return (this.t = 'mock-thread-id')
  })

  describe('transform', function () {
    describe('insert / insert', function () {
      it('with an insert before', function () {
        const dest = []
        text._tc(dest, { i: 'foo', p: 9 }, { i: 'bar', p: 3 })
        return dest.should.deep.equal([{ i: 'foo', p: 12 }])
      })

      it('with an insert after', function () {
        const dest = []
        text._tc(dest, { i: 'foo', p: 3 }, { i: 'bar', p: 9 })
        return dest.should.deep.equal([{ i: 'foo', p: 3 }])
      })

      it("with an insert at the same place with side == 'right'", function () {
        const dest = []
        text._tc(dest, { i: 'foo', p: 3 }, { i: 'bar', p: 3 }, 'right')
        return dest.should.deep.equal([{ i: 'foo', p: 6 }])
      })

      return it("with an insert at the same place with side == 'left'", function () {
        const dest = []
        text._tc(dest, { i: 'foo', p: 3 }, { i: 'bar', p: 3 }, 'left')
        return dest.should.deep.equal([{ i: 'foo', p: 3 }])
      })
    })

    describe('insert / delete', function () {
      it('with a delete before', function () {
        const dest = []
        text._tc(dest, { i: 'foo', p: 9 }, { d: 'bar', p: 3 })
        return dest.should.deep.equal([{ i: 'foo', p: 6 }])
      })

      it('with a delete after', function () {
        const dest = []
        text._tc(dest, { i: 'foo', p: 3 }, { d: 'bar', p: 9 })
        return dest.should.deep.equal([{ i: 'foo', p: 3 }])
      })

      it("with a delete at the same place with side == 'right'", function () {
        const dest = []
        text._tc(dest, { i: 'foo', p: 3 }, { d: 'bar', p: 3 }, 'right')
        return dest.should.deep.equal([{ i: 'foo', p: 3 }])
      })

      return it("with a delete at the same place with side == 'left'", function () {
        const dest = []

        text._tc(dest, { i: 'foo', p: 3 }, { d: 'bar', p: 3 }, 'left')
        return dest.should.deep.equal([{ i: 'foo', p: 3 }])
      })
    })

    describe('delete / insert', function () {
      it('with an insert before', function () {
        const dest = []
        text._tc(dest, { d: 'foo', p: 9 }, { i: 'bar', p: 3 })
        return dest.should.deep.equal([{ d: 'foo', p: 12 }])
      })

      it('with an insert after', function () {
        const dest = []
        text._tc(dest, { d: 'foo', p: 3 }, { i: 'bar', p: 9 })
        return dest.should.deep.equal([{ d: 'foo', p: 3 }])
      })

      it("with an insert at the same place with side == 'right'", function () {
        const dest = []
        text._tc(dest, { d: 'foo', p: 3 }, { i: 'bar', p: 3 }, 'right')
        return dest.should.deep.equal([{ d: 'foo', p: 6 }])
      })

      it("with an insert at the same place with side == 'left'", function () {
        const dest = []
        text._tc(dest, { d: 'foo', p: 3 }, { i: 'bar', p: 3 }, 'left')
        return dest.should.deep.equal([{ d: 'foo', p: 6 }])
      })

      return it('with a delete that overlaps the insert location', function () {
        const dest = []
        text._tc(dest, { d: 'foo', p: 3 }, { i: 'bar', p: 4 })
        return dest.should.deep.equal([
          { d: 'f', p: 3 },
          { d: 'oo', p: 6 },
        ])
      })
    })

    describe('delete / delete', function () {
      it('with a delete before', function () {
        const dest = []
        text._tc(dest, { d: 'foo', p: 9 }, { d: 'bar', p: 3 })
        return dest.should.deep.equal([{ d: 'foo', p: 6 }])
      })

      it('with a delete after', function () {
        const dest = []
        text._tc(dest, { d: 'foo', p: 3 }, { d: 'bar', p: 9 })
        return dest.should.deep.equal([{ d: 'foo', p: 3 }])
      })

      it('with deleting the same content', function () {
        const dest = []
        text._tc(dest, { d: 'foo', p: 3 }, { d: 'foo', p: 3 }, 'right')
        return dest.should.deep.equal([])
      })

      it('with the delete overlapping before', function () {
        const dest = []
        text._tc(dest, { d: 'foobar', p: 3 }, { d: 'abcfoo', p: 0 }, 'right')
        return dest.should.deep.equal([{ d: 'bar', p: 0 }])
      })

      it('with the delete overlapping after', function () {
        const dest = []
        text._tc(dest, { d: 'abcfoo', p: 3 }, { d: 'foobar', p: 6 })
        return dest.should.deep.equal([{ d: 'abc', p: 3 }])
      })

      it('with the delete overlapping the whole delete', function () {
        const dest = []
        text._tc(dest, { d: 'abcfoo123', p: 3 }, { d: 'foo', p: 6 })
        return dest.should.deep.equal([{ d: 'abc123', p: 3 }])
      })

      return it('with the delete inside the whole delete', function () {
        const dest = []
        text._tc(dest, { d: 'foo', p: 6 }, { d: 'abcfoo123', p: 3 })
        return dest.should.deep.equal([])
      })
    })

    describe('comment / insert', function () {
      it('with an insert before', function () {
        const dest = []
        text._tc(dest, { c: 'foo', p: 9, t: this.t }, { i: 'bar', p: 3 })
        return dest.should.deep.equal([{ c: 'foo', p: 12, t: this.t }])
      })

      it('with an insert after', function () {
        const dest = []
        text._tc(dest, { c: 'foo', p: 3, t: this.t }, { i: 'bar', p: 9 })
        return dest.should.deep.equal([{ c: 'foo', p: 3, t: this.t }])
      })

      it('with an insert at the left edge', function () {
        const dest = []
        text._tc(dest, { c: 'foo', p: 3, t: this.t }, { i: 'bar', p: 3 })
        // RangesTracker doesn't inject inserts into comments on edges, so neither should we
        return dest.should.deep.equal([{ c: 'foo', p: 6, t: this.t }])
      })

      it('with an insert at the right edge', function () {
        const dest = []
        text._tc(dest, { c: 'foo', p: 3, t: this.t }, { i: 'bar', p: 6 })
        // RangesTracker doesn't inject inserts into comments on edges, so neither should we
        return dest.should.deep.equal([{ c: 'foo', p: 3, t: this.t }])
      })

      return it('with an insert in the middle', function () {
        const dest = []
        text._tc(dest, { c: 'foo', p: 3, t: this.t }, { i: 'bar', p: 5 })
        return dest.should.deep.equal([{ c: 'fobaro', p: 3, t: this.t }])
      })
    })

    describe('comment / delete', function () {
      it('with a delete before', function () {
        const dest = []
        text._tc(dest, { c: 'foo', p: 9, t: this.t }, { d: 'bar', p: 3 })
        return dest.should.deep.equal([{ c: 'foo', p: 6, t: this.t }])
      })

      it('with a delete after', function () {
        const dest = []
        text._tc(dest, { c: 'foo', p: 3, t: this.t }, { i: 'bar', p: 9 })
        return dest.should.deep.equal([{ c: 'foo', p: 3, t: this.t }])
      })

      it('with a delete overlapping the comment content before', function () {
        const dest = []
        text._tc(dest, { c: 'foobar', p: 6, t: this.t }, { d: '123foo', p: 3 })
        return dest.should.deep.equal([{ c: 'bar', p: 3, t: this.t }])
      })

      it('with a delete overlapping the comment content after', function () {
        const dest = []
        text._tc(dest, { c: 'foobar', p: 6, t: this.t }, { d: 'bar123', p: 9 })
        return dest.should.deep.equal([{ c: 'foo', p: 6, t: this.t }])
      })

      it('with a delete overlapping the comment content in the middle', function () {
        const dest = []
        text._tc(dest, { c: 'foo123bar', p: 6, t: this.t }, { d: '123', p: 9 })
        return dest.should.deep.equal([{ c: 'foobar', p: 6, t: this.t }])
      })

      return it('with a delete overlapping the whole comment', function () {
        const dest = []
        text._tc(dest, { c: 'foo', p: 6, t: this.t }, { d: '123foo456', p: 3 })
        return dest.should.deep.equal([{ c: '', p: 3, t: this.t }])
      })
    })

    describe('comment / insert', function () {
      return it('should not do anything', function () {
        const dest = []
        text._tc(dest, { i: 'foo', p: 6 }, { c: 'bar', p: 3 })
        return dest.should.deep.equal([{ i: 'foo', p: 6 }])
      })
    })

    describe('comment / delete', function () {
      return it('should not do anything', function () {
        const dest = []
        text._tc(dest, { d: 'foo', p: 6 }, { c: 'bar', p: 3 })
        return dest.should.deep.equal([{ d: 'foo', p: 6 }])
      })
    })

    return describe('comment / comment', function () {
      return it('should not do anything', function () {
        const dest = []
        text._tc(dest, { c: 'foo', p: 6 }, { c: 'bar', p: 3 })
        return dest.should.deep.equal([{ c: 'foo', p: 6 }])
      })
    })
  })

  describe('apply', function () {
    it('should apply an insert', function () {
      return text.apply('foo', [{ i: 'bar', p: 2 }]).should.equal('fobaro')
    })

    it('should apply a delete', function () {
      return text
        .apply('foo123bar', [{ d: '123', p: 3 }])
        .should.equal('foobar')
    })

    it('should do nothing with a comment', function () {
      return text
        .apply('foo123bar', [{ c: '123', p: 3 }])
        .should.equal('foo123bar')
    })

    it('should throw an error when deleted content does not match', function () {
      return (() => text.apply('foo123bar', [{ d: '456', p: 3 }])).should.throw(
        Error
      )
    })

    return it('should throw an error when comment content does not match', function () {
      return (() => text.apply('foo123bar', [{ c: '456', p: 3 }])).should.throw(
        Error
      )
    })
  })

  return describe('applying ops and comments in different orders', function () {
    return it('should not matter which op or comment is applied first', function () {
      let length, p
      let asc, end
      let asc1, end1
      let asc3, end3
      const transform = function (op1, op2, side) {
        const d = []
        text._tc(d, op1, op2, side)
        return d
      }

      const applySnapshot = (snapshot, op) => text.apply(snapshot, op)

      const applyRanges = function (rangesTracker, ops) {
        for (const op of Array.from(ops)) {
          rangesTracker.applyOp(op, {})
        }
        return rangesTracker
      }

      const commentsEqual = function (comments1, comments2) {
        if (comments1.length !== comments2.length) {
          return false
        }
        comments1.sort((a, b) => {
          if (a.offset - b.offset === 0) {
            return a.length - b.length
          } else {
            return a.offset - b.offset
          }
        })
        comments2.sort((a, b) => {
          if (a.offset - b.offset === 0) {
            return a.length - b.length
          } else {
            return a.offset - b.offset
          }
        })
        for (let i = 0; i < comments1.length; i++) {
          const comment1 = comments1[i]
          const comment2 = comments2[i]
          if (
            comment1.offset !== comment2.offset ||
            comment1.length !== comment2.length
          ) {
            return false
          }
        }
        return true
      }

      const SNAPSHOT = '123'

      const OPS = []
      // Insert ops
      for (
        p = 0, end = SNAPSHOT.length, asc = end >= 0;
        asc ? p <= end : p >= end;
        asc ? p++ : p--
      ) {
        OPS.push({ i: 'a', p })
        OPS.push({ i: 'bc', p })
      }
      for (
        p = 0, end1 = SNAPSHOT.length - 1, asc1 = end1 >= 0;
        asc1 ? p <= end1 : p >= end1;
        asc1 ? p++ : p--
      ) {
        let asc2, end2
        for (
          length = 1, end2 = SNAPSHOT.length - p, asc2 = end2 >= 1;
          asc2 ? length <= end2 : length >= end2;
          asc2 ? length++ : length--
        ) {
          OPS.push({ d: SNAPSHOT.slice(p, p + length), p })
        }
      }
      for (
        p = 0, end3 = SNAPSHOT.length - 1, asc3 = end3 >= 0;
        asc3 ? p <= end3 : p >= end3;
        asc3 ? p++ : p--
      ) {
        let asc4, end4
        for (
          length = 1, end4 = SNAPSHOT.length - p, asc4 = end4 >= 1;
          asc4 ? length <= end4 : length >= end4;
          asc4 ? length++ : length--
        ) {
          OPS.push({ c: SNAPSHOT.slice(p, p + length), p, t: this.t })
        }
      }

      return (() => {
        const result = []
        for (const op1 of Array.from(OPS)) {
          result.push(
            (() => {
              const result1 = []
              for (const op2 of Array.from(OPS)) {
                const op1T = transform(op1, op2, 'left')
                const op2T = transform(op2, op1, 'right')

                const rt12 = new RangesTracker()
                const snapshot12 = applySnapshot(
                  applySnapshot(SNAPSHOT, [op1]),
                  op2T
                )
                applyRanges(rt12, [op1])
                applyRanges(rt12, op2T)

                const rt21 = new RangesTracker()
                const snapshot21 = applySnapshot(
                  applySnapshot(SNAPSHOT, [op2]),
                  op1T
                )
                applyRanges(rt21, [op2])
                applyRanges(rt21, op1T)

                if (snapshot12 !== snapshot21) {
                  console.error(
                    {
                      op1,
                      op2,
                      op1T,
                      op2T,
                      snapshot12,
                      snapshot21,
                    },
                    'Ops are not consistent'
                  )
                  throw new Error('OT is inconsistent')
                }

                if (!commentsEqual(rt12.comments, rt21.comments)) {
                  console.log(rt12.comments)
                  console.log(rt21.comments)
                  console.error(
                    {
                      op1,
                      op2,
                      op1T,
                      op2T,
                      rt12_comments: rt12.comments,
                      rt21_comments: rt21.comments,
                    },
                    'Comments are not consistent'
                  )
                  throw new Error('OT is inconsistent')
                } else {
                  result1.push(undefined)
                }
              }
              return result1
            })()
          )
        }
        return result
      })()
    })
  })
})
