/* Any copyright is dedicated to the Public Domain.
   http://creativecommons.org/publicdomain/zero/1.0/ */

"use strict";

/**
 * An actor unit test for testing RemoteSettings update behavior. This uses the
 * recommendations from:
 *
 * https://firefox-source-docs.mozilla.org/services/settings/index.html#unit-tests
 */
add_task(async function test_translations_actor_sync_update_models() {
  const { remoteClients, cleanup } = await setupActorTest({
    autoDownloadFromRemoteSettings: true,
    languagePairs: [
      { fromLang: "en", toLang: "es" },
      { fromLang: "es", toLang: "en" },
    ],
  });

  const decoder = new TextDecoder();
  const modelsPromise = TranslationsParent.getTranslationModelPayload(
    "en",
    "es"
  );

  const { languageModelFiles: oldModels } = await modelsPromise;

  is(
    decoder.decode(await oldModels.model.blob.arrayBuffer()),
    `Mocked download: test-translation-models model.enes.intgemm.alphas.bin ${TranslationsParent.LANGUAGE_MODEL_MAJOR_VERSION_MAX}.0`,
    `The version ${TranslationsParent.LANGUAGE_MODEL_MAJOR_VERSION_MAX}.0 model is downloaded.`
  );

  const useLexicalShortlist = Services.prefs.getBoolPref(
    "browser.translations.useLexicalShortlist"
  );
  const recordsToCreate = createRecordsForLanguagePair({
    sourceLanguage: "en",
    targetLanguage: "es",
  }).filter(({ fileType }) => useLexicalShortlist || fileType !== "lex");
  for (const newModelRecord of recordsToCreate) {
    newModelRecord.id = oldModels[newModelRecord.fileType].record.id;
    newModelRecord.version = `${TranslationsParent.LANGUAGE_MODEL_MAJOR_VERSION_MAX}.1`;
  }

  await modifyRemoteSettingsRecords(remoteClients.translationModels.client, {
    recordsToCreate,
    expectedUpdatedRecordsCount: downloadedFilesPerLanguagePair(),
  });

  const updatedModelsPromise = TranslationsParent.getTranslationModelPayload(
    "en",
    "es"
  );

  const { languageModelFiles } = await updatedModelsPromise;
  const { model: updatedModel } = languageModelFiles;

  is(
    decoder.decode(await updatedModel.blob.arrayBuffer()),
    `Mocked download: test-translation-models model.enes.intgemm.alphas.bin ${TranslationsParent.LANGUAGE_MODEL_MAJOR_VERSION_MAX}.1`,
    `The version ${TranslationsParent.LANGUAGE_MODEL_MAJOR_VERSION_MAX}.1 model is downloaded.`
  );

  return cleanup();
});

/**
 * An actor unit test for testing RemoteSettings delete behavior.
 */
add_task(async function test_translations_actor_sync_delete_models() {
  const { remoteClients, cleanup } = await setupActorTest({
    autoDownloadFromRemoteSettings: true,
    languagePairs: [
      { fromLang: "en", toLang: "es" },
      { fromLang: "es", toLang: "en" },
    ],
  });

  const decoder = new TextDecoder();
  const modelsPromise = TranslationsParent.getTranslationModelPayload(
    "en",
    "es"
  );

  const { languageModelFiles } = await modelsPromise;
  const { model } = languageModelFiles;

  is(
    decoder.decode(await model.blob.arrayBuffer()),
    `Mocked download: test-translation-models model.enes.intgemm.alphas.bin ${TranslationsParent.LANGUAGE_MODEL_MAJOR_VERSION_MAX}.0`,
    `The version ${TranslationsParent.LANGUAGE_MODEL_MAJOR_VERSION_MAX}.0 model is downloaded.`
  );

  info(
    `Removing record ${model.record.name} from mocked Remote Settings database.`
  );
  await modifyRemoteSettingsRecords(remoteClients.translationModels.client, {
    recordsToDelete: [model.record],
    expectedDeletedRecordsCount: 1,
  });

  let errorMessage;
  await TranslationsParent.getTranslationModelPayload("en", "es").catch(
    error => {
      errorMessage = error?.message;
    }
  );

  is(
    errorMessage,
    'No model file found for "en,es".',
    "The model was successfully removed."
  );

  return cleanup();
});

