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

import { assert } from 'chai';
import { v4 as uuid } from 'uuid';
import type { MessageAttributesType } from '../../model-types.d';
import { MessageModel } from '../../models/messages';
import { strictAssert } from '../../util/assert';

import { MessageCache } from '../../services/MessageCache';

describe('MessageCache', () => {
  describe('filterBySentAt', () => {
    it('returns an empty iterable if no messages match', () => {
      const mc = new MessageCache();

      assert.isEmpty([...mc.__DEPRECATED$filterBySentAt(123)]);
    });

    it('returns all messages that match the timestamp', () => {
      const mc = new MessageCache();

      let message1 = new MessageModel({
        conversationId: 'xyz',
        body: 'message1',
        id: uuid(),
        received_at: 1,
        sent_at: 1234,
        timestamp: 9999,
        type: 'incoming',
      });
      let message2 = new MessageModel({
        conversationId: 'xyz',
        body: 'message2',
        id: uuid(),
        received_at: 2,
        sent_at: 1234,
        timestamp: 9999,
        type: 'outgoing',
      });
      const message3 = new MessageModel({
        conversationId: 'xyz',
        body: 'message3',
        id: uuid(),
        received_at: 3,
        sent_at: 5678,
        timestamp: 9999,
        type: 'outgoing',
      });

      message1 = mc.__DEPRECATED$register(message1.id, message1, 'test');
      message2 = mc.__DEPRECATED$register(message2.id, message2, 'test');
      // We deliberately register this message twice for testing.
      message2 = mc.__DEPRECATED$register(message2.id, message2, 'test');
      mc.__DEPRECATED$register(message3.id, message3, 'test');

      const filteredMessages = Array.from(
        mc.__DEPRECATED$filterBySentAt(1234)
      ).map(x => x.attributes);

      assert.deepEqual(
        filteredMessages,
        [message1.attributes, message2.attributes],
        'first'
      );

      mc.__DEPRECATED$unregister(message2.id);

      const filteredMessages2 = Array.from(
        mc.__DEPRECATED$filterBySentAt(1234)
      ).map(x => x.attributes);

      assert.deepEqual(filteredMessages2, [message1.attributes], 'second');
    });
  });

  describe('__DEPRECATED$register: syncing with backbone', () => {
    it('backbone to redux', () => {
      const message1 = new MessageModel({
        conversationId: 'xyz',
        id: uuid(),
        body: 'test1',
        received_at: 1,
        sent_at: Date.now(),
        timestamp: Date.now(),
        type: 'outgoing',
      });
      const messageFromController = window.MessageCache.__DEPRECATED$register(
        message1.id,
        message1,
        'test'
      );

      assert.strictEqual(
        message1,
        messageFromController,
        'same objects from mc.__DEPRECATED$register'
      );

      const messageById = window.MessageCache.__DEPRECATED$getById(message1.id);

      assert.strictEqual(message1, messageById, 'same objects from mc.getById');

      const messageInCache = window.MessageCache.accessAttributes(message1.id);
      strictAssert(messageInCache, 'no message found');
      assert.deepEqual(
        message1.attributes,
        messageInCache,
        'same attributes as in cache'
      );

      message1.set({ body: 'test2' });
      assert.equal(message1.attributes.body, 'test2', 'message model updated');
      assert.equal(
        messageById?.attributes.body,
        'test2',
        'old reference from messageById was updated'
      );
      assert.equal(
        messageInCache.body,
        'test1',
        'old cache reference not updated'
      );

      const newMessageById = window.MessageCache.__DEPRECATED$getById(
        message1.id
      );
      assert.deepEqual(
        message1.attributes,
        newMessageById?.attributes,
        'same attributes from mc.getById (2)'
      );

      const newMessageInCache = window.MessageCache.accessAttributes(
        message1.id
      );
      strictAssert(newMessageInCache, 'no message found');
      assert.deepEqual(
        message1.attributes,
        newMessageInCache,
        'same attributes as in cache (2)'
      );
    });

    it('redux to backbone (working with models)', () => {
      const message = new MessageModel({
        conversationId: 'xyz',
        id: uuid(),
        body: 'test1',
        received_at: 1,
        sent_at: Date.now(),
        timestamp: Date.now(),
        type: 'outgoing',
      });

      window.MessageCache.toMessageAttributes(message.attributes);

      const messageFromController = window.MessageCache.__DEPRECATED$register(
        message.id,
        message,
        'test'
      );

      assert.notStrictEqual(
        message,
        messageFromController,
        'mc.__DEPRECATED$register returns existing but it is not the same reference'
      );
      assert.deepEqual(
        message.attributes,
        messageFromController.attributes,
        'mc.__DEPRECATED$register returns existing and is the same attributes'
      );

      messageFromController.set({ body: 'test2' });

      assert.notEqual(
        message.get('body'),
        messageFromController.get('body'),
        'new model is not equal to old model'
      );

      const messageInCache = window.MessageCache.accessAttributes(message.id);
      strictAssert(messageInCache, 'no message found');
      assert.equal(
        messageFromController.get('body'),
        messageInCache.body,
        'new update is in cache'
      );

      assert.isUndefined(
        messageFromController.get('storyReplyContext'),
        'storyReplyContext is undefined'
      );

      window.MessageCache.setAttributes({
        messageId: message.id,
        messageAttributes: {
          storyReplyContext: {
            attachment: undefined,
            authorAci: undefined,
            messageId: 'test123',
          },
        },
        skipSaveToDatabase: true,
      });

      // This works because we refresh the model whenever an attribute changes
      // but this should log a warning.
      assert.equal(
        messageFromController.get('storyReplyContext')?.messageId,
        'test123',
        'storyReplyContext was updated (stale model)'
      );

      const newMessageFromController =
        window.MessageCache.__DEPRECATED$register(message.id, message, 'test');

      assert.equal(
        newMessageFromController.get('storyReplyContext')?.messageId,
        'test123',
        'storyReplyContext was updated (not stale)'
      );
    });

    it('redux to backbone (working with attributes)', () => {
      it('sets the attributes and returns a fresh copy', () => {
        const mc = new MessageCache();

        const messageAttributes: MessageAttributesType = {
          conversationId: uuid(),
          id: uuid(),
          received_at: 1,
          sent_at: Date.now(),
          timestamp: Date.now(),
          type: 'incoming',
        };

        const messageModel = mc.__DEPRECATED$register(
          messageAttributes.id,
          messageAttributes,
          'test/updateAttributes'
        );

        assert.deepEqual(
          messageAttributes,
          messageModel.attributes,
          'initial attributes matches message model'
        );

        const proposedStoryReplyContext = {
          attachment: undefined,
          authorAci: undefined,
          messageId: 'test123',
        };

        assert.notDeepEqual(
          messageModel.attributes.storyReplyContext,
          proposedStoryReplyContext,
          'attributes were changed outside of the message model'
        );

        mc.setAttributes({
          messageId: messageAttributes.id,
          messageAttributes: {
            storyReplyContext: proposedStoryReplyContext,
          },
          skipSaveToDatabase: true,
        });

        const nextMessageAttributes = mc.accessAttributesOrThrow(
          'test',
          messageAttributes.id
        );

        assert.notDeepEqual(
          messageAttributes,
          nextMessageAttributes,
          'initial attributes are stale'
        );
        assert.notDeepEqual(
          messageAttributes.storyReplyContext,
          proposedStoryReplyContext,
          'initial attributes are stale 2'
        );

        assert.deepEqual(
          nextMessageAttributes.storyReplyContext,
          proposedStoryReplyContext,
          'fresh attributes match what was proposed'
        );
        assert.notStrictEqual(
          nextMessageAttributes.storyReplyContext,
          proposedStoryReplyContext,
          'fresh attributes are not the same reference as proposed attributes'
        );

        assert.deepEqual(
          messageModel.attributes,
          nextMessageAttributes,
          'model was updated'
        );

        assert.equal(
          messageModel.get('storyReplyContext')?.messageId,
          'test123',
          'storyReplyContext in model is set correctly'
        );
      });
    });
  });

  describe('accessAttributes', () => {
    it('gets the attributes if they exist', () => {
      const mc = new MessageCache();

      const messageAttributes: MessageAttributesType = {
        conversationId: uuid(),
        id: uuid(),
        received_at: 1,
        sent_at: Date.now(),
        timestamp: Date.now(),
        type: 'incoming',
      };

      mc.toMessageAttributes(messageAttributes);

      const accessAttributes = mc.accessAttributes(messageAttributes.id);

      assert.deepEqual(
        accessAttributes,
        messageAttributes,
        'attributes returned have the same values'
      );
      assert.notStrictEqual(
        accessAttributes,
        messageAttributes,
        'attributes returned are not the same references'
      );

      const undefinedMessage = mc.accessAttributes(uuid());
      assert.isUndefined(undefinedMessage, 'access did not find message');
    });
  });

  describe('accessAttributesOrThrow', () => {
    it('accesses the attributes or throws if they do not exist', () => {
      const mc = new MessageCache();

      const messageAttributes: MessageAttributesType = {
        conversationId: uuid(),
        id: uuid(),
        received_at: 1,
        sent_at: Date.now(),
        timestamp: Date.now(),
        type: 'incoming',
      };

      mc.toMessageAttributes(messageAttributes);

      const accessAttributes = mc.accessAttributesOrThrow(
        'tests.1',
        messageAttributes.id
      );

      assert.deepEqual(
        accessAttributes,
        messageAttributes,
        'attributes returned have the same values'
      );
      assert.notStrictEqual(
        accessAttributes,
        messageAttributes,
        'attributes returned are not the same references'
      );

      assert.throws(() => {
        mc.accessAttributesOrThrow('tests.2', uuid());
      });
    });
  });
});
