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

import { assert } from 'chai';
import { range } from 'lodash';
import { Proto } from '@signalapp/mock-server';
import type { StorageStateRecord } from '@signalapp/mock-server';
import fs from 'fs/promises';
import path from 'path';

import * as durations from '../../util/durations';
import type { App, Bootstrap } from './fixtures';
import { initStorage, debug } from './fixtures';

const { StickerPackOperation } = Proto.SyncMessage;

const FIXTURES = path.join(__dirname, '..', '..', '..', 'fixtures');
const IdentifierType = Proto.ManifestRecord.Identifier.Type;

const EMPTY = new Uint8Array(0);

export type StickerPackType = Readonly<{
  id: Buffer;
  key: Buffer;
  stickerCount: number;
}>;

const STICKER_PACKS: ReadonlyArray<StickerPackType> = [
  {
    id: Buffer.from('c40ed069cdc2b91eccfccf25e6bcddfc', 'hex'),
    key: Buffer.from(
      'cefadd6e81c128680aead1711eb5c92c10f63bdfbc78528a4519ba682de396e4',
      'hex'
    ),
    stickerCount: 1,
  },
  {
    id: Buffer.from('ae8fedafda4768fd3384d4b3b9db963d', 'hex'),
    key: Buffer.from(
      '53f4aa8b95e1c2e75afab2328fe67eb6d7affbcd4f50cd4da89dfc325dbc73ca',
      'hex'
    ),
    stickerCount: 1,
  },
];

function getStickerPackLink(pack: StickerPackType): string {
  return (
    `https://signal.art/addstickers/#pack_id=${pack.id.toString('hex')}&` +
    `pack_key=${pack.key.toString('hex')}`
  );
}

function getStickerPackRecordPredicate(
  pack: StickerPackType
): (record: StorageStateRecord) => boolean {
  return ({ type, record }: StorageStateRecord): boolean => {
    if (type !== IdentifierType.STICKER_PACK) {
      return false;
    }
    return pack.id.equals(record.stickerPack?.packId ?? EMPTY);
  };
}

