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

"use strict";

/* import-globals-from ../../../../extensions/newtab/test/xpcshell/head.js */

/* import-globals-from head_nimbus_trainhop.js */

const { AboutHomeStartupCache } = ChromeUtils.importESModule(
  "resource:///modules/AboutHomeStartupCache.sys.mjs"
);
const { sinon } = ChromeUtils.importESModule(
  "resource://testing-common/Sinon.sys.mjs"
);
const { FirstStartup } = ChromeUtils.importESModule(
  "resource://gre/modules/FirstStartup.sys.mjs"
);
const { updateAppInfo } = ChromeUtils.importESModule(
  "resource://testing-common/AppInfo.sys.mjs"
);

const PREF_CATEGORY_TASKS = "first-startup.category-tasks-enabled";
const CATEGORY_NAME = "first-startup-new-profile";

add_setup(async () => {
  Services.fog.testResetFOG();
  updateAppInfo();
});

/**
 * Test that AboutNewTabResourceMapping has a first-startup-new-profile
 * category entry registered for it for the
 * AboutNewTabResourceMapping.firstStartupNewProfile method.
 */
add_task(async function test_is_firstStartupNewProfile_registered() {
  const entry = Services.catMan.getCategoryEntry(
    CATEGORY_NAME,
    "resource:///modules/AboutNewTabResourceMapping.sys.mjs"
  );
  Assert.ok(
    entry,
    "An entry should exist for resource:///modules/AboutNewTabResourceMapping.sys.mjs"
  );
  Assert.equal(
    entry,
    "AboutNewTabResourceMapping.firstStartupNewProfile",
    "Entry value should point to the `firstStartupNewProfile` method"
  );
});

/**
 * Test that the firstStartupNewProfile hook gets called during FirstStartup
 * and performs a restartless install of a train-hop add-on when Nimbus is
 * configured with one.
 */
add_task(
  { skip_if: () => !AppConstants.MOZ_NORMANDY },
  async function test_firstStartup_trainhop_restartless_install() {
    // Enable category tasks for first startup
    Services.prefs.setBoolPref(PREF_CATEGORY_TASKS, true);
    FirstStartup.resetForTesting();

    // Reset AboutNewTabResourceMapping state so firstStartupNewProfile can run
    mockAboutNewTabUninit();

    // Sanity check - verify built-in add-on resources have been mapped
    assertNewTabResourceMapping();
    await asyncAssertNewTabAddon({
      locationName: BUILTIN_LOCATION_NAME,
    });
    assertTrainhopAddonNimbusExposure({ expectedExposure: false });

    const updateAddonVersion = `${BUILTIN_ADDON_VERSION}.123`;

    const { nimbusFeatureCleanup } = await setupNimbusTrainhopAddon({
      updateAddonVersion,
    });
    assertTrainhopAddonVersionPref(updateAddonVersion);

    // Track whether firstStartupNewProfile was called
    let sandbox = sinon.createSandbox();
    let firstStartupNewProfileSpy = sandbox.spy(
      AboutNewTabResourceMapping,
      "firstStartupNewProfile"
    );
    let aboutHomeStartupClearCacheStub = sandbox.stub(
      AboutHomeStartupCache,
      "clearCacheAndUninit"
    );

    let submissionPromise = new Promise(resolve => {
      GleanPings.firstStartup.testBeforeNextSubmit(() => {
        Assert.equal(FirstStartup.state, FirstStartup.SUCCESS);
        resolve();
      });
    });

    // Run FirstStartup which should trigger our category hook
    FirstStartup.init(true /* newProfile */);

    await submissionPromise;

    Assert.ok(
      firstStartupNewProfileSpy.calledOnce,
      "firstStartupNewProfile should have been called"
    );
    Assert.ok(
      aboutHomeStartupClearCacheStub.calledOnce,
      "AboutHomeStartupCache.clearCacheAndUninit called after installing train-hop"
    );

    // The train-hop add-on should have been installed restartlessly
    let addon = await asyncAssertNewTabAddon({
      locationName: PROFILE_LOCATION_NAME,
      version: updateAddonVersion,
    });

    Assert.ok(addon, "Train-hop add-on should be installed");

    // No pending installs should remain since we did a restartless install
    Assert.deepEqual(
      await AddonManager.getAllInstalls(),
      [],
      "Expect no pending install for restartless install"
    );

    sandbox.restore();

    await nimbusFeatureCleanup();
    info(
      "Simulated browser restart while newtabTrainhopAddon nimbus feature is unenrolled"
    );
    mockAboutNewTabUninit();
    await AddonTestUtils.promiseRestartManager();
    AboutNewTab.init();

    // Expected bundled newtab resources mapping for this session.
    assertNewTabResourceMapping();
    await AboutNewTabResourceMapping.updateTrainhopAddonState();
    await asyncAssertNewTabAddon({
      locationName: BUILTIN_LOCATION_NAME,
      version: BUILTIN_ADDON_VERSION,
    });

    assertTrainhopAddonVersionPref("");
    Services.prefs.clearUserPref(PREF_CATEGORY_TASKS);
  }
);