/**
 * An actor unit test for testing RemoteSettings creation behavior.
 */
add_task(async function test_translations_actor_sync_create_models() {
  const { remoteClients, cleanup } = await setupActorTest({
    autoDownloadFromRemoteSettings: true,
    languagePairs: [
      { fromLang: "en", toLang: "es" },
      { fromLang: "es", toLang: "en" },
    ],
  });

  const decoder = new TextDecoder();
  const modelsPromise = TranslationsParent.getTranslationModelPayload(
    "en",
    "es"
  );

  const { languageModelFiles: originalFiles } = await modelsPromise;
  is(
    decoder.decode(await originalFiles.model.blob.arrayBuffer()),
    `Mocked download: test-translation-models model.enes.intgemm.alphas.bin ${TranslationsParent.LANGUAGE_MODEL_MAJOR_VERSION_MAX}.0`,
    `The version ${TranslationsParent.LANGUAGE_MODEL_MAJOR_VERSION_MAX}.0 model is downloaded.`
  );

  const recordsToCreate = createRecordsForLanguagePair({
    sourceLanguage: "en",
    targetLanguage: "fr",
  });

  await modifyRemoteSettingsRecords(remoteClients.translationModels.client, {
    recordsToCreate,
    expectedCreatedRecordsCount: RECORDS_PER_LANGUAGE_PAIR_SHARED_VOCAB,
  });

  const updatedModelsPromise = TranslationsParent.getTranslationModelPayload(
    "en",
    "fr"
  );

  const { languageModelFiles: updatedFiles } = await updatedModelsPromise;
  const { vocab, lex, model } = updatedFiles;

  is(
    decoder.decode(await vocab.blob.arrayBuffer()),
    `Mocked download: test-translation-models vocab.enfr.spm ${TranslationsParent.LANGUAGE_MODEL_MAJOR_VERSION_MAX}.0`,
    "The en to fr vocab is downloaded."
  );
  is(
    decoder.decode(await model.blob.arrayBuffer()),
    `Mocked download: test-translation-models model.enfr.intgemm.alphas.bin ${TranslationsParent.LANGUAGE_MODEL_MAJOR_VERSION_MAX}.0`,
    "The en to fr model is downloaded."
  );

  if (Services.prefs.getBoolPref(USE_LEXICAL_SHORTLIST_PREF)) {
    is(
      decoder.decode(await lex.blob.arrayBuffer()),
      `Mocked download: test-translation-models lex.50.50.enfr.s2t.bin ${TranslationsParent.LANGUAGE_MODEL_MAJOR_VERSION_MAX}.0`,
      "The en to fr lex is downloaded."
    );
  }

  return cleanup();
});

/**
 * An actor unit test for testing creating a new record has a higher minor version than an existing record of the same kind.
 */
