/* 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"
);

add_task(async function test_download_and_staged_install_trainhop_addon() {
  Services.fog.testResetFOG();

  // Sanity check (verifies 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);

  await AboutNewTabResourceMapping.updateTrainhopAddonState();
  const { pendingInstall } = await asyncAssertNimbusTrainhopAddonStaged({
    updateAddonVersion,
  });

  // Verify that we are still using the New Tab resources from the builtin add-on.
  assertNewTabResourceMapping();
  // Verify that no exposure event has been recorded until the New Tab resources
  // for the train-hop add-on version are actually in use.
  assertTrainhopAddonNimbusExposure({ expectedExposure: false });

  // Verify the ASRouterTargeting attribute does still report the
  // builtin version while the train-hop version is staged but
  // not in use yet.
  assertASRouterTargetingNewtabAddonVersion(BUILTIN_ADDON_VERSION);

  await cancelPendingInstall(pendingInstall);
  await nimbusFeatureCleanup();
  assertTrainhopAddonVersionPref("");
});

add_task(async function test_trainhop_addon_download_errors() {
  server.registerPathHandler("/data/invalid-zip.xpi", (_request, response) => {
    response.write("NOT_A_VALID_XPI");
  });

  const brokenManifestXPI = await AddonTestUtils.createTempXPIFile({
    "manifest.json": "not valid JSON",
  });
  server.registerPathHandler(
    "/data/broken-manifest.xpi",
    (request, response) => {
      server._handler._writeFileResponse(request, brokenManifestXPI, response);
    }
  );

  const invalidManifestXPI = AddonTestUtils.createTempWebExtensionFile({
    manifest: {
      version: `${BUILTIN_ADDON_VERSION}.123`,
      browser_specific_settings: {
        gecko: { id: BUILTIN_ADDON_ID },
      },
      // Invalid manifest property that is expected to hit a manifest
      // validation error.
      background: { scripts: "it-should-be-an-array.js" },
    },
  });
  server.registerPathHandler(
    "/data/invalid-manifest.xpi",
    (request, response) => {
      server._handler._writeFileResponse(request, invalidManifestXPI, response);
    }
  );

  const invalidSignatureXPI = AddonTestUtils.createTempWebExtensionFile({
    manifest: {
      version: `${BUILTIN_ADDON_VERSION}.123`,
      browser_specific_settings: {
        gecko: { id: BUILTIN_ADDON_ID },
      },
    },
  });
  server.registerPathHandler(
    "/data/invalid-signature.xpi",
    (request, response) => {
      server._handler._writeFileResponse(
        request,
        invalidSignatureXPI,
        response
      );
    }
  );

  await ExperimentAPI.ready();
  await testDownloadError("data/non-existing.xpi");
  await testDownloadError("data/invalid-zip.xpi");
  await testDownloadError("data/broken-manifest.xpi");
  await testDownloadError(
    "data/invalid-manifest.xpi",
    `${BUILTIN_ADDON_VERSION}.123`
  );
  const oldUsePrivilegedSignatures = AddonTestUtils.usePrivilegedSignatures;
  AddonTestUtils.usePrivilegedSignatures = false;
  await testDownloadError(
    "data/invalid-signature.xpi",
    `${BUILTIN_ADDON_VERSION}.123`,
    AddonManager.STATE_CANCELLED
  );
  AddonTestUtils.usePrivilegedSignatures = oldUsePrivilegedSignatures;

  async function testDownloadError(
    xpi_download_path,
    addon_version = "9999.0",
    expectedInstallState = AddonManager.STATE_DOWNLOAD_FAILED
  ) {
    Services.fog.testResetFOG();
    const nimbusFeatureCleanup = await NimbusTestUtils.enrollWithFeatureConfig(
      {
        featureId: TRAINHOP_NIMBUS_FEATURE_ID,
        value: {
          xpi_download_path,
          addon_version,
        },
      },
      { isRollout: true }
    );

    const promiseDownloadFailed =
      AddonTestUtils.promiseInstallEvent("onDownloadFailed");
    const promiseDownloadEnded =
      AddonTestUtils.promiseInstallEvent("onDownloadEnded");

    info("Trigger download and install train-hop add-on version");
    const promiseTrainhopRequest =
      AboutNewTabResourceMapping.updateTrainhopAddonState();

    info("Wait for AddonManager onDownloadFailed");
    const [install] = await Promise.race([
      promiseDownloadFailed,
      // Ensure the test fails right away if the unexpected
      // onDownloadEnded install event is resolved.
      promiseDownloadEnded,
    ]);

    Assert.equal(
      install.state,
      expectedInstallState,
      `Expect install state to be ${AddonManager._states.get(expectedInstallState)}`
    );

    info("Wait for updateTrainhopAddonState call to be resolved as expected");
    await promiseTrainhopRequest;

    Assert.deepEqual(
      await AddonManager.getAllInstalls(),
      [],
      "Expect no pending install to be found"
    );

    assertTrainhopAddonNimbusExposure({ expectedExposure: false });
    await nimbusFeatureCleanup();
  }
});

add_task(async function test_trainhop_cancel_on_version_check() {
  await testTrainhopCancelOnVersionCheck({
    updateAddonVersion: BUILTIN_ADDON_VERSION,
    message:
      "Test train-hop add-on version equal to the built-in add-on version",
  });
  await testTrainhopCancelOnVersionCheck({
    updateAddonVersion: "140.0.1",
    message:
      "Test train-hop add-on version lower than the built-in add-on version",
  });

  async function testTrainhopCancelOnVersionCheck({
    updateAddonVersion,
    message,
  }) {
    Services.fog.testResetFOG();
    info(message);
    // Sanity check (verifies built-in add-on resources have been mapped).
    assertNewTabResourceMapping();
    assertTrainhopAddonNimbusExposure({ expectedExposure: false });

    await asyncAssertNewTabAddon({
      locationName: "app-builtin-addons",
    });
    const { nimbusFeatureCleanup } = await setupNimbusTrainhopAddon({
      updateAddonVersion,
    });

    await AboutNewTabResourceMapping.updateTrainhopAddonState();
    Assert.deepEqual(
      await AddonManager.getAllInstalls(),
      [],
      "Expect no pending install to be found"
    );

    info("Verify the built-in version is still the one installed");
    await asyncAssertNewTabAddon({
      locationName: "app-builtin-addons",
      version: BUILTIN_ADDON_VERSION,
    });
    // Verify that we are still using the New Tab resources from the builtin add-on.
    assertNewTabResourceMapping();
    assertTrainhopAddonNimbusExposure({ expectedExposure: false });

    await nimbusFeatureCleanup();
  }
});

add_task(async function test_trainhop_addon_after_browser_restart() {
  // Sanity check (verifies built-in add-on resources have been mapped).
  assertNewTabResourceMapping();
  await asyncAssertNewTabAddon({
    locationName: BUILTIN_LOCATION_NAME,
  });
  assertTrainhopAddonVersionPref("");

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

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

  await AboutNewTabResourceMapping.updateTrainhopAddonState();
  await asyncAssertNimbusTrainhopAddonStaged({
    updateAddonVersion,
  });
  // Verify that we are still using the New Tab resources from the builtin add-on.
  assertNewTabResourceMapping();
  Assert.ok(
    !Glean.newtab.addonXpiUsed.testGetValue(),
    "Probe says we're not using an XPI"
  );

  info(
    "Simulated browser restart while train-hop add-on is pending installation"
  );
  Services.fog.testResetFOG();
  mockAboutNewTabUninit();
  await AddonTestUtils.promiseRestartManager();
  AboutNewTab.init();

  await asyncAssertNewTabAddon({
    locationName: PROFILE_LOCATION_NAME,
    version: updateAddonVersion,
  });
  const trainhopAddonPolicy = WebExtensionPolicy.getByID(BUILTIN_ADDON_ID);
  Assert.equal(
    trainhopAddonPolicy?.extension?.version,
    updateAddonVersion,
    "Got newtab WebExtensionPolicy instance for the train-hop add-on version"
  );

  assertNewTabResourceMapping(trainhopAddonPolicy.extension.rootURI.spec);
  Assert.ok(
    Glean.newtab.addonXpiUsed.testGetValue(),
    "Probe says we're using an XPI"
  );

  Assert.deepEqual(
    await AddonManager.getAllInstalls(),
    [],
    "Expect no pending install to be found"
  );

  await AboutNewTabResourceMapping.updateTrainhopAddonState();
  Assert.deepEqual(
    await AddonManager.getAllInstalls(),
    [],
    "Expect no additional pending install for the same train-hop add-on version"
  );

  assertTrainhopAddonNimbusExposure({ expectedExposure: true });
  assertTrainhopAddonVersionPref(updateAddonVersion);
  // Verify the ASRouterTargeting attribute is reporting the
  // train-hop version as expected.
  assertASRouterTargetingNewtabAddonVersion(updateAddonVersion);

  info("Simulate newtabTrainhopAddon nimbus feature unenrolled");
  await nimbusFeatureCleanup();
  assertTrainhopAddonVersionPref("");

  // Expect train-hop add-on to not be uninstalled yet because it is still
  // used by newtab resources mapping.
  await AboutNewTabResourceMapping.updateTrainhopAddonState();
  assertNewTabResourceMapping(trainhopAddonPolicy.extension.rootURI.spec);
  await asyncAssertNewTabAddon({
    locationName: PROFILE_LOCATION_NAME,
    version: updateAddonVersion,
  });

  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,
  });

  // Verify the ASRouterTargeting attribute is reporting the
  // built-in version again after the client was fully unrolled
  // from the train-hop experiment version.
  assertASRouterTargetingNewtabAddonVersion(BUILTIN_ADDON_VERSION);
});

add_task(async function test_builtin_version_upgrades() {
  // Sanity check (verifies built-in addon resources have been mapped).
  assertNewTabResourceMapping();
  await asyncAssertNewTabAddon({
    locationName: BUILTIN_LOCATION_NAME,
    version: BUILTIN_ADDON_VERSION,
  });
  assertTrainhopAddonVersionPref("");

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

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

  await AboutNewTabResourceMapping.updateTrainhopAddonState();
  await asyncAssertNimbusTrainhopAddonStaged({
    updateAddonVersion,
  });
  // Verify that we are still using the New Tab resources from the builtin add-on.
  assertNewTabResourceMapping();
  assertASRouterTargetingNewtabAddonVersion(BUILTIN_ADDON_VERSION);

  info(
    "Simulated browser restart while train-hop add-on is pending installation"
  );
  mockAboutNewTabUninit();
  await AddonTestUtils.promiseRestartManager();
  AboutNewTab.init();

  await asyncAssertNewTabAddon({
    locationName: PROFILE_LOCATION_NAME,
    version: updateAddonVersion,
  });
  const trainhopAddonPolicy = WebExtensionPolicy.getByID(BUILTIN_ADDON_ID);
  Assert.equal(
    trainhopAddonPolicy?.extension?.version,
    updateAddonVersion,
    "Got newtab WebExtensionPolicy instance for the train-hop add-on version"
  );
  assertNewTabResourceMapping(trainhopAddonPolicy.extension.rootURI.spec);
  assertASRouterTargetingNewtabAddonVersion(updateAddonVersion);

  info(
    "Simulated browser restart with a builtin add-on version higher than the train-hop add-on version"
  );
  // Mock a builtin version with an add-on version higher than the train-hop add-on version.
  const fakeUpdatedBuiltinVersion = "9999.0";
  const restoreBuiltinAddonsSubstitution =
    await overrideBuiltinAddonsSubstitution(fakeUpdatedBuiltinVersion);

  mockAboutNewTabUninit();
  await AddonTestUtils.promiseRestartManager();
  AboutNewTab.init();
  assertNewTabResourceMapping();
  await AboutNewTabResourceMapping.updateTrainhopAddonState();
  // Expect the newtab xpi to have been uninstalled and the updated
  // builtin add-on to be the newtab add-on version becoming active.
  await asyncAssertNewTabAddon({
    locationName: BUILTIN_LOCATION_NAME,
    version: fakeUpdatedBuiltinVersion,
  });
  // Verify the ASRouter targeting attribute is also reflecting
  // the update to the new built-in version.
  assertASRouterTargetingNewtabAddonVersion(fakeUpdatedBuiltinVersion);
  Assert.deepEqual(
    await AddonManager.getAllInstalls(),
    [],
    "Expect no pending install to be found"
  );

  // Cleanup
  mockAboutNewTabUninit();
  await restoreBuiltinAddonsSubstitution();
  await AddonTestUtils.promiseRestartManager();
  AboutNewTab.init();
  assertNewTabResourceMapping();
  await asyncAssertNewTabAddon({
    locationName: BUILTIN_LOCATION_NAME,
    version: BUILTIN_ADDON_VERSION,
  });
  await nimbusFeatureCleanup();

  async function overrideBuiltinAddonsSubstitution(updatedBuiltinVersion) {
    const { ExtensionTestCommon } = ChromeUtils.importESModule(
      "resource://testing-common/ExtensionTestCommon.sys.mjs"
    );
    const fakeBuiltinAddonsDir = AddonTestUtils.tempDir.clone();
    fakeBuiltinAddonsDir.append("builtin-addons-override");
    const addonDir = fakeBuiltinAddonsDir.clone();
    addonDir.append("newtab");
    await AddonTestUtils.promiseWriteFilesToDir(
      addonDir.path,
      ExtensionTestCommon.generateFiles({
        manifest: {
          version: updatedBuiltinVersion,
          browser_specific_settings: {
            gecko: { id: BUILTIN_ADDON_ID },
          },
        },
      })
    );
    const resProto = Cc[
      "@mozilla.org/network/protocol;1?name=resource"
    ].getService(Ci.nsIResProtocolHandler);
    let defaultBuiltinAddonsSubstitution =
      resProto.getSubstitution("builtin-addons");
    resProto.setSubstitutionWithFlags(
      "builtin-addons",
      Services.io.newFileURI(fakeBuiltinAddonsDir),
      Ci.nsISubstitutingProtocolHandler.ALLOW_CONTENT_ACCESS
    );

    // Verify we mocked an updated newtab builtin manifest as expected.
    const mockedManifest = await fetch(
      "resource://builtin-addons/newtab/manifest.json"
    ).then(r => r.json());
    Assert.equal(
      mockedManifest.version,
      fakeUpdatedBuiltinVersion,
      "Got the expected manifest version in the mocked builtin add-on manifest"
    );

    // Update built_in_addons.json accordingly.
    await overrideBuiltinsNewTabVersion(updatedBuiltinVersion);

    return async () => {
      await overrideBuiltinsNewTabVersion(BUILTIN_ADDON_VERSION);
      resProto.setSubstitutionWithFlags(
        "builtin-addons",
        defaultBuiltinAddonsSubstitution,
        Ci.nsISubstitutingProtocolHandler.ALLOW_CONTENT_ACCESS
      );
      fakeBuiltinAddonsDir.remove(true);
    };
  }

  async function overrideBuiltinsNewTabVersion(addon_version) {
    // Override newtab builtin version in built_in_addons.json metadata.
    const builtinsConfig = await fetch(
      "chrome://browser/content/built_in_addons.json"
    ).then(res => res.json());
    await AddonTestUtils.overrideBuiltIns({
      system: [],
      builtins: builtinsConfig.builtins
        .filter(entry => entry.addon_id === BUILTIN_ADDON_ID)
        .map(entry => {
          entry.addon_version = addon_version;
          return entry;
        }),
    });
  }
});

add_task(async function test_nonsystem_xpi_uninstalled() {
  let sandbox = sinon.createSandbox();

  // Sanity check (verifies builtin add-on resources have been mapped).
  assertNewTabResourceMapping();

  const updateAddonVersion = `${BUILTIN_ADDON_VERSION}.123`;
  const { nimbusFeatureCleanup } = await setupNimbusTrainhopAddon({
    updateAddonVersion,
  });
  assertTrainhopAddonVersionPref(updateAddonVersion);
  await AboutNewTabResourceMapping.updateTrainhopAddonState();

  info("Simulated restart after train-hop add-on version install pending");
  mockAboutNewTabUninit();
  await AddonTestUtils.promiseRestartManager();
  AboutNewTab.init();

  await asyncAssertNewTabAddon({
    locationName: PROFILE_LOCATION_NAME,
    version: updateAddonVersion,
  });

  // Install non-system signed newtab XPI (Expected to be installed
  // right away because the fake train-hop add-on version doesn't
  // have an onUpdateAvailable listener).
  const xpiVersion = `${BUILTIN_ADDON_VERSION}.456`;
  let extension = await ExtensionTestUtils.loadExtension({
    useAddonManager: "permanent",
    manifest: {
      version: xpiVersion,
      browser_specific_settings: {
        gecko: { id: BUILTIN_ADDON_ID },
      },
    },
  });
  const oldUsePrivilegedSignatures = AddonTestUtils.usePrivilegedSignatures;
  AddonTestUtils.usePrivilegedSignatures = false;
  await extension.startup();
  AddonTestUtils.usePrivilegedSignatures = oldUsePrivilegedSignatures;

  let addon = await asyncAssertNewTabAddon({
    locationName: PROFILE_LOCATION_NAME,
    version: xpiVersion,
  });
  Assert.deepEqual(
    addon.signedState,
    AddonManager.SIGNEDSTATE_SIGNED,
    "Got the expected signedState for the installed XPI version"
  );

  mockAboutNewTabUninit();
  await AddonTestUtils.promiseRestartManager();
  AboutNewTab.init();
  assertNewTabResourceMapping();

  sandbox.stub(AboutHomeStartupCache, "clearCacheAndUninit").returns();
  await AboutNewTabResourceMapping.updateTrainhopAddonState();
  Assert.ok(
    AboutHomeStartupCache.clearCacheAndUninit.called,
    "Uninstalling caused the startup cache to be cleared."
  );

  // Expect the newtab xpi to have been uninstalled and the updated
  // builtin add-on to be the newtab add-on version becoming active.
  await asyncAssertNewTabAddon({
    locationName: BUILTIN_LOCATION_NAME,
    version: BUILTIN_ADDON_VERSION,
  });
  // Along with uninstalling the non-system signed xpi we expect the
  // call to updateTrainhopAddonState to be installing the original
  // train-hop add-on version again.
  const { pendingInstall } = await asyncAssertNimbusTrainhopAddonStaged({
    updateAddonVersion,
  });
  await cancelPendingInstall(pendingInstall);

  await nimbusFeatureCleanup();
  sandbox.restore();
});
