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

import { v4 as generateGuid } from 'uuid';

import { SendStatus } from '../../messages/MessageSendState.std.js';
import type { ConversationModel } from '../../models/conversations.preload.js';
import { GiftBadgeStates } from '../../types/GiftBadgeStates.std.js';

import { DataWriter } from '../../sql/Client.preload.js';
import { getRandomBytes } from '../../Crypto.node.js';
import * as Bytes from '../../Bytes.std.js';
import { generateAci } from '../../types/ServiceId.std.js';
import { ReadStatus } from '../../messages/MessageReadStatus.std.js';
import { SeenStatus } from '../../MessageSeenStatus.std.js';
import { ID_V1_LENGTH } from '../../types/groups.std.js';
import { DurationInSeconds, WEEK } from '../../util/durations/index.std.js';
import {
  setupBasics,
  asymmetricRoundtripHarness,
  symmetricRoundtripHarness,
  OUR_ACI,
} from './helpers.preload.js';
import { loadAllAndReinitializeRedux } from '../../services/allLoaders.preload.js';
import { strictAssert } from '../../util/assert.std.js';
import type { MessageAttributesType } from '../../model-types.d.ts';
import { IMAGE_PNG, TEXT_ATTACHMENT } from '../../types/MIME.std.js';
import { MY_STORY_ID } from '../../types/Stories.std.js';
import { generateAttachmentKeys } from '../../AttachmentCrypto.node.js';
import { itemStorage } from '../../textsecure/Storage.preload.js';
import { BodyRange } from '../../types/BodyRange.std.js';

const CONTACT_A = generateAci();
const CONTACT_B = generateAci();
const CONTACT_B_E164 = '+12135550123';
const GV1_ID = Bytes.toBinary(getRandomBytes(ID_V1_LENGTH));

const BADGE_RECEIPT =
  'AEpyZxbRBT+T5PQw9Wcx1QE2aFvL7LoLir9V4UF09Kk9qiP4SpIlHdlWHrAICy6F' +
  '6WdbdCj45fY6cadDKbBmkw+abohRTJnItrFhyKurnA5X+mZHZv4OvS+aZFmAYS6J' +
  'W+hpkbI+Fk7Gu3mEix7Pgz1I2EwGFlUBpm7/nuD5A0cKLrUJAMM142fnOEervePV' +
  'bf0c6Sw5X5aCsBw9J+dxFUGAAAAAAAAAAMH58UUeUj2oH1jfqc0Hb2RUtdA3ee8X' +
  '0Pp83WT8njwFw5rNGSHeKqOvBZzfAhMGJoiz7l1XfIfsPIreaFb/tA9aq2bOAdDl' +
  '5OYlxxl6DnjQ3+g3k9ycpl0elkaQnPW2Ai7yjeJ/96K1qssR2a/2b7xi10dmTRGg' +
  'gebhZnroYYgIgK22ZgAAAABkAAAAAAAAAD9j4f77Xo2Ox5tVyrV2DUo=';