add_task(
  async function test_translations_actor_sync_create_models_higher_minor_version() {
    const { remoteClients, cleanup } = await setupActorTest({
      autoDownloadFromRemoteSettings: true,
      languagePairs: [
        { fromLang: "en", toLang: "es" },
        { fromLang: "es", toLang: "en" },
      ],
    });

    const decoder = new TextDecoder();
    const modelsPromise = TranslationsParent.getTranslationModelPayload(
      "en",
      "es"
    );

    const { languageModelFiles: originalFiles } = await modelsPromise;
    is(
      decoder.decode(await originalFiles.model.blob.arrayBuffer()),
      `Mocked download: test-translation-models model.enes.intgemm.alphas.bin ${TranslationsParent.LANGUAGE_MODEL_MAJOR_VERSION_MAX}.0`,
      `The version ${TranslationsParent.LANGUAGE_MODEL_MAJOR_VERSION_MAX}.0 model is downloaded.`
    );

    const recordsToCreate = createRecordsForLanguagePair({
      sourceLanguage: "en",
      targetLanguage: "es",
    });
    for (const newModelRecord of recordsToCreate) {
      newModelRecord.version = `${TranslationsParent.LANGUAGE_MODEL_MAJOR_VERSION_MAX}.1`;
    }

    await modifyRemoteSettingsRecords(remoteClients.translationModels.client, {
      recordsToCreate,
      expectedCreatedRecordsCount: RECORDS_PER_LANGUAGE_PAIR_SHARED_VOCAB,
    });

    const updatedModelsPromise = TranslationsParent.getTranslationModelPayload(
      "en",
      "es"
    );

    const { languageModelFiles: updatedFiles } = await updatedModelsPromise;
    const { vocab, lex, model } = await updatedFiles;

    is(
      decoder.decode(await vocab.blob.arrayBuffer()),
      `Mocked download: test-translation-models vocab.enes.spm ${TranslationsParent.LANGUAGE_MODEL_MAJOR_VERSION_MAX}.1`,
      `The ${TranslationsParent.LANGUAGE_MODEL_MAJOR_VERSION_MAX}.1 vocab is downloaded.`
    );
    is(
      decoder.decode(await model.blob.arrayBuffer()),
      `Mocked download: test-translation-models model.enes.intgemm.alphas.bin ${TranslationsParent.LANGUAGE_MODEL_MAJOR_VERSION_MAX}.1`,
      `The ${TranslationsParent.LANGUAGE_MODEL_MAJOR_VERSION_MAX}.1 model is downloaded.`
    );

    if (Services.prefs.getBoolPref(USE_LEXICAL_SHORTLIST_PREF)) {
      is(
        decoder.decode(await lex.blob.arrayBuffer()),
        `Mocked download: test-translation-models lex.50.50.enes.s2t.bin ${TranslationsParent.LANGUAGE_MODEL_MAJOR_VERSION_MAX}.1`,
        "The en to es lex is downloaded."
      );
    }

    return cleanup();
  }
);

/**
 * An actor unit test for testing creating a new record has a higher major version than an existing record of the same kind.
 */
add_task(
  async function test_translations_actor_sync_create_models_higher_major_version() {
    const { remoteClients, cleanup } = await setupActorTest({
      autoDownloadFromRemoteSettings: true,
      languagePairs: [
        { fromLang: "en", toLang: "es" },
        { fromLang: "es", toLang: "en" },
      ],
    });

    const decoder = new TextDecoder();
    const modelsPromise = TranslationsParent.getTranslationModelPayload(
      "en",
      "es"
    );

    const { languageModelFiles: originalFiles } = await modelsPromise;
    is(
      decoder.decode(await originalFiles.model.blob.arrayBuffer()),
      `Mocked download: test-translation-models model.enes.intgemm.alphas.bin ${TranslationsParent.LANGUAGE_MODEL_MAJOR_VERSION_MAX}.0`,
      `The version ${TranslationsParent.LANGUAGE_MODEL_MAJOR_VERSION_MAX}.0 model is downloaded.`
    );

    const recordsToCreate = createRecordsForLanguagePair({
      sourceLanguage: "en",
      targetLanguage: "es",
    });
    for (const newModelRecord of recordsToCreate) {
      newModelRecord.version = "2.0";
    }

    await modifyRemoteSettingsRecords(remoteClients.translationModels.client, {
      recordsToCreate,
      expectedCreatedRecordsCount: RECORDS_PER_LANGUAGE_PAIR_SHARED_VOCAB,
    });

    const updatedModelsPromise = TranslationsParent.getTranslationModelPayload(
      "en",
      "es"
    );

    const { languageModelFiles: updatedFiles } = await updatedModelsPromise;
    const { vocab, lex, model } = updatedFiles;

    is(
      decoder.decode(await vocab.blob.arrayBuffer()),
      `Mocked download: test-translation-models vocab.enes.spm ${TranslationsParent.LANGUAGE_MODEL_MAJOR_VERSION_MAX}.0`,
      `The ${TranslationsParent.LANGUAGE_MODEL_MAJOR_VERSION_MAX}.0 vocab is downloaded.`
    );
    is(
      decoder.decode(await model.blob.arrayBuffer()),
      `Mocked download: test-translation-models model.enes.intgemm.alphas.bin ${TranslationsParent.LANGUAGE_MODEL_MAJOR_VERSION_MAX}.0`,
      `The ${TranslationsParent.LANGUAGE_MODEL_MAJOR_VERSION_MAX}.0 model is downloaded.`
    );

    if (Services.prefs.getBoolPref(USE_LEXICAL_SHORTLIST_PREF)) {
      is(
        decoder.decode(await lex.blob.arrayBuffer()),
        `Mocked download: test-translation-models lex.50.50.enes.s2t.bin ${TranslationsParent.LANGUAGE_MODEL_MAJOR_VERSION_MAX}.0`,
        "The en to es lex is downloaded."
      );
    }

    return cleanup();
  }
);

