import expect from 'expect';
import fetchMock from 'fetch-mock';

import {
  mockAllBadges,
  TEST_CHANNEL_ID,
} from 'mtest/chat/helpers/badgerFetchMocks';
import { TEST_MESSAGE, TEST_USERSTATE } from 'mtest/chat/helpers/chatObjects';

import { POST, messageEvent } from 'mweb/chat/events/messageEvent';
import { TIMEOUT, moderationEvent } from 'mweb/chat/events/moderationEvent';
import {
  SUBSCRIPTION,
  RESUBSCRIPTION,
  subscriptionEvent,
  resubscriptionEvent,
} from 'mweb/chat/events/subscribeEvent';
import {
  messagePartBuilder,
  BITS_COLOR_MAP,
  MessagePart,
  EmoteData,
  LinkData,
} from 'mweb/chat/events/utils/createMessageData';
import { getUsernameDisplay } from 'mweb/chat/events/utils/getUsernameDisplay';
import { BadgerService } from 'mweb/chat/badgerService';

describe('chatEvent', () => {
  describe('messageEvent shape', () => {
    afterEach(() => {
      expect(fetchMock.done()).toEqual(true);
      fetchMock.restore();
    });

    it('creates a properly formed event', () => {
      mockAllBadges();
      const badger = new BadgerService();
      return badger.init(TEST_CHANNEL_ID).then(() => {
        const event = messageEvent(
          POST,
          TEST_MESSAGE,
          TEST_USERSTATE,
          badger.getBadgeData(TEST_USERSTATE.badges),
        );

        expect(event.type).toEqual(POST);
        expect(event.id).toEqual(TEST_USERSTATE.id);
        expect(event.badges.length).toEqual(1);
        expect(event.user).toExist();
        expect(event.user.username).toEqual(TEST_USERSTATE.username);
        expect(event.user.usernameDisplay).toEqual(
          TEST_USERSTATE['display-name'],
        );
        expect(event.user.color).toEqual(TEST_USERSTATE.color);
        expect(event.user.isIntl).toEqual(false);
        expect(event.messageParts!.length).toEqual(2);
      });
    });
  });

  describe('moderationEvent shape', () => {
    const USERNAME = 'voxel';
    const REASON = 'for being too awesome';

    it('creates a properly formed event', () => {
      const event = moderationEvent(TIMEOUT, USERNAME, REASON, 10);

      expect(event.type).toEqual(TIMEOUT);
      expect(event.username).toEqual(USERNAME);
      expect(event.reason).toEqual(REASON);
      expect(event.duration).toEqual(10);
      expect(event.id).toExist();
    });
  });

  describe('subscriptionEvent shape', () => {
    it('creates a properly formed subscription event', () => {
      const event = subscriptionEvent('lirik', 'voxel', false);

      expect(event.type).toEqual(SUBSCRIPTION);
      expect(event.username).toEqual('voxel');
      expect(event.isPrime).toEqual(false);
      expect(event.channel).toEqual('lirik');
      expect(event.id).toExist();
    });

    it('creates a properly formed Twitch Prime subscription event', () => {
      const event = subscriptionEvent('lirik', 'voxel', true);

      expect(event.type).toEqual(SUBSCRIPTION);
      expect(event.username).toEqual('voxel');
      expect(event.isPrime).toEqual(true);
      expect(event.channel).toEqual('lirik');
      expect(event.id).toExist();
    });
  });

  describe('resubscriptionEvent shape', () => {
    it('creates a properly formed resubscription event', () => {
      const event = resubscriptionEvent(
        'lirik',
        'Voxel',
        false,
        3,
        TEST_MESSAGE,
        TEST_USERSTATE,
        [],
      );

      expect(event.type).toEqual(RESUBSCRIPTION);
      expect(event.isPrime).toEqual(false);
      expect(event.months).toEqual(3);
      expect(event.channel).toEqual('lirik');
      expect(event.id).toExist();
      expect(event.user).toExist();
      expect(event.user!.username).toEqual('voxel');
      expect(event.user!.usernameDisplay).toEqual('Voxel');
      expect(event.user!.color).toEqual(TEST_USERSTATE.color);
      expect(event.user!.isIntl).toEqual(false);
      expect(event.messageParts).toExist();
    });

    it('creates a properly formed Twitch Prime resubscription event', () => {
      const event = resubscriptionEvent(
        'lirik',
        TEST_USERSTATE['display-name'],
        true,
        2,
        TEST_MESSAGE,
        TEST_USERSTATE,
        [],
      );

      expect(event.type).toEqual(RESUBSCRIPTION);
      expect(event.isPrime).toEqual(true);
      expect(event.months).toEqual(2);
      expect(event.channel).toEqual('lirik');
      expect(event.id).toExist();
      expect(event.user).toExist();
      expect(event.user!.username).toEqual('voxel');
      expect(event.user!.usernameDisplay).toEqual('Voxel');
      expect(event.user!.color).toEqual(TEST_USERSTATE.color);
      expect(event.user!.isIntl).toEqual(false);
      expect(event.messageParts).toExist();
    });

    describe('with a message', () => {
      afterEach(() => {
        expect(fetchMock.done()).toEqual(true);
        fetchMock.restore();
      });

      it('creates a properly formed resubscription event', () => {
        mockAllBadges();
        const badger = new BadgerService();
        return badger.init(TEST_CHANNEL_ID).then(() => {
          const event = resubscriptionEvent(
            'testerfield',
            TEST_USERSTATE['display-name'],
            false,
            3,
            TEST_MESSAGE,
            TEST_USERSTATE,
            badger.getBadgeData(TEST_USERSTATE.badges),
          );

          expect(event.type).toEqual(RESUBSCRIPTION);
          expect(event.isPrime).toEqual(false);
          expect(event.months).toEqual(3);
          expect(event.channel).toEqual('testerfield');
          expect(event.id).toExist();
          expect(event.badges!.length).toEqual(1);
          expect(event.user).toExist();
          expect(event.user!.username).toEqual(TEST_USERSTATE.username);
          expect(event.user!.usernameDisplay).toEqual(
            TEST_USERSTATE['display-name'],
          );
          expect(event.user!.color).toEqual(TEST_USERSTATE.color);
          expect(event.user!.isIntl).toEqual(false);
          expect(event.messageParts!.length).toEqual(2);
        });
      });
    });
  });

  describe('getUsernameDisplay', () => {
    it('passes through a good username-display', () => {
      const { usernameDisplay, isIntl } = getUsernameDisplay('Voxel', 'voxel');
      expect(usernameDisplay).toEqual('Voxel');
      expect(isIntl).toEqual(false);
    });

    it('falls back to username when username-display is missing', () => {
      const { usernameDisplay, isIntl } = getUsernameDisplay(null, 'voxel');
      expect(usernameDisplay).toEqual('voxel');
      expect(isIntl).toEqual(false);
    });

    it('shows both when detecting that username-display is international', () => {
      const { usernameDisplay, isIntl } = getUsernameDisplay('⚡️👹⚡️', 'voxel');
      expect(usernameDisplay).toEqual('⚡️👹⚡️');
      expect(isIntl).toEqual(true);
    });
  });

  describe('messagePartBuilder', () => {
    describe('text', () => {
      it('handles a text-only message', () => {
        const messageParts = messagePartBuilder('text', null, undefined);
        expect(messageParts.length).toEqual(1);
        expectTextPart(messageParts[0], 'text');
      });
    });

    describe('emotes', () => {
      it('handles an emote-only message', () => {
        const messageParts = messagePartBuilder(
          'Kappa',
          { 25: ['0-4'] },
          undefined,
        );
        expect(messageParts.length).toEqual(1);
        expectEmotePart(messageParts[0], 'Kappa');
      });

      it('handles a message with leading and trailing emotes', () => {
        const messageParts = messagePartBuilder(
          'Kappa text Kappa',
          { 25: ['0-4', '11-15'] },
          undefined,
        );
        expect(messageParts.length).toEqual(3);
        expectEmotePart(messageParts[0], 'Kappa');
        expectTextPart(messageParts[1], ' text ');
        expectEmotePart(messageParts[2], 'Kappa');
      });

      it('handles a message with multiple emotes', () => {
        const messageParts = messagePartBuilder(
          'PogChamp Kappa',
          { 25: ['9-13'], 88: ['0-7'] },
          undefined,
        );
        expect(messageParts.length).toEqual(3);
        expectEmotePart(messageParts[0], 'PogChamp');
        expectTextPart(messageParts[1], ' ');
        expectEmotePart(messageParts[2], 'Kappa');
      });

      it('handles a message with an emote in the middle', () => {
        const messageParts = messagePartBuilder(
          'text Kappa text',
          { 25: ['5-9'] },
          undefined,
        );
        expect(messageParts.length).toEqual(3);

        expectTextPart(messageParts[0], 'text ');
        expectEmotePart(messageParts[1], 'Kappa');
        expectTextPart(messageParts[2], ' text');
      });

      it('handles emotes in fancy text', () => {
        const messageParts = messagePartBuilder(
          'PogChamp PogChamp HOLD CTRL AND TYPE "WTF" FOR ℱ𝓪𝓷𝓬𝔂 𝓦𝓣ℱ PogChamp PogChamp',
          { 88: ['0-7', '9-16', '57-64', '66-73'] },
          undefined,
        );
        expect(messageParts.length).toEqual(7);
        expectEmotePart(messageParts[0], 'PogChamp');
        expectTextPart(messageParts[1], ' ');
        expectEmotePart(messageParts[2], 'PogChamp');
        expectTextPart(
          messageParts[3],
          ' HOLD CTRL AND TYPE "WTF" FOR ℱ𝓪𝓷𝓬𝔂 𝓦𝓣ℱ ',
        );
        expectEmotePart(messageParts[4], 'PogChamp');
        expectTextPart(messageParts[5], ' ');
        expectEmotePart(messageParts[6], 'PogChamp');
      });
    });

    describe('links', () => {
      it('handles a link-only message', () => {
        const messageParts = messagePartBuilder('google.com', null, undefined);
        expect(messageParts.length).toEqual(1);
        expectLinkPart(messageParts[0], 'google.com', 'https://google.com');
      });

      it('handles a message with leading and trailing links', () => {
        const messageParts = messagePartBuilder(
          'google.com and bing.com',
          null,
          undefined,
        );
        expect(messageParts.length).toEqual(3);
        expectLinkPart(messageParts[0], 'google.com', 'https://google.com');
        expectTextPart(messageParts[1], ' and ');
        expectLinkPart(messageParts[2], 'bing.com', 'https://bing.com');
      });

      it('handles a message with a link in the middle', () => {
        const messageParts = messagePartBuilder(
          'go to google.com now',
          null,
          undefined,
        );
        expect(messageParts.length).toEqual(3);
        expectTextPart(messageParts[0], 'go to ');
        expectLinkPart(messageParts[1], 'google.com', 'https://google.com');
        expectTextPart(messageParts[2], ' now');
      });

      it('handles a link-with-path-only message', () => {
        const messageParts = messagePartBuilder(
          'google.com/asdf',
          null,
          undefined,
        );
        expect(messageParts.length).toEqual(1);
        expectLinkPart(
          messageParts[0],
          'google.com/asdf',
          'https://google.com/asdf',
        );
      });

      it('handles a message with leading and trailing links-with-path', () => {
        const messageParts = messagePartBuilder(
          'google.com/asdf and bing.com/asdf',
          null,
          undefined,
        );
        expect(messageParts.length).toEqual(3);
        expectLinkPart(
          messageParts[0],
          'google.com/asdf',
          'https://google.com/asdf',
        );
        expectTextPart(messageParts[1], ' and ');
        expectLinkPart(
          messageParts[2],
          'bing.com/asdf',
          'https://bing.com/asdf',
        );
      });

      it('handles a message with a link-with-path in the middle', () => {
        const messageParts = messagePartBuilder(
          'go to google.com/asdf now',
          null,
          undefined,
        );
        expect(messageParts.length).toEqual(3);
        expectTextPart(messageParts[0], 'go to ');
        expectLinkPart(
          messageParts[1],
          'google.com/asdf',
          'https://google.com/asdf',
        );
        expectTextPart(messageParts[2], ' now');
      });

      it('handles a link-with-protocol-only message', () => {
        const messageParts = messagePartBuilder(
          'https://google.com',
          null,
          undefined,
        );
        expect(messageParts.length).toEqual(1);
        expectLinkPart(
          messageParts[0],
          'https://google.com',
          'https://google.com',
        );
      });

      it('handles a link with http protocol message', () => {
        const messageParts = messagePartBuilder(
          'http://google.com',
          null,
          undefined,
        );
        expect(messageParts.length).toEqual(1);
        expectLinkPart(
          messageParts[0],
          'http://google.com',
          'http://google.com',
        );
      });

      it('handles a link-with-protocol-and-path-only message', () => {
        const messageParts = messagePartBuilder(
          'https://google.com/asdf',
          null,
          undefined,
        );
        expect(messageParts.length).toEqual(1);
        expectLinkPart(
          messageParts[0],
          'https://google.com/asdf',
          'https://google.com/asdf',
        );
      });

      it('handles a message with leading and trailing links-with-protocol', () => {
        const messageParts = messagePartBuilder(
          'https://google.com and http://bing.com',
          null,
          undefined,
        );
        expect(messageParts.length).toEqual(3);
        expectLinkPart(
          messageParts[0],
          'https://google.com',
          'https://google.com',
        );
        expectTextPart(messageParts[1], ' and ');
        expectLinkPart(messageParts[2], 'http://bing.com', 'http://bing.com');
      });

      it('handles a message with a link-with-protocol in the middle', () => {
        const messageParts = messagePartBuilder(
          'go to https://google.com now',
          null,
          undefined,
        );
        expect(messageParts.length).toEqual(3);
        expectTextPart(messageParts[0], 'go to ');
        expectLinkPart(
          messageParts[1],
          'https://google.com',
          'https://google.com',
        );
        expectTextPart(messageParts[2], ' now');
      });

      it('handles a message with adjacent invalid chars', () => {
        const messageParts = messagePartBuilder(
          '`google.com`',
          null,
          undefined,
        );
        expect(messageParts.length).toEqual(3);
        expectTextPart(messageParts[0], '` ');
        expectLinkPart(messageParts[1], 'google.com', 'https://google.com');
        expectTextPart(messageParts[2], ' `');
      });

      it('handles a message with adjacent invalid chars with link in the middle', () => {
        const messageParts = messagePartBuilder(
          'try `google.com` this',
          null,
          undefined,
        );
        expect(messageParts.length).toEqual(3);
        expectTextPart(messageParts[0], 'try ` ');
        expectLinkPart(messageParts[1], 'google.com', 'https://google.com');
        expectTextPart(messageParts[2], ' ` this');
      });

      it('handles a message with 2 links in the middle', () => {
        const messageParts = messagePartBuilder(
          'try google.com and then bing.com now',
          null,
          undefined,
        );
        expect(messageParts.length).toEqual(5);
        expectTextPart(messageParts[0], 'try ');
        expectLinkPart(messageParts[1], 'google.com', 'https://google.com');
        expectTextPart(messageParts[2], ' and then ');
        expectLinkPart(messageParts[3], 'bing.com', 'https://bing.com');
        expectTextPart(messageParts[4], ' now');
      });

      it('handles multiple periods in a row', () => {
        const messageParts = messagePartBuilder(
          'go...ogle.com',
          null,
          undefined,
        );
        expect(messageParts.length).toEqual(2);
        expectTextPart(messageParts[0], 'go... ');
        expectLinkPart(messageParts[1], 'ogle.com', 'https://ogle.com');
      });

      it('allows all valid chars in domain and path respectively', () => {
        const messageParts = messagePartBuilder(
          'go@#%-_+=:~ogle.com/./@#%&()-_+=:?~',
          null,
          undefined,
        );
        expect(messageParts.length).toEqual(1);
        expectLinkPart(
          messageParts[0],
          'go@#%-_+=:~ogle.com/./@#%&()-_+=:?~',
          'https://go@#%-_+=:~ogle.com/./@#%&()-_+=:?~',
        );
      });

      it('disallows 1-char tlds', () => {
        const messageParts = messagePartBuilder('test.i', null, undefined);
        expect(messageParts.length).toEqual(1);
        expectTextPart(messageParts[0], 'test.i');
      });

      it('disallows 7-char tlds', () => {
        const messageParts = messagePartBuilder(
          'test.asdfasd',
          null,
          undefined,
        );
        expect(messageParts.length).toEqual(1);
        expectTextPart(messageParts[0], 'test.asdfasd');
      });

      it('allows 2-char tlds', () => {
        const messageParts = messagePartBuilder('test.as', null, undefined);
        expect(messageParts.length).toEqual(1);
        expectLinkPart(messageParts[0], 'test.as', 'https://test.as');
      });

      it('allows 6-char tlds', () => {
        const messageParts = messagePartBuilder('test.asdfas', null, undefined);
        expect(messageParts.length).toEqual(1);
        expectLinkPart(messageParts[0], 'test.asdfas', 'https://test.asdfas');
      });
    });

    describe('bits', () => {
      it('handles a bits-only message', () => {
        const messageParts = messagePartBuilder('Cheer100', null, 100);
        expect(messageParts.length).toEqual(1);

        expectBitsPart(messageParts[0], 'Cheer', 100, BITS_COLOR_MAP.purple);
      });

      it('handles a message with lowercase bits and titlecases alt', () => {
        const messageParts = messagePartBuilder('cheer100', null, 100);
        expect(messageParts.length).toEqual(1);

        expectBitsPart(messageParts[0], 'Cheer', 100, BITS_COLOR_MAP.purple);
      });

      it('handles a message with cRaZyCaSe bits and titlecases alt', () => {
        const messageParts = messagePartBuilder('cHeEr100', null, 100);
        expect(messageParts.length).toEqual(1);

        expectBitsPart(messageParts[0], 'Cheer', 100, BITS_COLOR_MAP.purple);
      });

      const bitColorCombinations = [
        { bits: 1, color: BITS_COLOR_MAP.gray, colorName: 'gray' },
        { bits: 99, color: BITS_COLOR_MAP.gray, colorName: 'gray' },
        { bits: 100, color: BITS_COLOR_MAP.purple, colorName: 'purple' },
        { bits: 999, color: BITS_COLOR_MAP.purple, colorName: 'purple' },
        { bits: 1000, color: BITS_COLOR_MAP.green, colorName: 'green' },
        { bits: 4999, color: BITS_COLOR_MAP.green, colorName: 'green' },
        { bits: 5000, color: BITS_COLOR_MAP.blue, colorName: 'blue' },
        { bits: 9999, color: BITS_COLOR_MAP.blue, colorName: 'blue' },
        { bits: 10000, color: BITS_COLOR_MAP.red, colorName: 'red' },
        { bits: 20000, color: BITS_COLOR_MAP.red, colorName: 'red' },
      ];

      bitColorCombinations.forEach(({ bits, color, colorName }) => {
        it(`handles a message with ${bits} bits and properly sets the color to ${colorName}`, () => {
          const messageParts = messagePartBuilder(`Cheer${bits}`, null, bits);
          expect(messageParts.length).toEqual(1);

          expectBitsPart(messageParts[0], 'Cheer', bits, color);
        });
      });

      const cheermoteNames = [
        'Streamlabs',
        'Muxy',
        'Kappa',
        'Pogchamp',
        'Kreygasm',
        'Swiftrage',
      ];

      cheermoteNames.forEach(cheermote => {
        it(`handles a ${cheermote} cheermote and sets the alt properly`, () => {
          const messageParts = messagePartBuilder(`${cheermote}100`, null, 100);
          expect(messageParts.length).toEqual(1);

          expectBitsPart(
            messageParts[0],
            cheermote,
            100,
            BITS_COLOR_MAP.purple,
          );
        });
      });

      it('handles a message with with leading and trailing bits', () => {
        const messageParts = messagePartBuilder(
          'Cheer100 text Cheer100',
          null,
          200,
        );
        expect(messageParts.length).toEqual(3);

        expectBitsPart(messageParts[0], 'Cheer', 100, BITS_COLOR_MAP.purple);
        expectTextPart(messageParts[1], ' text ');
        expectBitsPart(messageParts[2], 'Cheer', 100, BITS_COLOR_MAP.purple);
      });

      it('handles a message with bits in the middle', () => {
        const messageParts = messagePartBuilder(
          'text Cheer100 text',
          null,
          100,
        );
        expect(messageParts.length).toEqual(3);

        expectTextPart(messageParts[0], 'text ');
        expectBitsPart(messageParts[1], 'Cheer', 100, BITS_COLOR_MAP.purple);
        expectTextPart(messageParts[2], ' text');
      });

      it('handles a message with different cheermotes of different values', () => {
        const messageParts = messagePartBuilder(
          'Kappa200 text Pogchamp6000',
          null,
          6200,
        );
        expect(messageParts.length).toEqual(3);

        expectBitsPart(messageParts[0], 'Kappa', 200, BITS_COLOR_MAP.purple);
        expectTextPart(messageParts[1], ' text ');
        expectBitsPart(messageParts[2], 'Pogchamp', 6000, BITS_COLOR_MAP.blue);
      });
    });

    describe('mixed', () => {
      it('handles a message with leading bits and trailing emote', () => {
        const messageParts = messagePartBuilder(
          'Cheer100 text Kappa',
          { 25: ['14-18'] },
          100,
        );
        expect(messageParts.length).toEqual(3);

        expectBitsPart(messageParts[0], 'Cheer', 100, BITS_COLOR_MAP.purple);
        expectTextPart(messageParts[1], ' text ');
        expectEmotePart(messageParts[2], 'Kappa');
      });

      it('handles a message with bits and an emote in the middle', () => {
        const messageParts = messagePartBuilder(
          'text Cheer100 text Kappa text',
          { 25: ['19-23'] },
          100,
        );
        expect(messageParts.length).toEqual(5);

        expectTextPart(messageParts[0], 'text ');
        expectBitsPart(messageParts[1], 'Cheer', 100, BITS_COLOR_MAP.purple);
        expectTextPart(messageParts[2], ' text ');
        expectEmotePart(messageParts[3], 'Kappa');
        expectTextPart(messageParts[4], ' text');
      });

      it('handles cheer & emotes with unicode', () => {
        const messageParts = messagePartBuilder(
          'PogChamp PogChamp Cheer100 HOLD CTRL AND TYPE "WTF" FOR ℱ𝓪𝓷𝓬𝔂 𝓦𝓣ℱ PogChamp PogChamp Cheer100',
          { 88: ['0-7', '9-16', '66-73', '75-82'] },
          200,
        );
        expectEmotePart(messageParts[0], 'PogChamp');
        expectTextPart(messageParts[1], ' ');
        expectEmotePart(messageParts[2], 'PogChamp');
        expectTextPart(messageParts[3], ' ');
        expectBitsPart(messageParts[4], 'Cheer', 100, BITS_COLOR_MAP.purple);
        expectTextPart(
          messageParts[5],
          ' HOLD CTRL AND TYPE "WTF" FOR ℱ𝓪𝓷𝓬𝔂 𝓦𝓣ℱ ',
        );
        expectEmotePart(messageParts[6], 'PogChamp');
        expectTextPart(messageParts[7], ' ');
        expectEmotePart(messageParts[8], 'PogChamp');
        expectTextPart(messageParts[9], ' ');
        expectBitsPart(messageParts[10], 'Cheer', 100, BITS_COLOR_MAP.purple);
        expect(messageParts.length).toEqual(11);
      });

      it('handles cheer, links, & emotes with unicode', () => {
        const messageParts = messagePartBuilder(
          'google.com PogChamp PogChamp Cheer100 HOLD CTRL AND TYPE "WTF" FOR ℱ𝓪𝓷𝓬𝔂 𝓦𝓣ℱ t.co PogChamp PogChamp Cheer100 twitch.tv/mrrywthr',
          { 88: ['11-18', '20-27', '82-89', '91-98'] },
          200,
        );
        expectLinkPart(messageParts[0], 'google.com', 'https://google.com');
        expectTextPart(messageParts[1], ' ');
        expectEmotePart(messageParts[2], 'PogChamp');
        expectTextPart(messageParts[3], ' ');
        expectEmotePart(messageParts[4], 'PogChamp');
        expectTextPart(messageParts[5], ' ');
        expectBitsPart(messageParts[6], 'Cheer', 100, BITS_COLOR_MAP.purple);
        expectTextPart(
          messageParts[7],
          ' HOLD CTRL AND TYPE "WTF" FOR ℱ𝓪𝓷𝓬𝔂 𝓦𝓣ℱ ',
        );
        expectLinkPart(messageParts[8], 't.co', 'https://t.co');
        expectTextPart(messageParts[9], ' ');
        expectEmotePart(messageParts[10], 'PogChamp');
        expectTextPart(messageParts[11], ' ');
        expectEmotePart(messageParts[12], 'PogChamp');
        expectTextPart(messageParts[13], ' ');
        expectBitsPart(messageParts[14], 'Cheer', 100, BITS_COLOR_MAP.purple);
        expectTextPart(messageParts[15], ' ');
        expectLinkPart(
          messageParts[16],
          'twitch.tv/mrrywthr',
          'https://twitch.tv/mrrywthr',
        );
        expect(messageParts.length).toEqual(17);
      });
    });
  });
});

