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

import { v4 as uuid } from 'uuid';

import { incrementMessageCounter } from '../util/incrementMessageCounter.preload.ts';
import { ReadStatus } from '../messages/MessageReadStatus.std.ts';
import { SendStatus } from '../messages/MessageSendState.std.ts';
import { DataWriter } from '../sql/Client.preload.ts';
import { BodyRange } from '../types/BodyRange.std.ts';
import { CURRENT_SCHEMA_VERSION } from '../types/Message2.preload.ts';
import { strictAssert } from '../util/assert.std.ts';
import { MINUTE } from '../util/durations/index.std.ts';
import { isOlderThan } from '../util/timestamp.std.ts';
import { sleep } from '../util/sleep.std.ts';
import { stats } from '../test-helpers/benchmarkStats.std.ts';
import type { StatsType } from '../test-helpers/benchmarkStats.std.ts';
import type { MessageAttributesType } from '../model-types.d.ts';
import { createLogger } from '../logging/log.std.ts';
import { postSaveUpdates } from '../util/cleanup.preload.ts';
import { itemStorage } from '../textsecure/Storage.preload.ts';
import { getSelectedConversationId } from '../state/selectors/nav.std.ts';

const log = createLogger('benchmarkConversationOpen');

const BUFFER_DELAY_MS = 50;

type PopulateConversationArgsType = {
  conversationId: string;
  messageCount: number;
  unreadCount?: number;
  customizeMessage?: (
    idx: number,
    baseMessage: MessageAttributesType
  ) => MessageAttributesType;
};

async function populateConversationWithMessages({
  conversationId,
  messageCount,
  unreadCount = 0,
  customizeMessage,
}: PopulateConversationArgsType): Promise<void> {
  strictAssert(
    window.SignalCI,
    'CI not enabled; ensure this is a staging build'
  );
  const logId = 'benchmarkConversationOpen/populateConversationWithMessages';
  log.info(`${logId}: populating conversation`);

  const ourAci = itemStorage.user.getCheckedAci();
  const conversation = window.ConversationController.get(conversationId);

  strictAssert(
    conversation,
    `Conversation with id [${conversationId}] not found`
  );

  log.info(`${logId}: destroying all messages in ${conversationId}`);
  await conversation.destroyMessages({ source: 'local-delete' });

  log.info(`${logId}: adding ${messageCount} messages to ${conversationId}`);
  let timestamp = Date.now();
  const messages: Array<MessageAttributesType> = [];
  for (let i = 0; i < messageCount; i += 1) {
    const isUnread = messageCount - i <= unreadCount;
    const isIncoming = isUnread || i % 2 === 0;
    const message: MessageAttributesType = {
      body: `Message ${i}: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam venenatis nec sapien id porttitor.`,
      bodyRanges: [{ start: 0, length: 7, style: BodyRange.Style.BOLD }],
      attachments: [],
      conversationId,
      id: uuid(),
      type: isIncoming ? 'incoming' : 'outgoing',
      timestamp,
      sent_at: timestamp,
      schemaVersion: CURRENT_SCHEMA_VERSION,
      received_at: incrementMessageCounter(),
      readStatus: isUnread ? ReadStatus.Unread : ReadStatus.Read,
      sourceServiceId: isIncoming
        ? conversation.getCheckedServiceId('CI')
        : ourAci,
      ...(isIncoming
        ? {}
        : {
            sendStateByConversationId: {
              [conversationId]: { status: SendStatus.Sent },
            },
          }),
    };
    messages.push(customizeMessage?.(i, message) ?? message);

    timestamp += 1;
  }

  await DataWriter.saveMessages(messages, {
    forceSave: true,
    ourAci,
    postSaveUpdates,
  });

  conversation.set({ active_at: Date.now() });
  await DataWriter.updateConversation(conversation.attributes);
  log.info(`${logId}: populating conversation complete`);
}

export async function benchmarkConversationOpen({
  conversationId,
  messageCount = 10_000,
  runCount = 50,
  runCountToSkip = 0,
  customizeMessage,
  unreadCount,
  testRunId,
}: Partial<PopulateConversationArgsType> & {
  runCount?: number;
  runCountToSkip?: number;
  testRunId?: string;
} = {}): Promise<{ durations: Array<number>; stats: StatsType }> {
  strictAssert(
    window.SignalCI,
    'CI not enabled; ensure this is a staging build'
  );

  // oxlint-disable-next-line no-param-reassign
  conversationId =
    conversationId || getSelectedConversationId(window.reduxStore.getState());

  strictAssert(conversationId, 'Must open a conversation for benchmarking');

  const logId = `benchmarkConversationOpen${testRunId ? `/${testRunId}` : ''}`;

  log.info(`${logId}: starting conversation open benchmarks, config:`, {
    conversationId,
    messageCount,
    runCount,
    customMessageMethod: !!customizeMessage,
    unreadCount,
    testRunId,
  });

  await populateConversationWithMessages({
    conversationId,
    messageCount,
    unreadCount,
    customizeMessage,
  });
  log.info(`${logId}: populating conversation complete`);

  const durations: Array<number> = [];
  for (let i = 0; i < runCount; i += 1) {
    // Give some buffer between tests
    // oxlint-disable-next-line no-await-in-loop
    await sleep(BUFFER_DELAY_MS);

    log.info(`${logId}: running open test run ${i + 1}/${runCount}`);

    // oxlint-disable-next-line no-await-in-loop
    const duration = await timeConversationOpen(conversationId);

    if (i >= runCountToSkip) {
      durations.push(duration);
    }
  }

  const result = {
    durations,
    stats: stats(durations),
  };

  log.info(`${logId}: tests complete, results:`, result);
  return result;
}

async function waitForSelector(
  selector: string,
  timeout = MINUTE
): Promise<Node> {
  const start = Date.now();

  while (!isOlderThan(start, timeout)) {
    const element = window.document.querySelector(selector);
    if (element) {
      return element;
    }

    // oxlint-disable-next-line no-await-in-loop
    await sleep(BUFFER_DELAY_MS);
  }

  throw new Error('Timed out');
}

async function timeConversationOpen(id: string): Promise<number> {
  strictAssert(
    window.SignalCI,
    'CI not enabled; ensure this is a staging build'
  );

  await showEmptyInbox();

  const element = await waitForSelector(`[data-id="${id}"]`);

  const conversationOpenPromise = window.SignalCI.waitForEvent(
    'conversation:open',
    { ignorePastEvents: true }
  );

  const start = Date.now();

  element.dispatchEvent(new Event('click', { bubbles: true }));
  window.reduxActions.conversations.showConversation({
    conversationId: id,
  });

  await conversationOpenPromise;
  const end = Date.now();

  return end - start;
}

async function showEmptyInbox() {
  strictAssert(
    window.SignalCI,
    'CI not enabled; ensure this is a staging build'
  );
  if (!getSelectedConversationId(window.reduxStore.getState())) {
    return;
  }
  const promise = window.SignalCI.waitForEvent('empty-inbox:rendered', {
    ignorePastEvents: true,
  });
  window.reduxActions.conversations.showConversation({
    conversationId: undefined,
  });
  return promise;
}