/**
 * An actor unit test for testing removing a record that has a higher minor version than another record, ensuring
 * that the models roll back to the previous version.
 */
add_task(async function test_translations_actor_sync_rollback_models() {
  const { remoteClients, cleanup } = await setupActorTest({
    autoDownloadFromRemoteSettings: true,
    languagePairs: [
      { fromLang: "en", toLang: "es" },
      { fromLang: "es", toLang: "en" },
    ],
  });

  const newRecords = createRecordsForLanguagePair({
    sourceLanguage: "en",
    targetLanguage: "es",
  });
  for (const newModelRecord of newRecords) {
    newModelRecord.version = `${TranslationsParent.LANGUAGE_MODEL_MAJOR_VERSION_MAX}.1`;
  }

  await modifyRemoteSettingsRecords(remoteClients.translationModels.client, {
    recordsToCreate: newRecords,
    expectedCreatedRecordsCount: RECORDS_PER_LANGUAGE_PAIR_SHARED_VOCAB,
  });

  const decoder = new TextDecoder();
  const modelsPromise = TranslationsParent.getTranslationModelPayload(
    "en",
    "es"
  );

  const { languageModelFiles: originalFiles } = await modelsPromise;
  is(
    decoder.decode(await originalFiles.model.blob.arrayBuffer()),
    `Mocked download: test-translation-models model.enes.intgemm.alphas.bin ${TranslationsParent.LANGUAGE_MODEL_MAJOR_VERSION_MAX}.1`,
    `The version ${TranslationsParent.LANGUAGE_MODEL_MAJOR_VERSION_MAX}.1 model is downloaded.`
  );

  await modifyRemoteSettingsRecords(remoteClients.translationModels.client, {
    recordsToDelete: newRecords,
    expectedDeletedRecordsCount: RECORDS_PER_LANGUAGE_PAIR_SHARED_VOCAB,
  });

  const rolledBackModelsPromise = TranslationsParent.getTranslationModelPayload(
    "en",
    "es"
  );

  const { languageModelFiles: rolledBackFiles } = await rolledBackModelsPromise;
  const { vocab, lex, model } = rolledBackFiles;

  is(
    decoder.decode(await vocab.blob.arrayBuffer()),
    `Mocked download: test-translation-models vocab.enes.spm ${TranslationsParent.LANGUAGE_MODEL_MAJOR_VERSION_MAX}.0`,
    `The ${TranslationsParent.LANGUAGE_MODEL_MAJOR_VERSION_MAX}.0 vocab is downloaded.`
  );
  is(
    decoder.decode(await model.blob.arrayBuffer()),
    `Mocked download: test-translation-models model.enes.intgemm.alphas.bin ${TranslationsParent.LANGUAGE_MODEL_MAJOR_VERSION_MAX}.0`,
    `The ${TranslationsParent.LANGUAGE_MODEL_MAJOR_VERSION_MAX}.0 model is downloaded.`
  );

  if (Services.prefs.getBoolPref(USE_LEXICAL_SHORTLIST_PREF)) {
    is(
      decoder.decode(await lex.blob.arrayBuffer()),
      `Mocked download: test-translation-models lex.50.50.enes.s2t.bin ${TranslationsParent.LANGUAGE_MODEL_MAJOR_VERSION_MAX}.0`,
      "The en to es lex is downloaded."
    );
  }

  return cleanup();
});

