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

import { assert } from 'chai';
import * as sinon from 'sinon';
import { v4 as uuid } from 'uuid';

import { handleDataMessage } from '../../messages/handleDataMessage.preload.ts';
import type { MessageAttributesType } from '../../model-types.d.ts';
import { MessageModel } from '../../models/messages.preload.ts';
import { MessageCache } from '../../services/MessageCache.preload.ts';
import { DataWriter } from '../../sql/Client.preload.ts';
import { itemStorage } from '../../textsecure/Storage.preload.ts';
import type { ProcessedDataMessage } from '../../textsecure/Types.d.ts';
import type { AciString } from '../../types/ServiceId.std.ts';
import { DurationInSeconds } from '../../util/durations/duration-in-seconds.std.ts';
import { SignalService } from '../../protobuf/index.std.ts';
import {
  generateAci,
  generatePni,
} from '../../test-helpers/serviceIdUtils.std.ts';

describe('handleDataMessage', () => {
  let ourAci: AciString;

  beforeEach(async () => {
    ourAci = generateAci();
    MessageCache.install();
    await itemStorage.user.setAciAndDeviceId(ourAci, 1);
    await itemStorage.user.setPni(generatePni());

    window.ConversationController.reset();
    MessageCache.install();
    await window.ConversationController.load();
  });

  afterEach(async () => {
    await DataWriter.removeAll();
    await itemStorage.fetch();
    window.ConversationController.reset();
  });

  it('deduplicates incoming messages with same sender/timestamp, if existing message saved to DB', async () => {
    const senderAci = generateAci();
    const conversation = await window.ConversationController.getOrCreateAndWait(
      senderAci,
      'private'
    );
    const sentAt = Date.now();

    const existingAttributes: MessageAttributesType = {
      id: uuid(),
      conversationId: conversation.id,
      type: 'incoming',
      sourceServiceId: senderAci,
      sourceDevice: 1,
      sent_at: sentAt,
      timestamp: sentAt,
      received_at: sentAt,
    };

    await DataWriter.saveMessage(existingAttributes, {
      ourAci,
      forceSave: true,
      postSaveUpdates: () => Promise.resolve(),
    });

    const dataMessage: ProcessedDataMessage = {
      attachments: [],
      flags: 0,
      body: 'body',
      expireTimer: DurationInSeconds.fromDays(0),
      expireTimerVersion: 1,
      isViewOnce: false,
      timestamp: sentAt,
      requiredProtocolVersion:
        SignalService.DataMessage.ProtocolVersion.CURRENT,
    };

    const duplicateMessage = new MessageModel({
      id: uuid(),
      conversationId: conversation.id,
      type: 'incoming',
      sourceServiceId: senderAci,
      sourceDevice: 2,
      sent_at: sentAt,
      timestamp: sentAt,
      received_at: sentAt + 1,
    });

    const saveAndNotify = sinon.stub();
    const confirm = sinon.stub();

    await handleDataMessage(
      duplicateMessage,
      dataMessage,
      confirm,
      {},
      { saveAndNotify }
    );

    assert.strictEqual(saveAndNotify.callCount, 0, 'not saved');
    assert.strictEqual(confirm.callCount, 1, 'confirmed immediately');
  });

  it('deduplicates incoming messages with same sender/timestamp, if existing message only in memory', async () => {
    const senderAci = generateAci();
    const conversation = await window.ConversationController.getOrCreateAndWait(
      senderAci,
      'private'
    );
    const sentAt = Date.now();
    const dataMessage: ProcessedDataMessage = {
      attachments: [],
      flags: 0,
      body: 'body',
      expireTimer: DurationInSeconds.fromDays(0),
      expireTimerVersion: 1,
      isViewOnce: false,
      timestamp: sentAt,
      requiredProtocolVersion:
        SignalService.DataMessage.ProtocolVersion.CURRENT,
    };
    const saveAndNotify = sinon.stub();
    const confirm = sinon.stub();

    const attributes: MessageAttributesType = {
      id: uuid(),
      conversationId: conversation.id,
      type: 'incoming',
      sourceServiceId: senderAci,
      sourceDevice: 1,
      sent_at: sentAt,
      timestamp: sentAt,
      received_at: sentAt,
    };

    await handleDataMessage(
      new MessageModel(attributes),
      dataMessage,
      confirm,
      {},
      { saveAndNotify }
    );

    assert.strictEqual(saveAndNotify.callCount, 1, 'initial message saved');
    assert.strictEqual(confirm.callCount, 0, 'not confirmed until saved');

    // Calling it again with same message does not call saveAndNotify, but does confirm()
    await handleDataMessage(
      new MessageModel(attributes),
      dataMessage,
      confirm,
      {},
      { saveAndNotify }
    );

    assert.strictEqual(saveAndNotify.callCount, 1, 'not saved again');
    assert.strictEqual(confirm.callCount, 1, 'duplicate confirmed immediately');

    // Calling it again with different message but same aci/timestamp does not call
    // saveAndNotify, but does confirm()
    await handleDataMessage(
      new MessageModel({
        ...attributes,
        // we intentionally (if suboptimally) do not consider deviceId when deduplicating
        sourceDevice: 2,
        id: uuid(),
      }),
      dataMessage,
      confirm,
      {},
      { saveAndNotify }
    );

    assert.strictEqual(saveAndNotify.callCount, 1, 'not saved again');
    assert.strictEqual(confirm.callCount, 2, 'duplicate confirmed immediately');
  });
});