describe('backup/bubble messages', () => {
  let contactA: ConversationModel;
  let contactB: ConversationModel;
  let gv1: ConversationModel;

  beforeEach(async () => {
    await DataWriter._removeAllMessages();
    await DataWriter._removeAllConversations();
    itemStorage.reset();

    await setupBasics();

    contactA = await window.ConversationController.getOrCreateAndWait(
      CONTACT_A,
      'private',
      { systemGivenName: 'CONTACT_A', active_at: 1 }
    );
    contactB = await window.ConversationController.getOrCreateAndWait(
      CONTACT_B,
      'private',
      { systemGivenName: 'CONTACT_B', e164: CONTACT_B_E164, active_at: 1 }
    );

    gv1 = await window.ConversationController.getOrCreateAndWait(
      GV1_ID,
      'group',
      {
        groupVersion: 1,
        active_at: 1,
      }
    );

    await loadAllAndReinitializeRedux();
  });

  it('roundtrips incoming edited message', async () => {
    await symmetricRoundtripHarness([
      {
        conversationId: contactA.id,
        id: generateGuid(),
        type: 'incoming',
        received_at: 3,
        received_at_ms: 3,
        sent_at: 3,
        timestamp: 3,
        sourceServiceId: CONTACT_A,
        body: 'd',
        readStatus: ReadStatus.Unread,
        seenStatus: SeenStatus.Unseen,
        unidentifiedDeliveryReceived: true,
        editMessageTimestamp: 5,
        editMessageReceivedAtMs: 5,
        editHistory: [
          {
            body: 'd',
            timestamp: 5,
            received_at: 5,
            received_at_ms: 5,
            readStatus: ReadStatus.Unread,
            unidentifiedDeliveryReceived: true,
          },
          {
            body: 'c',
            timestamp: 4,
            received_at: 4,
            received_at_ms: 4,
            readStatus: ReadStatus.Unread,
            unidentifiedDeliveryReceived: false,
          },
          {
            body: 'b',
            timestamp: 3,
            received_at: 3,
            received_at_ms: 3,
            readStatus: ReadStatus.Read,
            unidentifiedDeliveryReceived: false,
          },
        ],
      },
    ]);
  });

  it('roundtrips outgoing edited message', async () => {
    await symmetricRoundtripHarness([
      {
        conversationId: contactA.id,
        id: generateGuid(),
        type: 'outgoing',
        readStatus: ReadStatus.Read,
        received_at: 3,
        received_at_ms: 43,
        seenStatus: SeenStatus.Seen,
        sent_at: 3,
        sourceServiceId: OUR_ACI,
        sendStateByConversationId: {
          [contactA.id]: {
            status: SendStatus.Delivered,
          },
        },
        unidentifiedDeliveries: [CONTACT_A],
        timestamp: 3,
        editMessageTimestamp: 5,
        editMessageReceivedAtMs: 45,
        body: 'd',
        editHistory: [
          {
            body: 'd',
            timestamp: 5,
            received_at: 5,
            received_at_ms: 45,
            sendStateByConversationId: {
              [contactA.id]: {
                status: SendStatus.Delivered,
              },
            },
          },
          {
            body: 'c',
            timestamp: 4,
            received_at: 4,
            received_at_ms: 44,
            sendStateByConversationId: {
              [contactA.id]: {
                status: SendStatus.Viewed,
              },
            },
          },
          {
            body: 'b',
            timestamp: 3,
            received_at: 3,
            received_at_ms: 43,
            sendStateByConversationId: {
              [contactA.id]: {
                status: SendStatus.Viewed,
              },
            },
          },
        ],
      },
    ]);
  });

  it('roundtrips unopened gift badge', async () => {
    await symmetricRoundtripHarness([
      {
        conversationId: contactA.id,
        id: generateGuid(),
        type: 'incoming',
        received_at: 3,
        received_at_ms: 3,
        sent_at: 3,
        sourceServiceId: CONTACT_A,
        readStatus: ReadStatus.Unread,
        seenStatus: SeenStatus.Unseen,
        unidentifiedDeliveryReceived: true,
        timestamp: 3,
        giftBadge: {
          id: undefined,
          level: 100,
          expiration: 1723248000000,
          receiptCredentialPresentation: BADGE_RECEIPT,
          state: GiftBadgeStates.Opened,
        },
      },
    ]);
  });

  it('roundtrips opened gift badge', async () => {
    await symmetricRoundtripHarness([
      {
        conversationId: contactA.id,
        id: generateGuid(),
        type: 'incoming',
        received_at: 3,
        received_at_ms: 3,
        sent_at: 3,
        sourceServiceId: CONTACT_A,
        readStatus: ReadStatus.Unread,
        seenStatus: SeenStatus.Unseen,
        unidentifiedDeliveryReceived: true,
        timestamp: 3,
        giftBadge: {
          id: undefined,
          level: 100,
          expiration: 1723248000000,
          receiptCredentialPresentation: BADGE_RECEIPT,
          state: GiftBadgeStates.Opened,
        },
      },
    ]);
  });

  it('fixes e164-only incoming 1:1 messages', async () => {
    await asymmetricRoundtripHarness(
      [
        {
          conversationId: contactA.id,
          id: generateGuid(),
          type: 'incoming',
          received_at: 3,
          received_at_ms: 3,
          sent_at: 3,
          // Note: contact B e164 vs contact A conversationId
          source: CONTACT_B_E164,
          readStatus: ReadStatus.Unread,
          seenStatus: SeenStatus.Unseen,
          unidentifiedDeliveryReceived: true,
          timestamp: 3,
          body: 'hello',
        },
      ],
      [
        {
          conversationId: contactA.id,
          id: generateGuid(),
          type: 'incoming',
          received_at: 3,
          received_at_ms: 3,
          sent_at: 3,
          sourceServiceId: CONTACT_A,
          readStatus: ReadStatus.Unread,
          seenStatus: SeenStatus.Unseen,
          unidentifiedDeliveryReceived: true,
          timestamp: 3,
          body: 'hello',
        },
      ]
    );
  });

  describe('quotes', () => {
    it('roundtrips gift badge quote', async () => {
      await symmetricRoundtripHarness([
        {
          conversationId: contactA.id,
          id: generateGuid(),
          type: 'incoming',
          received_at: 3,
          received_at_ms: 3,
          sent_at: 3,
          sourceServiceId: CONTACT_A,
          readStatus: ReadStatus.Unread,
          seenStatus: SeenStatus.Unseen,
          unidentifiedDeliveryReceived: true,
          timestamp: 3,
          giftBadge: {
            id: undefined,
            level: 100,
            expiration: 1723248000000,
            receiptCredentialPresentation: BADGE_RECEIPT,
            state: GiftBadgeStates.Opened,
          },
        },
        {
          conversationId: contactA.id,
          id: generateGuid(),
          type: 'incoming',
          received_at: 4,
          received_at_ms: 4,
          sent_at: 4,
          sourceServiceId: CONTACT_A,
          readStatus: ReadStatus.Unread,
          seenStatus: SeenStatus.Unseen,
          unidentifiedDeliveryReceived: true,
          timestamp: 4,
          body: '123',
          quote: {
            authorAci: CONTACT_A,
            attachments: [],
            id: 3,
            isViewOnce: false,
            isGiftBadge: true,
            referencedMessageNotFound: false,
          },
        },
      ]);
    });
    it('roundtrips quote with referenced message found', async () => {
      await symmetricRoundtripHarness([
        {
          conversationId: contactA.id,
          id: generateGuid(),
          type: 'incoming',
          received_at: 3,
          received_at_ms: 3,
          sent_at: 3,
          sourceServiceId: CONTACT_A,
          readStatus: ReadStatus.Unread,
          seenStatus: SeenStatus.Unseen,
          unidentifiedDeliveryReceived: true,
          timestamp: 3,
          body: '123',
          quote: {
            authorAci: CONTACT_A,
            attachments: [],
            id: 42,
            isGiftBadge: false,
            text: 'quote text',
            isViewOnce: false,
            referencedMessageNotFound: false,
          },
        },
      ]);
    });
    it('roundtrips quote without referenced message found', async () => {
      const message = {
        conversationId: contactA.id,
        id: generateGuid(),
        type: 'incoming',
        received_at: 3,
        received_at_ms: 3,
        sent_at: 3,
        sourceServiceId: CONTACT_A,
        readStatus: ReadStatus.Unread,
        seenStatus: SeenStatus.Unseen,
        unidentifiedDeliveryReceived: true,
        timestamp: 3,
        body: '123',
        quote: {
          authorAci: CONTACT_A,
          attachments: [],
          id: 42,
          text: 'quote text',
          isViewOnce: false,
          isGiftBadge: false,
          referencedMessageNotFound: true,
        },
      } as const;

      await asymmetricRoundtripHarness(
        [message],
        [
          {
            ...message,
            quote: {
              ...message.quote,
              // id is removed during roundtrip
              id: null,
            },
          },
        ]
      );
    });

    it('roundtrips view-once-quotes', async () => {
      const message = {
        conversationId: contactA.id,
        id: generateGuid(),
        type: 'incoming',
        received_at: 3,
        received_at_ms: 3,
        sent_at: 3,
        sourceServiceId: CONTACT_A,
        readStatus: ReadStatus.Unread,
        seenStatus: SeenStatus.Unseen,
        unidentifiedDeliveryReceived: true,
        timestamp: 3,
        body: '123',
        quote: {
          authorAci: CONTACT_A,
          attachments: [],
          id: 42,
          text: 'quote text',
          isViewOnce: true,
          isGiftBadge: false,
          referencedMessageNotFound: true,
        },
      } as const;

      await asymmetricRoundtripHarness(
        [message],
        [
          {
            ...message,
            quote: {
              ...message.quote,
              // id is removed during roundtrip
              id: null,
            },
          },
        ]
      );
    });
  });

  it('roundtrips sealed/unsealed incoming message', async () => {
    await symmetricRoundtripHarness([
      {
        conversationId: contactA.id,
        id: generateGuid(),
        type: 'incoming',
        received_at: 3,
        received_at_ms: 3,
        sent_at: 3,
        sourceServiceId: CONTACT_A,
        readStatus: ReadStatus.Unread,
        seenStatus: SeenStatus.Unseen,
        unidentifiedDeliveryReceived: false,
        timestamp: 3,
        body: 'unsealed',
      },
      {
        conversationId: contactA.id,
        id: generateGuid(),
        type: 'incoming',
        received_at: 4,
        received_at_ms: 4,
        sent_at: 4,
        sourceServiceId: CONTACT_A,
        readStatus: ReadStatus.Unread,
        seenStatus: SeenStatus.Unseen,
        unidentifiedDeliveryReceived: true,
        timestamp: 4,
        body: 'sealed',
      },
    ]);
  });

  it('roundtrips sealed/unsealed outgoing message', async () => {
    await symmetricRoundtripHarness([
      {
        conversationId: contactA.id,
        id: generateGuid(),
        type: 'outgoing',
        received_at: 3,
        received_at_ms: 4,
        sent_at: 3,
        sourceServiceId: OUR_ACI,
        sendStateByConversationId: {
          [contactA.id]: {
            status: SendStatus.Delivered,
          },
        },
        unidentifiedDeliveries: undefined,
        timestamp: 3,
        body: 'unsealed',
      },
      {
        conversationId: contactA.id,
        id: generateGuid(),
        type: 'outgoing',
        readStatus: ReadStatus.Read,
        received_at: 4,
        received_at_ms: 4,
        seenStatus: SeenStatus.Seen,
        sent_at: 4,
        sourceServiceId: OUR_ACI,
        sendStateByConversationId: {
          [contactA.id]: {
            status: SendStatus.Delivered,
          },
        },
        unidentifiedDeliveries: [CONTACT_A],
        timestamp: 4,
        body: 'sealed',
      },
    ]);
  });

  it('roundtrips messages with send errors', async () => {
    await symmetricRoundtripHarness([
      {
        conversationId: contactA.id,
        id: generateGuid(),
        type: 'outgoing',
        received_at: 3,
        received_at_ms: 3,
        sent_at: 3,
        sourceServiceId: OUR_ACI,
        sendStateByConversationId: {
          [contactA.id]: {
            status: SendStatus.Delivered,
          },
          [contactB.id]: {
            status: SendStatus.Failed,
          },
        },
        errors: [
          {
            serviceId: CONTACT_B,
            name: 'OutgoingIdentityKeyError',
            message: `The identity of ${CONTACT_B} has changed.`,
          },
        ],
        timestamp: 3,
        body: 'body',
      },
      {
        conversationId: contactA.id,
        id: generateGuid(),
        type: 'outgoing',
        readStatus: ReadStatus.Read,
        received_at: 4,
        received_at_ms: 4,
        seenStatus: SeenStatus.Seen,
        sent_at: 4,
        sourceServiceId: OUR_ACI,
        sendStateByConversationId: {
          [contactA.id]: {
            status: SendStatus.Failed,
          },
          [contactB.id]: {
            status: SendStatus.Delivered,
          },
        },
        errors: [
          {
            serviceId: CONTACT_A,
            name: 'OutgoingMessageError',
            message: 'no http error',
          },
        ],
        timestamp: 4,
        body: 'body',
      },
    ]);
  });

  it('roundtrips sms messages', async () => {
    await symmetricRoundtripHarness([
      {
        conversationId: contactA.id,
        id: generateGuid(),
        type: 'incoming',
        received_at: 3,
        received_at_ms: 3,
        sent_at: 3,
        timestamp: 3,
        sourceServiceId: CONTACT_A,
        body: 'd',
        readStatus: ReadStatus.Unread,
        seenStatus: SeenStatus.Unseen,
        unidentifiedDeliveryReceived: true,
        sms: true,
      },
    ]);
  });

  it('drops gv1 messages', async () => {
    await asymmetricRoundtripHarness(
      [
        {
          conversationId: gv1.id,
          id: generateGuid(),
          type: 'incoming',
          received_at: 3,
          received_at_ms: 3,
          sent_at: 3,
          timestamp: 3,
          sourceServiceId: CONTACT_A,
          body: 'd',
          readStatus: ReadStatus.Unread,
          seenStatus: SeenStatus.Unseen,
          unidentifiedDeliveryReceived: true,
        },
      ],
      []
    );
  });
  it('drops erased messages', async () => {
    await asymmetricRoundtripHarness(
      [
        {
          conversationId: contactA.id,
          id: generateGuid(),
          type: 'incoming',
          received_at: 3,
          received_at_ms: 3,
          isErased: true,
          sent_at: 3,
          timestamp: 3,
          sourceServiceId: CONTACT_A,
          body: 'd',
          readStatus: ReadStatus.Unread,
          seenStatus: SeenStatus.Unseen,
          unidentifiedDeliveryReceived: true,
        },
      ],
      []
    );
  });
  it('drops invalid body ranges', async () => {
    await asymmetricRoundtripHarness(
      [
        {
          conversationId: contactA.id,
          id: generateGuid(),
          type: 'incoming',
          received_at: 3,
          received_at_ms: 3,
          sent_at: 3,
          timestamp: 3,
          sourceServiceId: CONTACT_A,
          body: 'd',
          bodyRanges: [
            {
              start: 0,
              length: 1,
              // @ts-expect-error invalid data
              style: undefined,
            },
            {
              start: 1,
              length: 0,
              style: BodyRange.Style.BOLD,
            },
          ],
          readStatus: ReadStatus.Unread,
          seenStatus: SeenStatus.Unseen,
          unidentifiedDeliveryReceived: true,
        },
      ],
      [
        {
          conversationId: contactA.id,
          id: generateGuid(),
          type: 'incoming',
          received_at: 3,
          received_at_ms: 3,
          sent_at: 3,
          timestamp: 3,
          sourceServiceId: CONTACT_A,
          body: 'd',
          bodyRanges: [
            {
              start: 1,
              length: 0,
              style: BodyRange.Style.BOLD,
            },
          ],
          readStatus: ReadStatus.Unread,
          seenStatus: SeenStatus.Unseen,
          unidentifiedDeliveryReceived: true,
        },
      ]
    );
  });

  it('drops messages that expire soon', async () => {
    await asymmetricRoundtripHarness(
      [
        {
          conversationId: contactA.id,
          id: generateGuid(),
          type: 'incoming',
          received_at: 3,
          received_at_ms: 3,
          sent_at: 3,
          timestamp: 3,
          sourceServiceId: CONTACT_A,
          body: 'd',
          readStatus: ReadStatus.Unread,
          seenStatus: SeenStatus.Unseen,
          unidentifiedDeliveryReceived: true,
          expirationStartTimestamp: Date.now(),
          expireTimer: DurationInSeconds.fromSeconds(1),
        },
      ],
      []
    );
  });

  it('does not drop messages that expire far in the future', async () => {
    await symmetricRoundtripHarness([
      {
        conversationId: contactA.id,
        id: generateGuid(),
        type: 'incoming',
        received_at: 3,
        received_at_ms: 3,
        sent_at: 3,
        timestamp: 3,
        sourceServiceId: CONTACT_A,
        body: 'd',
        readStatus: ReadStatus.Unread,
        seenStatus: SeenStatus.Unseen,
        unidentifiedDeliveryReceived: true,
        expirationStartTimestamp: Date.now(),
        expireTimer: DurationInSeconds.fromMillis(WEEK),
      },
    ]);
  });

  it('drops messages that have not started to expire but have an expireTimer of <= DAY', async () => {
    await asymmetricRoundtripHarness(
      [
        {
          conversationId: contactA.id,
          id: generateGuid(),
          type: 'incoming',
          received_at: 3,
          received_at_ms: 3,
          sent_at: 3,
          timestamp: 3,
          sourceServiceId: CONTACT_A,
          body: 'd',
          readStatus: ReadStatus.Unread,
          seenStatus: SeenStatus.Unseen,
          unidentifiedDeliveryReceived: true,
          expireTimer: DurationInSeconds.fromDays(1),
        },
      ],
      []
    );
  });

  it('does not drop messages that have not started to expire but have an expireTimer of > DAY', async () => {
    await symmetricRoundtripHarness([
      {
        conversationId: contactA.id,
        id: generateGuid(),
        type: 'incoming',
        received_at: 3,
        received_at_ms: 3,
        sent_at: 3,
        timestamp: 3,
        sourceServiceId: CONTACT_A,
        body: 'd',
        readStatus: ReadStatus.Unread,
        seenStatus: SeenStatus.Unseen,
        unidentifiedDeliveryReceived: true,
        expireTimer: DurationInSeconds.fromDays(1.01),
      },
    ]);
  });

  describe('link previews', async () => {
    it('roundtrips link preview', async () => {
      await symmetricRoundtripHarness([
        {
          conversationId: contactA.id,
          id: generateGuid(),
          type: 'incoming',
          received_at: 3,
          received_at_ms: 3,
          sent_at: 3,
          timestamp: 3,
          sourceServiceId: CONTACT_A,
          body: 'https://signal.org is a cool place',
          readStatus: ReadStatus.Unread,
          seenStatus: SeenStatus.Unseen,
          unidentifiedDeliveryReceived: true,
          preview: [
            {
              url: 'https://signal.org',
              title: 'Signal',
            },
          ],
        },
      ]);
    });
    it('drops preview if URL does not exist in body', async () => {
      const message: MessageAttributesType = {
        conversationId: contactA.id,
        id: generateGuid(),
        type: 'incoming',
        received_at: 3,
        received_at_ms: 3,
        sent_at: 3,
        timestamp: 3,
        sourceServiceId: CONTACT_A,
        body: 'no urls here',
        readStatus: ReadStatus.Unread,
        seenStatus: SeenStatus.Unseen,
        unidentifiedDeliveryReceived: true,
      };
      await asymmetricRoundtripHarness(
        [
          {
            ...message,
            preview: [
              {
                url: 'https://signal.org',
                title: 'Signal',
              },
            ],
          },
        ],
        [
          {
            ...message,
            preview: [],
          },
        ]
      );
    });
  });
  describe('lonely-in-group messages', async () => {
    const GROUP_ID = Bytes.toBase64(getRandomBytes(32));
    let group: ConversationModel | undefined;
    let ourConversation: ConversationModel | undefined;

    beforeEach(async () => {
      group = await window.ConversationController.getOrCreateAndWait(
        GROUP_ID,
        'group',
        {
          groupVersion: 2,
          masterKey: Bytes.toBase64(getRandomBytes(32)),
          name: 'Rock Enthusiasts',
          active_at: 1,
        }
      );
      ourConversation = window.ConversationController.get(OUR_ACI);
    });

    it('roundtrips messages that have our id in sendStateByConversationId', async () => {
      strictAssert(group, 'conversations exist');
      strictAssert(ourConversation, 'conversations exist');
      await symmetricRoundtripHarness([
        {
          conversationId: group.id,
          id: generateGuid(),
          type: 'outgoing',
          received_at: 3,
          received_at_ms: 3,
          sent_at: 3,
          timestamp: 3,
          sourceServiceId: OUR_ACI,
          body: 'd',
          readStatus: ReadStatus.Read,
          seenStatus: SeenStatus.Seen,
          sendStateByConversationId: {
            [ourConversation.id]: { status: SendStatus.Read, updatedAt: 3 },
          },
          expirationStartTimestamp: Date.now(),
          expireTimer: DurationInSeconds.fromMillis(WEEK),
        },
      ]);
    });
    it(
      'if a message did not have sendStateByConversationId (e.g. to mimic post-import from primary), ' +
        'would add it with our conversationId when importing',
      async () => {
        strictAssert(group, 'conversations exist');
        strictAssert(ourConversation, 'conversations exist');
        const message: MessageAttributesType = {
          conversationId: group.id,
          id: generateGuid(),
          type: 'outgoing',
          received_at: 3,
          received_at_ms: 3,
          sent_at: 3,
          timestamp: 3,
          sourceServiceId: OUR_ACI,
          body: 'd',
          readStatus: ReadStatus.Read,
          seenStatus: SeenStatus.Seen,
          expirationStartTimestamp: Date.now(),
          expireTimer: DurationInSeconds.fromMillis(WEEK),
        };

        await asymmetricRoundtripHarness(
          [
            {
              ...message,
              sendStateByConversationId: {},
            },
          ],
          [
            {
              ...message,
              sendStateByConversationId: {
                [ourConversation.id]: { status: SendStatus.Read, updatedAt: 3 },
              },
            },
          ]
        );
      }
    );
    it('filters out our conversation id from sendStateByConversationId in non-note-to-self convos', async () => {
      strictAssert(group, 'conversations exist');
      strictAssert(ourConversation, 'conversations exist');
      const message: MessageAttributesType = {
        conversationId: group.id,
        id: generateGuid(),
        type: 'outgoing',
        received_at: 3,
        received_at_ms: 3,
        sent_at: 3,
        timestamp: 3,
        sourceServiceId: OUR_ACI,
        body: 'd',
        readStatus: ReadStatus.Read,
        seenStatus: SeenStatus.Seen,
        expirationStartTimestamp: Date.now(),
        expireTimer: DurationInSeconds.fromMillis(WEEK),
      };

      await asymmetricRoundtripHarness(
        [
          {
            ...message,
            sendStateByConversationId: {
              [ourConversation.id]: { status: SendStatus.Read, updatedAt: 3 },
              [contactA.id]: { status: SendStatus.Delivered, updatedAt: 4 },
            },
          },
        ],
        [
          {
            ...message,
            sendStateByConversationId: {
              [contactA.id]: { status: SendStatus.Delivered, updatedAt: 4 },
            },
          },
        ]
      );
    });
    it('does not filter out our conversation id from sendStateByConversationId in Note-to-Self', async () => {
      strictAssert(ourConversation, 'conversations exist');
      const message: MessageAttributesType = {
        conversationId: ourConversation.id,
        id: generateGuid(),
        type: 'outgoing',
        received_at: 3,
        received_at_ms: 3,
        sent_at: 3,
        timestamp: 3,
        sourceServiceId: OUR_ACI,
        body: 'd',
        readStatus: ReadStatus.Read,
        seenStatus: SeenStatus.Seen,
        expirationStartTimestamp: Date.now(),
        expireTimer: DurationInSeconds.fromMillis(WEEK),
      };
      ourConversation.set({ active_at: 3 });

      await symmetricRoundtripHarness([
        {
          ...message,
          sendStateByConversationId: {
            [ourConversation.id]: { status: SendStatus.Read, updatedAt: 3 },
          },
        },
      ]);
    });
  });
  describe('stories', () => {
    const GROUP_ID = Bytes.toBase64(getRandomBytes(32));
    let group: ConversationModel | undefined;
    let ourConversation: ConversationModel | undefined;

    beforeEach(async () => {
      group = await window.ConversationController.getOrCreateAndWait(
        GROUP_ID,
        'group',
        {
          groupVersion: 2,
          masterKey: Bytes.toBase64(getRandomBytes(32)),
          name: 'Rock Enthusiasts',
          active_at: 1,
        }
      );
      ourConversation = window.ConversationController.get(OUR_ACI);
    });
    it('does not export stories', async () => {
      strictAssert(ourConversation, 'conversations exist');
      strictAssert(group, 'conversations exist');
      const commonProps = {
        type: 'story',
        received_at: 3,
        received_at_ms: 3,
        sent_at: 3,
        timestamp: 3,
        sourceServiceId: OUR_ACI,
        attachments: [
          {
            contentType: TEXT_ATTACHMENT,
            size: 4,
            textAttachment: {
              color: 4285041620,
              text: 'test',
              textForegroundColor: 4294967295,
              textStyle: 1,
            },
          },
        ],
        readStatus: ReadStatus.Read,
        seenStatus: SeenStatus.Seen,
        expirationStartTimestamp: Date.now(),
        expireTimer: DurationInSeconds.fromMillis(WEEK),
      } satisfies Partial<MessageAttributesType>;

      const directStory: MessageAttributesType = {
        ...commonProps,
        id: generateGuid(),
        conversationId: ourConversation.id,
        storyDistributionListId: MY_STORY_ID,
      };

      const groupStory: MessageAttributesType = {
        ...commonProps,
        id: generateGuid(),
        conversationId: group.id,
      };

      await asymmetricRoundtripHarness([directStory, groupStory], []);
    });
    it('roundtrips direct story emoji replies', async () => {
      strictAssert(ourConversation, 'conversations exist');
      const commonProps = {
        received_at: 3,
        received_at_ms: 3,
        sent_at: 3,
        timestamp: 3,
        readStatus: ReadStatus.Read,
        seenStatus: SeenStatus.Seen,
        conversationId: contactA.id,
      } satisfies Partial<MessageAttributesType>;

      const incomingReply: MessageAttributesType = {
        ...commonProps,
        id: generateGuid(),
        type: 'incoming',
        unidentifiedDeliveryReceived: true,
        sourceServiceId: CONTACT_A,
        storyReaction: {
          emoji: '🤷‍♂️',
          targetAuthorAci: OUR_ACI,
          targetTimestamp: 0, // targetTimestamp is not roundtripped
        },
        storyReplyContext: {
          authorAci: OUR_ACI,
          messageId: '',
        },
      };

      const outgoingReply: MessageAttributesType = {
        ...commonProps,
        id: generateGuid(),
        type: 'outgoing',
        sourceServiceId: OUR_ACI,
        storyReaction: {
          emoji: '🤷‍♂️',
          targetAuthorAci: CONTACT_A,
          targetTimestamp: 0, // targetTimestamp is not roundtripped
        },
        storyReplyContext: {
          authorAci: CONTACT_A,
          messageId: '',
        },
        sendStateByConversationId: {
          [CONTACT_A]: {
            status: SendStatus.Read,
            updatedAt: 3,
          },
        },
      };

      await symmetricRoundtripHarness([incomingReply, outgoingReply]);
    });
    it('roundtrips direct story text replies', async () => {
      strictAssert(ourConversation, 'conversations exist');
      const commonProps = {
        received_at: 3,
        received_at_ms: 3,
        sent_at: 3,
        timestamp: 3,
        readStatus: ReadStatus.Read,
        seenStatus: SeenStatus.Seen,
        conversationId: contactA.id,
        body: 'text reply to story',
      } satisfies Partial<MessageAttributesType>;

      const incomingReply: MessageAttributesType = {
        ...commonProps,
        id: generateGuid(),
        type: 'incoming',
        unidentifiedDeliveryReceived: true,
        sourceServiceId: CONTACT_A,
        storyReplyContext: {
          authorAci: OUR_ACI,
          messageId: '',
        },
      };

      const outgoingReply: MessageAttributesType = {
        ...commonProps,
        id: generateGuid(),
        type: 'outgoing',
        sourceServiceId: OUR_ACI,
        storyReplyContext: {
          authorAci: CONTACT_A,
          messageId: '',
        },
        sendStateByConversationId: {
          [CONTACT_A]: {
            status: SendStatus.Read,
            updatedAt: 3,
          },
        },
      };

      await symmetricRoundtripHarness([incomingReply, outgoingReply]);
    });

    it('does not export group story replies', async () => {
      strictAssert(ourConversation, 'conversations exist');
      strictAssert(group, 'conversations exist');
      const commonProps = {
        conversationId: group.id,
        received_at: 3,
        received_at_ms: 3,
        sent_at: 3,
        timestamp: 3,
        readStatus: ReadStatus.Read,
        seenStatus: SeenStatus.Seen,
        body: 'text reply to story',
      } satisfies Partial<MessageAttributesType>;

      const incomingReply: MessageAttributesType = {
        ...commonProps,
        id: generateGuid(),
        type: 'incoming',
        unidentifiedDeliveryReceived: true,
        sourceServiceId: CONTACT_A,
        storyReplyContext: {
          authorAci: OUR_ACI,
          messageId: '',
        },
      };

      const outgoingReply: MessageAttributesType = {
        ...commonProps,
        id: generateGuid(),
        type: 'outgoing',
        sourceServiceId: OUR_ACI,
        storyReplyContext: {
          authorAci: CONTACT_A,
          messageId: '',
        },
        sendStateByConversationId: {
          [CONTACT_A]: {
            status: SendStatus.Read,
            updatedAt: 3,
          },
        },
      };

      await asymmetricRoundtripHarness([incomingReply, outgoingReply], []);
    });
  });

  describe('view-once', () => {
    it('roundtrips incoming viewed view-once message', async () => {
      await symmetricRoundtripHarness([
        {
          conversationId: contactA.id,
          id: generateGuid(),
          type: 'incoming',
          isErased: true,
          isViewOnce: true,
          received_at: 3,
          received_at_ms: 3,
          sent_at: 3,
          sourceServiceId: CONTACT_A,
          readStatus: ReadStatus.Viewed,
          seenStatus: SeenStatus.Seen,
          unidentifiedDeliveryReceived: false,
          timestamp: 3,
        },
      ]);
    });
    it('roundtrips incoming unviewed view-once message', async () => {
      await symmetricRoundtripHarness([
        {
          conversationId: contactA.id,
          id: generateGuid(),
          type: 'incoming',
          isViewOnce: true,
          attachments: [
            {
              size: 128,
              contentType: IMAGE_PNG,
              cdnKey: 'cdnKey',
              cdnNumber: 2,
              uploadTimestamp: 2001,
              key: Bytes.toBase64(generateAttachmentKeys()),
              digest: Bytes.toBase64(getRandomBytes(32)),
              caption: 'shhhh',
            },
          ],
          received_at: 3,
          received_at_ms: 3,
          sent_at: 3,
          sourceServiceId: CONTACT_A,
          readStatus: ReadStatus.Unread,
          seenStatus: SeenStatus.Unseen,
          unidentifiedDeliveryReceived: false,
          timestamp: 3,
        },
      ]);
    });
    it('roundtrips outgoing view-once message', async () => {
      await symmetricRoundtripHarness([
        {
          conversationId: contactA.id,
          id: generateGuid(),
          type: 'outgoing',
          isViewOnce: true,
          isErased: true,
          received_at: 3,
          received_at_ms: 3,
          sent_at: 3,
          sourceServiceId: OUR_ACI,
          seenStatus: SeenStatus.Seen,
          sendStateByConversationId: {
            [contactA.id]: {
              status: SendStatus.Delivered,
            },
          },
          unidentifiedDeliveries: [CONTACT_A],
          timestamp: 3,
        },
      ]);
    });
  });

  describe('polls', () => {
    const GROUP_ID = Bytes.toBase64(getRandomBytes(32));
    let group: ConversationModel | undefined;

    let basePollMessage: MessageAttributesType;

    beforeEach(async () => {
      group = await window.ConversationController.getOrCreateAndWait(
        GROUP_ID,
        'group',
        {
          groupVersion: 2,
          masterKey: Bytes.toBase64(getRandomBytes(32)),
          name: 'Poll Test Group',
          active_at: 1,
        }
      );

      strictAssert(group, 'group must exist');

      basePollMessage = {
        conversationId: group.id,
        id: generateGuid(),
        type: 'incoming',
        received_at: 3,
        received_at_ms: 3,
        sent_at: 3,
        sourceServiceId: CONTACT_A,
        readStatus: ReadStatus.Unread,
        seenStatus: SeenStatus.Unseen,
        unidentifiedDeliveryReceived: true,
        timestamp: 3,
        poll: {
          question: '',
          options: [],
          allowMultiple: false,
          votes: undefined,
          terminatedAt: undefined,
        },
      };
    });

    it('roundtrips poll with no votes', async () => {
      await symmetricRoundtripHarness([
        {
          ...basePollMessage,
          poll: {
            question: 'How do you feel about unit testing?',
            options: ['yay', 'ok', 'nay'],
            allowMultiple: false,
          },
        },
      ]);
    });

    it('roundtrips poll with single vote', async () => {
      await symmetricRoundtripHarness([
        {
          ...basePollMessage,
          poll: {
            question: 'How do you feel about unit testing?',
            options: ['yay', 'ok', 'nay'],
            allowMultiple: false,
            votes: [
              {
                fromConversationId: contactA.id,
                optionIndexes: [0], // Voted for "yay"
                voteCount: 1,
                timestamp: 3,
              },
            ],
          },
        },
      ]);
    });

    it('roundtrips poll with multiple voters', async () => {
      await symmetricRoundtripHarness([
        {
          ...basePollMessage,
          poll: {
            question: 'Pizza toppings?',
            options: ['pepperoni', 'mushrooms', 'pineapple'],
            allowMultiple: false,
            votes: [
              {
                fromConversationId: contactA.id,
                optionIndexes: [0], // contactA voted for pepperoni
                voteCount: 2, // Changed their vote twice
                timestamp: 3,
              },
              {
                fromConversationId: contactB.id,
                optionIndexes: [2], // contactB voted for pineapple
                voteCount: 1,
                timestamp: 3,
              },
            ],
          },
        },
      ]);
    });

    it('roundtrips poll with multiple selections', async () => {
      await symmetricRoundtripHarness([
        {
          ...basePollMessage,
          poll: {
            question: 'Which features do you want?',
            options: ['dark mode', 'better search', 'polls', 'voice notes'],
            allowMultiple: true,
            votes: [
              {
                fromConversationId: contactA.id,
                optionIndexes: [0, 2, 3], // Selected dark mode, polls, and voice notes
                voteCount: 1,
                timestamp: 3,
              },
            ],
          },
        },
      ]);
    });

    it('roundtrips ended poll', async () => {
      const pollData = {
        poll: {
          question: 'This poll is closed',
          options: ['option1', 'option2'],
          allowMultiple: false,
          votes: [
            {
              fromConversationId: contactA.id,
              optionIndexes: [0],
              voteCount: 1,
              timestamp: 3,
            },
          ],
        },
      };

      await asymmetricRoundtripHarness(
        [
          {
            ...basePollMessage,
            ...pollData,
            poll: {
              ...pollData.poll,
              terminatedAt: 5, // Original termination timestamp
            },
          },
        ],
        [
          {
            ...basePollMessage,
            ...pollData,
            poll: {
              ...pollData.poll,
              terminatedAt: 3, // After roundtrip, set to message timestamp
            },
          },
        ]
      );
    });

    it('roundtrips outgoing poll', async () => {
      await symmetricRoundtripHarness([
        {
          ...basePollMessage,
          type: 'outgoing',
          sourceServiceId: OUR_ACI,
          readStatus: ReadStatus.Read,
          seenStatus: SeenStatus.Seen,
          unidentifiedDeliveryReceived: false, // Outgoing messages default to false
          sendStateByConversationId: {
            [contactA.id]: {
              status: SendStatus.Delivered,
            },
          },
          poll: {
            question: 'Meeting time?',
            options: ['10am', '2pm', '4pm'],
            allowMultiple: false,
            votes: [
              {
                fromConversationId: contactA.id,
                optionIndexes: [1],
                voteCount: 1,
                timestamp: 3,
              },
            ],
          },
        },
      ]);
    });

    it('excludes pending votes from backup', async () => {
      strictAssert(group, 'group must exist');
      const ourConversation = window.ConversationController.get(OUR_ACI);
      strictAssert(ourConversation, 'our conversation must exist');

      await asymmetricRoundtripHarness(
        [
          {
            ...basePollMessage,
            poll: {
              question: 'Test question?',
              options: ['yes', 'no'],
              allowMultiple: false,
              votes: [
                {
                  fromConversationId: contactA.id,
                  optionIndexes: [0],
                  voteCount: 1,
                  timestamp: 3,
                  // No sendStateByConversationId - this vote is sent
                },
                {
                  fromConversationId: ourConversation.id,
                  optionIndexes: [1],
                  voteCount: 1,
                  timestamp: 3,
                  // This vote is still pending, should NOT be in backup
                  sendStateByConversationId: {
                    [contactA.id]: {
                      status: SendStatus.Pending,
                      updatedAt: 3,
                    },
                  },
                },
              ],
            },
          },
        ],
        [
          {
            ...basePollMessage,
            poll: {
              question: 'Test question?',
              options: ['yes', 'no'],
              allowMultiple: false,
              votes: [
                {
                  fromConversationId: contactA.id,
                  optionIndexes: [0],
                  voteCount: 1,
                  timestamp: 3,
                  // Only the sent vote should remain after roundtrip
                },
                // The pending vote should be excluded from the backup
              ],
            },
          },
        ]
      );
    });

    it('roundtrips poll with reactions', async () => {
      await symmetricRoundtripHarness([
        {
          ...basePollMessage,
          poll: {
            question: 'React to this poll?',
            options: ['yes', 'no'],
            allowMultiple: false,
          },
          reactions: [
            {
              emoji: '👍',
              fromId: contactA.id,
              targetTimestamp: 3,
              timestamp: 3,
            },
            {
              emoji: '❤️',
              fromId: contactB.id,
              targetTimestamp: 3,
              timestamp: 3,
            },
          ],
        },
      ]);
    });

    it('roundtrips quote of poll', async () => {
      await symmetricRoundtripHarness([
        {
          ...basePollMessage,
          poll: {
            question: 'Original poll?',
            options: ['option1', 'option2'],
            allowMultiple: false,
          },
        },
        {
          ...basePollMessage,
          id: generateGuid(), // Generate new ID to avoid duplicate
          sent_at: 4,
          timestamp: 4,
          body: 'Replying to poll',
          poll: undefined,
          quote: {
            id: 3,
            authorAci: CONTACT_A,
            text: 'Original poll?',
            attachments: [],
            isGiftBadge: false,
            isPoll: true,
            isViewOnce: false,
            referencedMessageNotFound: false,
          },
        },
      ]);
    });
  });
});