/**
 * Test that if AboutNewTabResourceMapping.init() has already been called
 * by the time firstStartupNewProfile runs, it logs an error and exits early.
 * This is not an expected or realistic condition, but we cover it all the same.
 */
add_task(
  { skip_if: () => !AppConstants.MOZ_NORMANDY },
  async function test_firstStartup_after_initialization() {
    // Initialize AboutNewTabResourceMapping before FirstStartup runs.
    AboutNewTabResourceMapping.init();
    Assert.ok(
      AboutNewTabResourceMapping.initialized,
      "AboutNewTabResourceMapping should be initialized"
    );

    Services.prefs.setBoolPref(PREF_CATEGORY_TASKS, true);
    FirstStartup.resetForTesting();

    const updateAddonVersion = `${BUILTIN_ADDON_VERSION}.456`;

    const { nimbusFeatureCleanup } = await setupNimbusTrainhopAddon({
      updateAddonVersion,
    });

    // Track error logging
    let errorLogged = false;
    let sandbox = sinon.createSandbox();
    sandbox.stub(AboutNewTabResourceMapping.logger, "error").callsFake(() => {
      errorLogged = true;
    });

    let submissionPromise = new Promise(resolve => {
      GleanPings.firstStartup.testBeforeNextSubmit(() => {
        resolve();
      });
    });

    FirstStartup.init(true /* newProfile */);
    await submissionPromise;

    Assert.ok(
      errorLogged,
      "An error should have been logged when trying to run after initialization"
    );

    // The add-on should NOT have been installed since we were too late
    await asyncAssertNewTabAddon({
      locationName: BUILTIN_LOCATION_NAME,
      version: BUILTIN_ADDON_VERSION,
    });

    sandbox.restore();
    await nimbusFeatureCleanup();
    Services.prefs.clearUserPref(PREF_CATEGORY_TASKS);
  }
);

/**
 * Test that firstStartupNewProfile doesn't run when the category tasks pref
 * is disabled.
 */
add_task(
  { skip_if: () => !AppConstants.MOZ_NORMANDY },
  async function test_firstStartup_category_disabled() {
    // Disable category tasks
    Services.prefs.setBoolPref(PREF_CATEGORY_TASKS, false);
    FirstStartup.resetForTesting();

    // Reset AboutNewTabResourceMapping state
    mockAboutNewTabUninit();

    const updateAddonVersion = `${BUILTIN_ADDON_VERSION}.789`;

    const { nimbusFeatureCleanup } = await setupNimbusTrainhopAddon({
      updateAddonVersion,
    });

    let sandbox = sinon.createSandbox();
    let firstStartupNewProfileSpy = sandbox.spy(
      AboutNewTabResourceMapping,
      "firstStartupNewProfile"
    );

    let submissionPromise = new Promise(resolve => {
      GleanPings.firstStartup.testBeforeNextSubmit(() => {
        resolve();
      });
    });

    FirstStartup.init(true /* newProfile */);
    await submissionPromise;

    Assert.ok(
      !firstStartupNewProfileSpy.called,
      "firstStartupNewProfile should not have been called when pref is disabled"
    );

    // The add-on should still be the builtin version
    await asyncAssertNewTabAddon({
      locationName: BUILTIN_LOCATION_NAME,
      version: BUILTIN_ADDON_VERSION,
    });

    sandbox.restore();
    await nimbusFeatureCleanup();
    Services.prefs.clearUserPref(PREF_CATEGORY_TASKS);
  }
);

/**
 * Test that if AboutNewTabResourceMapping.init() is called after the XPI
 * download has started but before onInstallPostponed is called, we skip
 * attempting to force the restartless install and fall back to a staged
 * install instead.
 */