function expectTextPart(part: MessagePart, expectedString: string): void {
  expect(part.type).toEqual('TEXT');
  expect(part.content).toEqual(expectedString);
}

function expectLinkPart(
  part: MessagePart,
  expectedDisplayText: string,
  expectedURL: string,
): void {
  expect(part.type).toEqual('LINK');
  expect((part.content as LinkData).displayText).toEqual(expectedDisplayText);
  expect((part.content as LinkData).url).toEqual(expectedURL);
}

function expectEmotePart(part: MessagePart, expectedAlt: string): void {
  expect(part.type).toEqual('EMOTE');
  expect(Object.keys((part.content as EmoteData).images).length).toEqual(3);
  expect((part.content as EmoteData).alt).toEqual(expectedAlt);
}

function expectBitsPart(
  part: MessagePart,
  expectedAlt: string,
  expectedCheerAmount: number,
  expectedCheerColor: string,
): void {
  expect(part.type).toEqual('EMOTE');
  expect(Object.keys((part.content as EmoteData).images).length).toEqual(5);
  expect((part.content as EmoteData).alt).toEqual(expectedAlt);
  expect((part.content as EmoteData).cheerAmount).toEqual(expectedCheerAmount);
  expect((part.content as EmoteData).cheerColor).toEqual(expectedCheerColor);
}
