// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only

import { assert } from 'chai';

import {
  getDeltaToRemoveStaleMentions,
  getTextAndRangesFromOps,
  getDeltaToRestartMention,
} from '../../quill/util';
import { BodyRange } from '../../types/BodyRange';
import { generateAci } from '../../types/ServiceId';

const SERVICE_ID_1 = generateAci();
const SERVICE_ID_2 = generateAci();
const SERVICE_ID_3 = generateAci();
const SERVICE_ID_4 = generateAci();

describe('getDeltaToRemoveStaleMentions', () => {
  const memberUuids = [SERVICE_ID_1, SERVICE_ID_2];

  describe('given text', () => {
    it('retains the text', () => {
      const originalOps = [
        {
          insert: 'whoa, nobody here',
        },
      ];

      const { ops } = getDeltaToRemoveStaleMentions(originalOps, memberUuids);

      assert.deepEqual(ops, [{ retain: 17 }]);
    });
  });

  describe('given stale and valid mentions', () => {
    it('retains the valid and replaces the stale', () => {
      const originalOps = [
        {
          insert: {
            mention: { aci: SERVICE_ID_3, title: 'Klaus' },
          },
        },
        { insert: { mention: { aci: SERVICE_ID_1, title: 'Werner' } } },
      ];

      const { ops } = getDeltaToRemoveStaleMentions(originalOps, memberUuids);

      assert.deepEqual(ops, [
        { delete: 1 },
        { insert: '@Klaus' },
        { retain: 1 },
      ]);
    });
  });

  describe('given emoji embeds', () => {
    it('retains the embeds', () => {
      const originalOps = [
        {
          insert: {
            emoji: { value: '😂' },
          },
        },
        {
          insert: {
            emoji: { value: '🍋' },
          },
        },
      ];

      const { ops } = getDeltaToRemoveStaleMentions(originalOps, memberUuids);

      assert.deepEqual(ops, [{ retain: 1 }, { retain: 1 }]);
    });
  });

  describe('given other ops', () => {
    it('passes them through', () => {
      const originalOps = [
        {
          delete: 5,
        },
      ];

      const { ops } = getDeltaToRemoveStaleMentions(originalOps, memberUuids);

      assert.deepEqual(ops, originalOps);
    });
  });
});