describe('storage service', function (this: Mocha.Suite) {
  this.timeout(durations.MINUTE);
  this.retries(4);

  let bootstrap: Bootstrap;
  let app: App;

  beforeEach(async () => {
    ({ bootstrap, app } = await initStorage());

    const { server } = bootstrap;

    await Promise.all(
      STICKER_PACKS.map(async ({ id, stickerCount }) => {
        const hexId = id.toString('hex');

        await server.storeStickerPack({
          id,
          manifest: await fs.readFile(
            path.join(FIXTURES, `stickerpack-${hexId}.bin`)
          ),
          stickers: await Promise.all(
            range(0, stickerCount).map(async index =>
              fs.readFile(
                path.join(FIXTURES, `stickerpack-${hexId}-${index}.bin`)
              )
            )
          ),
        });
      })
    );
  });

  afterEach(async function (this: Mocha.Context) {
    if (!bootstrap) {
      return;
    }

    await bootstrap.maybeSaveLogs(this.currentTest, app);
    await app.close();
    await bootstrap.teardown();
  });

  it('should install/uninstall stickers', async () => {
    const { phone, desktop, contacts } = bootstrap;
    const [firstContact] = contacts;

    const window = await app.getWindow();

    const leftPane = window.locator('#LeftPane');
    const conversationView = window.locator(
      '.Inbox__conversation > .ConversationView'
    );

    debug('sending two sticker pack links');
    await firstContact.sendText(
      desktop,
      `First sticker pack ${getStickerPackLink(STICKER_PACKS[0])}`
    );
    await firstContact.sendText(
      desktop,
      `Second sticker pack ${getStickerPackLink(STICKER_PACKS[1])}`
    );

    await leftPane
      .locator(`[data-testid="${firstContact.toContact().aci}"]`)
      .click();

    {
      debug('installing first sticker pack via UI');
      const state = await phone.expectStorageState('initial state');

      await conversationView
        .locator(`a:has-text("${STICKER_PACKS[0].id.toString('hex')}")`)
        .click({ noWaitAfter: true });
      await window
        .locator(
          '.module-sticker-manager__preview-modal__footer--install button >> "Install"'
        )
        .click();

      debug('waiting for sync message');
      const { syncMessage } = await phone.waitForSyncMessage(entry =>
        Boolean(entry.syncMessage.stickerPackOperation?.length)
      );
      const [syncOp] = syncMessage.stickerPackOperation ?? [];
      assert.isTrue(STICKER_PACKS[0].id.equals(syncOp?.packId ?? EMPTY));
      assert.isTrue(STICKER_PACKS[0].key.equals(syncOp?.packKey ?? EMPTY));
      assert.strictEqual(syncOp?.type, StickerPackOperation.Type.INSTALL);

      debug('waiting for storage service update');
      const stateAfter = await phone.waitForStorageState({ after: state });
      const stickerPack = stateAfter.findRecord(
        getStickerPackRecordPredicate(STICKER_PACKS[0])
      );
      assert.ok(
        stickerPack,
        'New storage state should have sticker pack record'
      );
      assert.isTrue(
        STICKER_PACKS[0].key.equals(
          stickerPack?.record.stickerPack?.packKey ?? EMPTY
        ),
        'Wrong sticker pack key'
      );
      assert.strictEqual(
        stickerPack?.record.stickerPack?.position,
        6,
        'Wrong sticker pack position'
      );
    }

    {
      debug('uninstalling first sticker pack via UI');
      const state = await phone.expectStorageState('initial state');

      await conversationView
        .locator(`a:has-text("${STICKER_PACKS[0].id.toString('hex')}")`)
        .click({ noWaitAfter: true });
      await window
        .locator(
          '.module-sticker-manager__preview-modal__footer--install button ' +
            '>> "Uninstall"'
        )
        .click();

      // Confirm
      await window
        .locator('.module-Button--destructive >> "Uninstall"')
        .click();

      debug('waiting for sync message');
      const { syncMessage } = await phone.waitForSyncMessage(entry =>
        Boolean(entry.syncMessage.stickerPackOperation?.length)
      );
      const [syncOp] = syncMessage.stickerPackOperation ?? [];
      assert.isTrue(STICKER_PACKS[0].id.equals(syncOp?.packId ?? EMPTY));
      assert.strictEqual(syncOp?.type, StickerPackOperation.Type.REMOVE);

      debug('waiting for storage service update');
      const stateAfter = await phone.waitForStorageState({ after: state });
      const stickerPack = stateAfter.findRecord(
        getStickerPackRecordPredicate(STICKER_PACKS[0])
      );
      assert.ok(
        stickerPack,
        'New storage state should have sticker pack record'
      );
      assert.deepStrictEqual(
        stickerPack?.record.stickerPack?.packKey,
        EMPTY,
        'Sticker pack key should be removed'
      );
      const deletedAt =
        stickerPack?.record.stickerPack?.deletedAtTimestamp?.toNumber() ?? 0;
      assert.isAbove(
        deletedAt,
        Date.now() - durations.HOUR,
        'Sticker pack should have deleted at timestamp'
      );
    }

    debug('opening sticker manager');
    await conversationView
      .locator('.CompositionArea .module-sticker-button__button')
      .click();

    const stickerManager = conversationView.locator(
      '[data-testid=StickerManager]'
    );

    debug('switching to Installed tab');
    await stickerManager.locator('.Tabs__tab >> "Installed"').click();

    {
      debug('installing first sticker pack via storage service');
      const state = await phone.expectStorageState('initial state');

      await phone.setStorageState(
        state.updateRecord(
          getStickerPackRecordPredicate(STICKER_PACKS[0]),
          record => ({
            ...record,
            stickerPack: {
              ...record?.stickerPack,
              packKey: STICKER_PACKS[0].key,
              position: 7,
              deletedAtTimestamp: undefined,
            },
          })
        )
      );
      await phone.sendFetchStorage({
        timestamp: bootstrap.getTimestamp(),
      });

      debug('waiting for sticker pack to become visible');
      await stickerManager
        .locator(`[data-testid="${STICKER_PACKS[0].id.toString('hex')}"]`)
        .waitFor();
    }

    {
      debug('installing second sticker pack via sync message');
      const state = await phone.expectStorageState('initial state');

      await phone.sendStickerPackSync({
        type: 'install',
        packId: STICKER_PACKS[1].id,
        packKey: STICKER_PACKS[1].key,
        timestamp: bootstrap.getTimestamp(),
      });

      debug('waiting for sticker pack to become visible');
      await stickerManager
        .locator(`[data-testid="${STICKER_PACKS[1].id.toString('hex')}"]`)
        .waitFor();

      debug('waiting for storage service update');
      const stateAfter = await phone.waitForStorageState({ after: state });
      const stickerPack = stateAfter.findRecord(
        getStickerPackRecordPredicate(STICKER_PACKS[1])
      );
      assert.ok(
        stickerPack,
        'New storage state should have sticker pack record'
      );
      assert.isTrue(
        STICKER_PACKS[1].key.equals(
          stickerPack?.record.stickerPack?.packKey ?? EMPTY
        ),
        'Wrong sticker pack key'
      );
      assert.strictEqual(
        stickerPack?.record.stickerPack?.position,
        7,
        'Wrong sticker pack position'
      );
    }

    debug('Verifying the final manifest version');
    const finalState = await phone.expectStorageState('consistency check');

    assert.strictEqual(finalState.version, 5);
  });
});
