import expect from 'expect';

import {
  buildTestMessageEvent,
  buildTestModerationEvent,
  buildTestSubscriptionEvent,
  buildTestResubscriptionEvent,
  buildTestConnectionStatusMessage,
  buildTestHostingStatusMessage,
} from 'mtest/chat/helpers/chatObjects';

import { RECONNECT, DISCONNECTED, UNHOST } from 'mweb/chat/events/statusEvent';
import ChatBuffer from 'mweb/chat/chatBuffer';
import { MessageEvent } from 'mweb/chat/events/messageEvent';

describe('ChatBuffer', () => {
  let chatBuffer: ChatBuffer;
  beforeEach(() => {
    chatBuffer = new ChatBuffer(2);
  });

  it('constructs with empty buffer and no moderated users', () => {
    expect(chatBuffer.length).toEqual(0);
    expect(chatBuffer.moderatedUserCount).toEqual(0);
  });

  it('adds a single chat message to the buffer and updates dirty state', () => {
    const event = buildTestMessageEvent();
    expect(chatBuffer.isDirty).toEqual(false);

    chatBuffer.consumeChatEvent({ data: event });
    expect(chatBuffer.length).toEqual(1);
    expect(chatBuffer.moderatedUserCount).toEqual(0);
    expect(chatBuffer.isDirty).toEqual(true);

    const bufferArray = chatBuffer.toArray();
    expect(bufferArray[0]).toBe(event);
    expect(chatBuffer.isDirty).toEqual(false);
  });

  it('adds messages to fill the buffer without rolling over', () => {
    const events = [buildTestMessageEvent(), buildTestMessageEvent()];
    events.forEach(event => chatBuffer.consumeChatEvent({ data: event }));

    expect(chatBuffer.length).toEqual(2);
    expect(chatBuffer.moderatedUserCount).toEqual(0);

    const bufferArray = chatBuffer.toArray();
    expect(bufferArray[0]).toBe(events[0]);
    expect(bufferArray[1]).toBe(events[1]);
  });

  it('adds messages to overflow the buffer and truncates while preserving order', () => {
    const events = [
      buildTestMessageEvent(),
      buildTestMessageEvent(),
      buildTestMessageEvent(),
    ];
    events.forEach(event => chatBuffer.consumeChatEvent({ data: event }));

    expect(chatBuffer.length).toEqual(3);
    expect(chatBuffer.moderatedUserCount).toEqual(0);

    const bufferArray = chatBuffer.toArray();
    expect(bufferArray[0]).toBe(events[1]);
    expect(bufferArray[1]).toBe(events[2]);
    expect(bufferArray.length).toEqual(2);
    expect(chatBuffer.length).toEqual(2);
  });

  it('moderates a single message', () => {
    const chatEvent = buildTestMessageEvent();
    chatBuffer.consumeChatEvent({ data: chatEvent });
    expect(chatEvent.deleted).toEqual(false);

    chatBuffer.consumeChatEvent({ data: buildTestModerationEvent() });

    expect(chatBuffer.length).toEqual(2);
    expect(chatEvent.deleted).toEqual(true);
    expect(chatBuffer.moderatedUserCount).toEqual(1);
  });

  it('moderates multiple messages and truncates as necessary', () => {
    const events = [
      buildTestMessageEvent(),
      buildTestMessageEvent(),
      buildTestMessageEvent(),
    ];
    events.forEach(event => chatBuffer.consumeChatEvent({ data: event }));

    const modEvent = buildTestModerationEvent();
    chatBuffer.consumeChatEvent({ data: modEvent });
    events.forEach(event => expect(event.deleted).toEqual(true));

    const bufferArray = chatBuffer.toArray();
    expect(bufferArray[0]).toBe(events[2]);
    expect((bufferArray[0] as MessageEvent).deleted).toBe(true);
    expect(bufferArray[1]).toBe(modEvent);
    expect(chatBuffer.moderatedUserCount).toEqual(1);
  });

  it('does not stack duplicate moderation messages', () => {
    const chatEvent = buildTestMessageEvent();
    chatBuffer.consumeChatEvent({ data: chatEvent });
    expect(chatEvent.deleted).toEqual(false);

    const modEvent = buildTestModerationEvent();
    chatBuffer.consumeChatEvent({ data: modEvent });
    chatBuffer.consumeChatEvent({ data: buildTestModerationEvent() });

    expect(chatBuffer.length).toEqual(2);
    expect(chatEvent.deleted).toEqual(true);

    const bufferArray = chatBuffer.toArray();
    expect(bufferArray[0]).toBe(chatEvent);
    expect(bufferArray[1]).toBe(modEvent);
    expect(chatBuffer.moderatedUserCount).toEqual(1);
  });

  it('only moderates messages for the moderated user', () => {
    const chatEventUser1 = buildTestMessageEvent();
    const chatEventUser2 = buildTestMessageEvent('anotherUser');

    chatBuffer.consumeChatEvent({ data: chatEventUser1 });
    chatBuffer.consumeChatEvent({ data: chatEventUser2 });

    chatBuffer.consumeChatEvent({ data: buildTestModerationEvent() });

    expect(chatEventUser1.deleted).toEqual(true);
    expect(chatEventUser2.deleted).toEqual(false);
    expect(chatBuffer.moderatedUserCount).toEqual(1);
  });

  it('does not error when first message received is moderation', () => {
    const modEvent = buildTestModerationEvent();
    chatBuffer.consumeChatEvent({ data: modEvent });

    expect(chatBuffer.length).toEqual(1);
    expect(chatBuffer.moderatedUserCount).toEqual(1);
  });

  it('clears the moderated user list after duration', () => {
    chatBuffer.consumeChatEvent({ data: buildTestModerationEvent() });
    expect(chatBuffer.moderatedUserCount).toEqual(1);

    return new Promise(resolve => {
      setTimeout(() => {
        expect(chatBuffer.moderatedUserCount).toEqual(0);
        resolve();
      }, 0.001);
    });
  });

  it('handles multiple rounds of consumption with overflow and toArray() calls', () => {
    expect(chatBuffer.isDirty).toEqual(false);

    chatBuffer.consumeChatEvent({ data: buildTestMessageEvent() });

    expect(chatBuffer.length).toEqual(1);
    expect(chatBuffer.isDirty).toEqual(true);

    chatBuffer.toArray();

    expect(chatBuffer.length).toEqual(1);
    expect(chatBuffer.isDirty).toEqual(false);

    chatBuffer.consumeChatEvent({ data: buildTestMessageEvent() });
    chatBuffer.consumeChatEvent({ data: buildTestMessageEvent() });
    chatBuffer.consumeChatEvent({ data: buildTestMessageEvent() });

    expect(chatBuffer.length).toEqual(4);
    expect(chatBuffer.isDirty).toEqual(true);

    chatBuffer.toArray();

    expect(chatBuffer.length).toEqual(2);
    expect(chatBuffer.isDirty).toEqual(false);
  });

  it('adds a single subscription message to the buffer and updates dirty state', () => {
    const event = buildTestSubscriptionEvent();
    expect(chatBuffer.isDirty).toEqual(false);

    chatBuffer.consumeChatEvent({ data: event });
    expect(chatBuffer.length).toEqual(1);
    expect(chatBuffer.moderatedUserCount).toEqual(0);
    expect(chatBuffer.isDirty).toEqual(true);

    const bufferArray = chatBuffer.toArray();
    expect(bufferArray[0]).toBe(event);
    expect(chatBuffer.isDirty).toEqual(false);
  });

  it('adds a single resubscription message to the buffer and updates dirty state', () => {
    const event = buildTestResubscriptionEvent();
    expect(chatBuffer.isDirty).toEqual(false);

    chatBuffer.consumeChatEvent({ data: event });
    expect(chatBuffer.length).toEqual(1);
    expect(chatBuffer.moderatedUserCount).toEqual(0);
    expect(chatBuffer.isDirty).toEqual(true);

    const bufferArray = chatBuffer.toArray();
    expect(bufferArray[0]).toBe(event);
    expect(chatBuffer.isDirty).toEqual(false);
  });

  it('moderates a single resubscription message', () => {
    const subEvent = buildTestResubscriptionEvent(undefined, true);
    chatBuffer.consumeChatEvent({ data: subEvent });
    expect(subEvent.deleted).toEqual(false);

    chatBuffer.consumeChatEvent({ data: buildTestModerationEvent() });

    expect(chatBuffer.length).toEqual(2);
    expect(subEvent.deleted).toEqual(true);
    expect(chatBuffer.moderatedUserCount).toEqual(1);
  });

  it('adds connected status events to the buffer and updates dirty state', () => {
    const event = buildTestConnectionStatusMessage();
    expect(chatBuffer.isDirty).toEqual(false);

    chatBuffer.consumeChatEvent({ data: event });
    expect(chatBuffer.length).toEqual(1);
    expect(chatBuffer.moderatedUserCount).toEqual(0);
    expect(chatBuffer.isDirty).toEqual(true);

    const bufferArray = chatBuffer.toArray();
    expect(bufferArray[0]).toBe(event);
    expect(chatBuffer.isDirty).toEqual(false);
  });

  it('adds reconnection status events to the buffer and updates dirty state', () => {
    const event = buildTestConnectionStatusMessage(RECONNECT);
    expect(chatBuffer.isDirty).toEqual(false);

    chatBuffer.consumeChatEvent({ data: event });
    expect(chatBuffer.length).toEqual(1);
    expect(chatBuffer.moderatedUserCount).toEqual(0);
    expect(chatBuffer.isDirty).toEqual(true);

    const bufferArray = chatBuffer.toArray();
    expect(bufferArray[0]).toBe(event);
    expect(chatBuffer.isDirty).toEqual(false);
  });

  it('does not add duplicate reconnection status events to the buffer', () => {
    chatBuffer.consumeChatEvent({
      data: buildTestConnectionStatusMessage(RECONNECT),
    });
    chatBuffer.consumeChatEvent({
      data: buildTestConnectionStatusMessage(RECONNECT),
    });
    expect(chatBuffer.length).toEqual(1);
  });

  it('does not add disconnected events to the buffer', () => {
    chatBuffer.consumeChatEvent({
      data: buildTestConnectionStatusMessage(DISCONNECTED),
    });
    expect(chatBuffer.length).toEqual(0);
  });

  it('adds hosting status events to the buffer and updates dirty state', () => {
    const event = buildTestHostingStatusMessage();
    expect(chatBuffer.isDirty).toEqual(false);

    chatBuffer.consumeChatEvent({ data: event });
    expect(chatBuffer.length).toEqual(1);
    expect(chatBuffer.moderatedUserCount).toEqual(0);
    expect(chatBuffer.isDirty).toEqual(true);

    const bufferArray = chatBuffer.toArray();
    expect(bufferArray[0]).toBe(event);
    expect(chatBuffer.isDirty).toEqual(false);
  });

  it('adds unhost status events to the buffer and updates dirty state', () => {
    const event = buildTestHostingStatusMessage(UNHOST);
    expect(chatBuffer.isDirty).toEqual(false);

    chatBuffer.consumeChatEvent({ data: event });
    expect(chatBuffer.length).toEqual(1);
    expect(chatBuffer.moderatedUserCount).toEqual(0);
    expect(chatBuffer.isDirty).toEqual(true);

    const bufferArray = chatBuffer.toArray();
    expect(bufferArray[0]).toBe(event);
    expect(chatBuffer.isDirty).toEqual(false);
  });
});
