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

"use strict";

add_setup(async function setup() {
  await SpecialPowers.pushPrefEnv({
    set: [["test.wait300msAfterTabSwitch", true]],
  });

  const cleanup = await setupTest();

  SpecialPowers.addTaskImport(
    "ExperimentAPI",
    "resource://nimbus/ExperimentAPI.sys.mjs"
  );
  SpecialPowers.addTaskImport(
    "NimbusFeatures",
    "resource://nimbus/ExperimentAPI.sys.mjs"
  );
  SpecialPowers.addTaskImport(
    "TestUtils",
    "resource://testing-common/TestUtils.sys.mjs"
  );

  registerCleanupFunction(cleanup);
});

add_task(async function testGetFromChildNewEnrollment() {
  const browserWindow = Services.wm.getMostRecentWindow("navigator:browser");

  // Open a tab so we have a content process.
  const tab = await BrowserTestUtils.openNewForegroundTab({
    gBrowser: browserWindow.gBrowser,
    url: "https://example.com",
    forceNewProcess: true,
  });
  const browser = tab.linkedBrowser;

  // Assert that the tab is in fact a content process and that we don't have any
  // experiments available yet.
  await SpecialPowers.spawn(browser, [], async () => {
    Assert.equal(
      Services.appinfo.processType,
      Services.appinfo.PROCESS_TYPE_CONTENT,
      "This is running in a content process"
    );

    await ExperimentAPI.ready();

    Assert.equal(
      NimbusFeatures.testFeature.getEnrollmentMetadata(),
      null,
      "Experiment should not exist in child yet"
    );
  });

  let childUpdated = await childSharedDataChanged(browser);

  // Enroll in an experiment in the parent process.
  await ExperimentAPI.manager.enroll(
    NimbusTestUtils.factories.recipe.withFeatureConfig("foo", {
      featureId: "testFeature",
      value: {
        enabled: true,
        testInt: 123,
      },
    }),
    "test"
  );

  // Immediately serialize sharedData and broadcast changes to the child processes.
  //
  // In normal operation, this will happen during idle dispatch [1], but we want
  // to test that our IPC mechanisms work correctly, so we force it to happen so
  // that we can act immediately in the child.
  //
  // [1]: https://searchfox.org/mozilla-central/rev/5bea6ede57be43d450ecc24af7a535288c9a9f7d/dom/ipc/SharedMap.cpp#416-422
  Services.ppmm.sharedData.flush();
  await childUpdated.promise;

  // Check that the new state is reflected in the content process.
  await SpecialPowers.spawn(browser, [], async () => {
    await TestUtils.waitForCondition(
      () => NimbusFeatures.testFeature.getEnrollmentMetadata(),
      "Wait for enrollment child to sync"
    );

    const meta = NimbusFeatures.testFeature.getEnrollmentMetadata();

    Assert.equal(meta.slug, "foo", "Experiment slug is correct");
    Assert.equal(meta.branch, "control", "Experiment branch slug is correct");

    Assert.deepEqual(
      NimbusFeatures.testFeature.getAllVariables(),
      { enabled: true, testInt: 123 },
      "Experiment values are correct"
    );

    Assert.equal(
      NimbusFeatures.testFeature.getVariable("enabled"),
      true,
      "Experiment values are correct"
    );

    Assert.equal(
      NimbusFeatures.testFeature.getVariable("testInt"),
      123,
      "Experiment values are correct"
    );
  });

  childUpdated = await childSharedDataChanged(browser);
  // Unenroll from the experiment in the parent process.
  ExperimentAPI.manager.unenroll("foo");
  // Propagate the change to child processes.
  Services.ppmm.sharedData.flush();
  await childUpdated.promise;

  // Check that the new state is reflected in the content process.
  await SpecialPowers.spawn(browser, [], async () => {
    await TestUtils.waitForCondition(
      () => NimbusFeatures.testFeature.getEnrollmentMetadata() === null,
      "Wait for unenrollment to sync"
    );
  });

  ExperimentAPI.manager.store._deleteForTests("foo");
  await NimbusTestUtils.flushStore();

  BrowserTestUtils.removeTab(tab);

  Services.ppmm.sharedData.flush();
});

add_task(async function testGetFromChildExistingEnrollment() {
  const browserWindow =
    Services.wm.getMostRecentBrowserWindow("navigator:browser");

  // We only want to test the new process case, so make sure to shut down any
  // existing processes.
  Services.ppmm.releaseCachedProcesses();

  await ExperimentAPI.manager.enroll(
    NimbusTestUtils.factories.recipe.withFeatureConfig("qux", {
      branchSlug: "treatment",
      featureId: "testFeature",
      value: {
        enabled: false,
        testInt: 456,
      },
    }),
    "test"
  );

  // We don't have to wait for this to update in the client, but we *do* have to
  // flush to make it re-serialize the contents to make it available to new
  // processes.
  Services.ppmm.sharedData.flush();

  // Open a tab so that we have a content process.
  const tab = await BrowserTestUtils.openNewForegroundTab({
    gBrowser: browserWindow.gBrowser,
    url: "https://example.com",
    forceNewProcess: true,
  });
  const browser = tab.linkedBrowser;

  // Check that the experiment is available in the child process.
  await SpecialPowers.spawn(browser, [], async () => {
    await ExperimentAPI.ready();

    const meta = NimbusFeatures.testFeature.getEnrollmentMetadata();

    Assert.equal(meta.slug, "qux", "Experiment slug is correct");
    Assert.equal(meta.branch, "treatment", "Experiment branch slug is correct");

    Assert.deepEqual(
      NimbusFeatures.testFeature.getAllVariables(),
      { enabled: false, testInt: 456 },
      "Experiment values are correct"
    );

    Assert.equal(
      NimbusFeatures.testFeature.getVariable("enabled"),
      false,
      "Experiment values are correct"
    );

    Assert.equal(
      NimbusFeatures.testFeature.getVariable("testInt"),
      456,
      "Experiment values are correct"
    );
  });

  ExperimentAPI.manager.unenroll("qux");
  ExperimentAPI.manager.store._deleteForTests("qux");
  await NimbusTestUtils.flushStore();
  BrowserTestUtils.removeTab(tab);

  Services.ppmm.sharedData.flush();
});