add_task(
  { skip_if: () => !AppConstants.MOZ_NORMANDY },
  async function test_firstStartup_init_during_download() {
    Services.prefs.setBoolPref(PREF_CATEGORY_TASKS, true);
    FirstStartup.resetForTesting();

    // Reset AboutNewTabResourceMapping state so firstStartupNewProfile can run
    mockAboutNewTabUninit();

    assertNewTabResourceMapping();
    await asyncAssertNewTabAddon({
      locationName: BUILTIN_LOCATION_NAME,
    });

    const updateAddonVersion = `${BUILTIN_ADDON_VERSION}.999`;

    const { nimbusFeatureCleanup } = await setupNimbusTrainhopAddon({
      updateAddonVersion,
    });
    assertTrainhopAddonVersionPref(updateAddonVersion);

    // Stub updateTrainhopAddonState to call init() in the middle of its execution
    let sandbox = sinon.createSandbox();
    let aboutNewTabInitSpy = sandbox.spy(AboutNewTabResourceMapping, "init");

    let originalUpdateTrainhopAddonState =
      AboutNewTabResourceMapping.updateTrainhopAddonState.bind(
        AboutNewTabResourceMapping
      );

    let updateTrainhopStarted = false;
    sandbox
      .stub(AboutNewTabResourceMapping, "updateTrainhopAddonState")
      .callsFake(async function (forceRestartlessInstall) {
        updateTrainhopStarted = true;

        // Start the update process
        let updatePromise = originalUpdateTrainhopAddonState(
          forceRestartlessInstall
        );

        // Call init immediately after starting the update, simulating
        // the browser window opening during the XPI download
        info(
          "Calling AboutNewTabResourceMapping.init() during updateTrainhopAddonState"
        );
        AboutNewTabResourceMapping.init();

        // Wait for the update to complete
        await updatePromise;
      });

    let submissionPromise = new Promise(resolve => {
      GleanPings.firstStartup.testBeforeNextSubmit(() => {
        Assert.equal(FirstStartup.state, FirstStartup.SUCCESS);
        resolve();
      });
    });

    FirstStartup.init(true /* newProfile */);
    await submissionPromise;

    Assert.ok(
      updateTrainhopStarted,
      "updateTrainhopAddonState should have started"
    );
    Assert.ok(
      aboutNewTabInitSpy.calledOnce,
      "AboutNewTabResourceMapping.init should have been called"
    );

    // The add-on should be staged for install, not installed restartlessly
    await asyncAssertNewTabAddon({
      locationName: BUILTIN_LOCATION_NAME,
      version: BUILTIN_ADDON_VERSION,
    });

    // Verify there's a pending install
    const pendingInstall = (await AddonManager.getAllInstalls()).find(
      install => install.addon.id === BUILTIN_ADDON_ID
    );
    Assert.ok(pendingInstall, "Should have a pending install");
    Assert.equal(
      pendingInstall.state,
      AddonManager.STATE_POSTPONED,
      "Install should be postponed"
    );
    Assert.equal(
      pendingInstall.addon.version,
      updateAddonVersion,
      "Pending install should be for the train-hop version"
    );

    // Clean up
    await cancelPendingInstall(pendingInstall);
    sandbox.restore();
    await nimbusFeatureCleanup();
    assertTrainhopAddonVersionPref("");
    Services.prefs.clearUserPref(PREF_CATEGORY_TASKS);
  }
);

/**
 * Test that the TRAINHOP_NIMBUS_FIRST_STARTUP_FEATURE_ID Nimbus feature can be
 * used to remotely disable the FirstStartup force-install flow.
 */
add_task(
  { skip_if: () => !AppConstants.MOZ_NORMANDY },
  async function test_firstStartup_remote_disable() {
    // Enable category tasks for first startup
    Services.prefs.setBoolPref(PREF_CATEGORY_TASKS, true);
    FirstStartup.resetForTesting();

    // Reset AboutNewTabResourceMapping state so firstStartupNewProfile can run
    mockAboutNewTabUninit();

    // Sanity check - verify built-in add-on resources have been mapped
    assertNewTabResourceMapping();
    await asyncAssertNewTabAddon({
      locationName: BUILTIN_LOCATION_NAME,
    });
    assertTrainhopAddonNimbusExposure({ expectedExposure: false });

    const updateAddonVersion = `${BUILTIN_ADDON_VERSION}.123`;

    const { nimbusFeatureCleanup } = await setupNimbusTrainhopAddon({
      updateAddonVersion,
    });
    assertTrainhopAddonVersionPref(updateAddonVersion);

    const firstStartupFeatureCleanup =
      await NimbusTestUtils.enrollWithFeatureConfig(
        {
          featureId: TRAINHOP_NIMBUS_FIRST_STARTUP_FEATURE_ID,
          value: { enabled: false },
        },
        { isRollout: true }
      );

    // Track whether firstStartupNewProfile was called
    let sandbox = sinon.createSandbox();
    let firstStartupNewProfileSpy = sandbox.spy(
      AboutNewTabResourceMapping,
      "firstStartupNewProfile"
    );

    let submissionPromise = new Promise(resolve => {
      GleanPings.firstStartup.testBeforeNextSubmit(() => {
        Assert.equal(FirstStartup.state, FirstStartup.SUCCESS);
        resolve();
      });
    });

    // Run FirstStartup which should trigger our category hook
    FirstStartup.init(true /* newProfile */);

    await submissionPromise;

    Assert.ok(
      firstStartupNewProfileSpy.calledOnce,
      "firstStartupNewProfile should have been called"
    );

    // The add-on should still be the builtin version
    await asyncAssertNewTabAddon({
      locationName: BUILTIN_LOCATION_NAME,
      version: BUILTIN_ADDON_VERSION,
    });

    sandbox.restore();
    await nimbusFeatureCleanup();
    await firstStartupFeatureCleanup();
    assertTrainhopAddonVersionPref("");
    Services.prefs.clearUserPref(PREF_CATEGORY_TASKS);
  }
);