describe('getTextAndRangesFromOps', () => {
  describe('given only text', () => {
    it('returns only text trimmed', () => {
      const ops = [{ insert: ' The ' }, { insert: ' text \n' }];
      const { text, bodyRanges } = getTextAndRangesFromOps(ops);
      assert.equal(text, 'The  text');
      assert.equal(bodyRanges.length, 0);
    });

    it('returns trimmed of trailing newlines', () => {
      const ops = [{ insert: ' The\ntext\n\n\n' }];
      const { text, bodyRanges } = getTextAndRangesFromOps(ops);
      assert.equal(text, 'The\ntext');
      assert.equal(bodyRanges.length, 0);
    });
  });

  describe('given formatting', () => {
    it('handles trimming with simple ops', () => {
      const ops = [
        {
          insert: 'test test ',
          attributes: { bold: true },
        },
        // This is something Quill does for some reason
        {
          insert: '\n',
        },
      ];
      const { text, bodyRanges } = getTextAndRangesFromOps(ops);
      assert.equal(text, 'test test');
      assert.equal(bodyRanges.length, 1);
      assert.deepEqual(bodyRanges, [
        {
          start: 0,
          length: 9,
          style: BodyRange.Style.BOLD,
        },
      ]);
    });

    it('handles trimming with no-formatting single- and multi-newline ops', () => {
      const ops = [
        {
          insert: 'line1',
          attributes: { bold: true },
        },
        // quill doesn't put formatting on all-newline ops
        {
          insert: '\n',
        },
        {
          insert: 'line2',
          attributes: { bold: true },
        },
        {
          insert: '\n\n',
        },
        {
          insert: 'line4',
          attributes: { bold: true },
        },
        {
          insert: '\n\n',
        },
      ];
      const { text, bodyRanges } = getTextAndRangesFromOps(ops);
      assert.equal(text, 'line1\nline2\n\nline4');
      assert.equal(bodyRanges.length, 1);
      assert.deepEqual(bodyRanges, [
        {
          start: 0,
          length: 18,
          style: BodyRange.Style.BOLD,
        },
      ]);
    });

    it('handles trimming at the end of the message', () => {
      const ops = [
        {
          insert: 'Text with trailing ',
          attributes: { bold: true },
        },
        {
          insert: 'whitespace     ',
          attributes: { bold: true, italic: true },
        },
      ];
      const { text, bodyRanges } = getTextAndRangesFromOps(ops);
      assert.equal(text, 'Text with trailing whitespace');
      assert.equal(bodyRanges.length, 2);
      assert.deepEqual(bodyRanges, [
        {
          start: 0,
          length: 29,
          style: BodyRange.Style.BOLD,
        },
        {
          start: 19,
          length: 10,
          style: BodyRange.Style.ITALIC,
        },
      ]);
    });

    it('handles trimming at beginning of the message', () => {
      const ops = [
        {
          insert: '    Text with leading ',
          attributes: { bold: true },
        },
        {
          insert: 'whitespace!!',
          attributes: { bold: true, italic: true },
        },
      ];
      const { text, bodyRanges } = getTextAndRangesFromOps(ops);
      assert.equal(text, 'Text with leading whitespace!!');
      assert.equal(bodyRanges.length, 2);
      assert.deepEqual(bodyRanges, [
        {
          start: 0,
          length: 30,
          style: BodyRange.Style.BOLD,
        },
        {
          start: 18,
          length: 12,
          style: BodyRange.Style.ITALIC,
        },
      ]);
    });

    it('does not trim at beginning of the message if monospace', () => {
      const ops = [
        {
          insert: '  ',
        },
        {
          insert: '  Text with leading ',
          attributes: { monospace: true },
        },
        {
          insert: 'whitespace!!',
          attributes: { bold: true, italic: true },
        },
      ];
      const { text, bodyRanges } = getTextAndRangesFromOps(ops);
      assert.equal(text, '  Text with leading whitespace!!');
      assert.equal(bodyRanges.length, 3);
      assert.deepEqual(bodyRanges, [
        {
          start: 0,
          length: 20,
          style: BodyRange.Style.MONOSPACE,
        },
        {
          start: 20,
          length: 12,
          style: BodyRange.Style.BOLD,
        },
        {
          start: 20,
          length: 12,
          style: BodyRange.Style.ITALIC,
        },
      ]);
    });

    it('handles formatting of whitespace at beginning/ending of message', () => {
      const ops = [
        {
          insert: '  ',
          attributes: { bold: true, italic: true, strike: true },
        },
        {
          insert: '  ',
          attributes: { italic: true, strike: true, spoiler: true },
        },
        {
          insert: 'so much whitespace',
          attributes: { strike: true, spoiler: true, monospace: true },
        },
        {
          insert: '  ',
          attributes: { spoiler: true, monospace: true, italic: true },
        },
        {
          insert: '  ',
          attributes: { monospace: true, italic: true, bold: true },
        },
        { insert: '\n' },
      ];
      const { text, bodyRanges } = getTextAndRangesFromOps(ops);
      assert.equal(text, 'so much whitespace');
      assert.equal(bodyRanges.length, 3);
      assert.deepEqual(bodyRanges, [
        {
          start: 0,
          length: 18,
          style: BodyRange.Style.STRIKETHROUGH,
        },
        {
          start: 0,
          length: 18,
          style: BodyRange.Style.SPOILER,
        },
        {
          start: 0,
          length: 18,
          style: BodyRange.Style.MONOSPACE,
        },
      ]);
    });
  });

  describe('given text, emoji, and mentions', () => {
    it('returns the trimmed text with placeholders and mentions', () => {
      const ops = [
        {
          insert: {
            emoji: { value: '😂' },
          },
        },
        {
          insert: ' wow, funny, ',
        },
        {
          insert: {
            mention: {
              aci: SERVICE_ID_1,
              title: '@fred',
            },
          },
        },
      ];
      const { text, bodyRanges } = getTextAndRangesFromOps(ops);
      assert.equal(text, '😂 wow, funny, \uFFFC');
      assert.deepEqual(bodyRanges, [
        {
          length: 1,
          mentionAci: SERVICE_ID_1,
          replacementText: '@fred',
          start: 15,
        },
      ]);
    });
  });

  describe('given only mentions', () => {
    it('returns the trimmed text with placeholders and mentions', () => {
      const ops = [
        {
          insert: {
            mention: {
              aci: SERVICE_ID_1,
              title: '@fred',
            },
          },
        },
      ];
      const { text, bodyRanges } = getTextAndRangesFromOps(ops);
      assert.equal(text, '\uFFFC');
      assert.deepEqual(bodyRanges, [
        {
          length: 1,
          mentionAci: SERVICE_ID_1,
          replacementText: '@fred',
          start: 0,
        },
      ]);
    });

    it('does not trim newlines padding mentions', () => {
      const ops = [
        { insert: 'test \n' },
        {
          insert: {
            mention: {
              aci: SERVICE_ID_1,
              title: '@fred',
            },
          },
        },
        { insert: '\n test' },
      ];
      const { text, bodyRanges } = getTextAndRangesFromOps(ops);
      assert.equal(text, 'test \n\uFFFC\n test');
      assert.deepEqual(bodyRanges, [
        {
          length: 1,
          mentionAci: SERVICE_ID_1,
          replacementText: '@fred',
          start: 6,
        },
      ]);
    });
  });

  describe('given formatting on text, with emoji and mentions', () => {
    it('handles overlapping and contiguous format sections properly', () => {
      const ops = [
        {
          insert: 'Hey, ',
          attributes: {
            spoiler: true,
          },
        },
        {
          insert: {
            mention: {
              aci: SERVICE_ID_4,
              title: '@alice',
            },
          },
          attributes: {
            spoiler: true,
          },
        },
        {
          insert: ': this is ',
          attributes: {
            spoiler: true,
          },
        },
        {
          insert: 'bold',
          attributes: {
            bold: true,
            spoiler: true,
          },
        },
        {
          insert: ' and',
          attributes: {
            bold: true,
            italic: true,
            spoiler: true,
          },
        },
        {
          insert: ' italic',
          attributes: {
            italic: true,
            spoiler: true,
          },
        },
        {
          insert: ' and strikethrough',
          attributes: {
            strike: true,
          },
        },
        { insert: ' ' },
        {
          insert: 'and monospace',
          attributes: {
            monospace: true,
          },
        },
      ];
      const { text, bodyRanges } = getTextAndRangesFromOps(ops);
      assert.equal(
        text,
        'Hey, \uFFFC: this is bold and italic and strikethrough and monospace'
      );
      assert.deepEqual(bodyRanges, [
        {
          start: 5,
          length: 1,
          mentionAci: SERVICE_ID_4,
          replacementText: '@alice',
        },
        {
          start: 16,
          length: 8,
          style: BodyRange.Style.BOLD,
        },
        {
          start: 20,
          length: 11,
          style: BodyRange.Style.ITALIC,
        },
        {
          start: 0,
          length: 31,
          style: BodyRange.Style.SPOILER,
        },
        {
          start: 31,
          length: 18,
          style: BodyRange.Style.STRIKETHROUGH,
        },
        {
          start: 50,
          length: 13,
          style: BodyRange.Style.MONOSPACE,
        },
      ]);
    });

    it('handles lots of the same format', () => {
      const ops = [
        {
          insert: 'Every',
          attributes: {
            bold: true,
          },
        },
        {
          insert: ' other ',
        },
        {
          insert: 'word',
          attributes: {
            bold: true,
          },
        },
        {
          insert: ' is ',
        },
        {
          insert: 'bold!',
          attributes: {
            bold: true,
          },
        },
      ];
      const { text, bodyRanges } = getTextAndRangesFromOps(ops);
      assert.equal(text, 'Every other word is bold!');
      assert.deepEqual(bodyRanges, [
        {
          start: 0,
          length: 5,
          style: BodyRange.Style.BOLD,
        },
        {
          start: 12,
          length: 4,
          style: BodyRange.Style.BOLD,
        },
        {
          start: 20,
          length: 5,
          style: BodyRange.Style.BOLD,
        },
      ]);
    });

    it('handles formatting on mentions', () => {
      const ops = [
        {
          insert: {
            mention: {
              aci: SERVICE_ID_4,
              title: '@alice',
            },
          },
          attributes: {
            bold: true,
          },
        },
      ];
      const { text, bodyRanges } = getTextAndRangesFromOps(ops);
      assert.equal(text, '\uFFFC');
      assert.deepEqual(bodyRanges, [
        {
          start: 0,
          length: 1,
          mentionAci: SERVICE_ID_4,
          replacementText: '@alice',
        },
        {
          start: 0,
          length: 1,
          style: BodyRange.Style.BOLD,
        },
      ]);
    });
  });
});

describe('getDeltaToRestartMention', () => {
  describe('given text and emoji', () => {
    it('returns the correct retains, a delete, and an @', () => {
      const originalOps = [
        {
          insert: {
            emoji: { value: '😂' },
          },
        },
        {
          insert: {
            mention: {
              aci: SERVICE_ID_2,
              title: '@sam',
            },
          },
        },
        {
          insert: ' wow, funny, ',
        },
        {
          insert: {
            mention: {
              aci: SERVICE_ID_1,
              title: '@fred',
            },
          },
        },
      ];

      const { ops } = getDeltaToRestartMention(originalOps);

      assert.deepEqual(ops, [
        {
          retain: 1,
        },
        {
          retain: 1,
        },
        {
          retain: 13,
        },
        {
          retain: 1,
        },
        {
          delete: 1,
        },
        {
          insert: '@',
        },
      ]);
    });
  });
});