add_task(async function test_translations_parent_download_size() {
  const { cleanup } = await setupActorTest({
    languagePairs: [
      { fromLang: "en", toLang: "es" },
      { fromLang: "es", toLang: "en" },
      { fromLang: "en", toLang: "de" },
      { fromLang: "de", toLang: "en" },
    ],
  });

  const directSize =
    await TranslationsParent.getExpectedTranslationDownloadSize("en", "es");
  is(
    directSize,
    downloadedFilesPerLanguagePair() * 123,
    "Returned the expected download size for a direct translation."
  );

  const pivotSize = await TranslationsParent.getExpectedTranslationDownloadSize(
    "es",
    "de"
  );
  // Includes a pivot (x2), model, lex, and vocab files (x3), each mocked at 123 bytes.
  is(
    pivotSize,
    2 * downloadedFilesPerLanguagePair() * 123,
    "Returned the expected download size for a pivot."
  );

  const notApplicableSize =
    await TranslationsParent.getExpectedTranslationDownloadSize(
      "unknown",
      "unknown"
    );
  is(
    notApplicableSize,
    0,
    "Returned the expected download size for an unknown or not applicable model."
  );
  return cleanup();
});

/**
 * An actor unit test for testing the scenarios where we update a model via minor version bump to
 * either transition from a shared vocab to a split vocab configuration, or the converse transition
 * from a split vocab to a shared vocab configuration.
 */
add_task(async function test_transition_from_vocab_configurations() {
  const { remoteClients, cleanup } = await setupActorTest({
    autoDownloadFromRemoteSettings: true,
    languagePairs: [
      { fromLang: "en", toLang: "es" },
      { fromLang: "es", toLang: "en" },
    ],
  });

  const decoder = new TextDecoder();
  const modelsPromise = TranslationsParent.getTranslationModelPayload(
    "en",
    "es"
  );

  const { languageModelFiles: originalFiles } = await modelsPromise;
  is(
    decoder.decode(await originalFiles.model.blob.arrayBuffer()),
    `Mocked download: test-translation-models model.enes.intgemm.alphas.bin ${TranslationsParent.LANGUAGE_MODEL_MAJOR_VERSION_MAX}.0`,
    `The version ${TranslationsParent.LANGUAGE_MODEL_MAJOR_VERSION_MAX}.0 model is downloaded.`
  );

  {
    info(
      `Publishing a new split-vocab configuration as version ${TranslationsParent.LANGUAGE_MODEL_MAJOR_VERSION_MAX}.1`
    );
    const recordsToCreate = createRecordsForLanguagePair({
      sourceLanguage: "en",
      targetLanguage: "es",
      splitVocab: true,
    });
    for (const newModelRecord of recordsToCreate) {
      newModelRecord.version = `${TranslationsParent.LANGUAGE_MODEL_MAJOR_VERSION_MAX}.1`;
    }

    await modifyRemoteSettingsRecords(remoteClients.translationModels.client, {
      recordsToCreate,
      expectedCreatedRecordsCount: RECORDS_PER_LANGUAGE_PAIR_SPLIT_VOCAB,
    });

    const updatedModelsPromise = TranslationsParent.getTranslationModelPayload(
      "en",
      "es"
    );

    const { languageModelFiles: updatedFiles } = await updatedModelsPromise;
    const { srcvocab, trgvocab, vocab, lex, model } = await updatedFiles;

    is(
      vocab,
      undefined,
      `The shared vocab is undefined after upgrading to split vocab.`
    );
    is(
      decoder.decode(await srcvocab.blob.arrayBuffer()),
      `Mocked download: test-translation-models srcvocab.enes.spm ${TranslationsParent.LANGUAGE_MODEL_MAJOR_VERSION_MAX}.1`,
      `The ${TranslationsParent.LANGUAGE_MODEL_MAJOR_VERSION_MAX}.1 srcvocab is downloaded.`
    );
    is(
      decoder.decode(await trgvocab.blob.arrayBuffer()),
      `Mocked download: test-translation-models trgvocab.enes.spm ${TranslationsParent.LANGUAGE_MODEL_MAJOR_VERSION_MAX}.1`,
      `The ${TranslationsParent.LANGUAGE_MODEL_MAJOR_VERSION_MAX}.1 trgvocab is downloaded.`
    );
    is(
      decoder.decode(await model.blob.arrayBuffer()),
      `Mocked download: test-translation-models model.enes.intgemm.alphas.bin ${TranslationsParent.LANGUAGE_MODEL_MAJOR_VERSION_MAX}.1`,
      `The ${TranslationsParent.LANGUAGE_MODEL_MAJOR_VERSION_MAX}.1 model is downloaded.`
    );

    if (Services.prefs.getBoolPref(USE_LEXICAL_SHORTLIST_PREF)) {
      is(
        decoder.decode(await lex.blob.arrayBuffer()),
        `Mocked download: test-translation-models lex.50.50.enes.s2t.bin ${TranslationsParent.LANGUAGE_MODEL_MAJOR_VERSION_MAX}.1`,
        `The ${TranslationsParent.LANGUAGE_MODEL_MAJOR_VERSION_MAX}.1 lex is downloaded.`
      );
    }
  }

  {
    info(
      `Publishing a new shared-vocab configuration as version ${TranslationsParent.LANGUAGE_MODEL_MAJOR_VERSION_MAX}.2`
    );
    const recordsToCreate = createRecordsForLanguagePair({
      sourceLanguage: "en",
      targetLanguage: "es",
      splitVocab: false,
    });
    for (const newModelRecord of recordsToCreate) {
      newModelRecord.version = `${TranslationsParent.LANGUAGE_MODEL_MAJOR_VERSION_MAX}.2`;
    }

    await modifyRemoteSettingsRecords(remoteClients.translationModels.client, {
      recordsToCreate,
      expectedCreatedRecordsCount: RECORDS_PER_LANGUAGE_PAIR_SHARED_VOCAB,
    });

    const updatedModelsPromise = TranslationsParent.getTranslationModelPayload(
      "en",
      "es"
    );

    const { languageModelFiles: updatedFiles } = await updatedModelsPromise;
    const { srcvocab, trgvocab, vocab, lex, model } = await updatedFiles;

    is(
      srcvocab,
      undefined,
      `The source vocab is undefined after transitioning to a higher-version shared vocab.`
    );
    is(
      trgvocab,
      undefined,
      `The target vocab is undefined after transitioning to a higher-version shared vocab.`
    );

    is(
      decoder.decode(await vocab.blob.arrayBuffer()),
      `Mocked download: test-translation-models vocab.enes.spm ${TranslationsParent.LANGUAGE_MODEL_MAJOR_VERSION_MAX}.2`,
      `The ${TranslationsParent.LANGUAGE_MODEL_MAJOR_VERSION_MAX}.2 shared vocab is downloaded.`
    );
    is(
      decoder.decode(await model.blob.arrayBuffer()),
      `Mocked download: test-translation-models model.enes.intgemm.alphas.bin ${TranslationsParent.LANGUAGE_MODEL_MAJOR_VERSION_MAX}.2`,
      `The ${TranslationsParent.LANGUAGE_MODEL_MAJOR_VERSION_MAX}.2 model is downloaded.`
    );
    if (Services.prefs.getBoolPref(USE_LEXICAL_SHORTLIST_PREF)) {
      is(
        decoder.decode(await lex.blob.arrayBuffer()),
        `Mocked download: test-translation-models lex.50.50.enes.s2t.bin ${TranslationsParent.LANGUAGE_MODEL_MAJOR_VERSION_MAX}.2`,
        `The ${TranslationsParent.LANGUAGE_MODEL_MAJOR_VERSION_MAX}.2 lex is downloaded.`
      );
    }
  }

  return cleanup();
});
