"use strict";

const { AddonRolloutAction } = ChromeUtils.importESModule(
  "resource://normandy/actions/AddonRolloutAction.sys.mjs"
);
const { BaseAction } = ChromeUtils.importESModule(
  "resource://normandy/actions/BaseAction.sys.mjs"
);
const { AddonRollouts } = ChromeUtils.importESModule(
  "resource://normandy/lib/AddonRollouts.sys.mjs"
);
const { NormandyTestUtils } = ChromeUtils.importESModule(
  "resource://testing-common/NormandyTestUtils.sys.mjs"
);

// Test that a simple recipe enrolls as expected
decorate_task(
  AddonRollouts.withTestMock(),
  ensureAddonCleanup(),
  withMockNormandyApi(),
  withStub(TelemetryEnvironment, "setExperimentActive"),
  withSendEventSpy(),
  async function simple_recipe_enrollment({
    mockNormandyApi,
    setExperimentActiveStub,
    sendEventSpy,
  }) {
    const recipe = {
      id: 1,
      arguments: {
        slug: "test-rollout",
        extensionApiId: 1,
      },
    };
    mockNormandyApi.extensionDetails = {
      [recipe.arguments.extensionApiId]: extensionDetailsFactory({
        id: recipe.arguments.extensionApiId,
      }),
    };

    const webExtStartupPromise =
      AddonTestUtils.promiseWebExtensionStartup(FIXTURE_ADDON_ID);

    const action = new AddonRolloutAction();
    await action.processRecipe(recipe, BaseAction.suitability.FILTER_MATCH);
    is(action.lastError, null, "lastError should be null");

    await webExtStartupPromise;

    // addon was installed
    const addon = await AddonManager.getAddonByID(FIXTURE_ADDON_ID);
    is(addon.id, FIXTURE_ADDON_ID, "addon should be installed");

    // rollout was stored
    const rollouts = await AddonRollouts.getAll();
    Assert.deepEqual(
      rollouts,
      [
        {
          recipeId: recipe.id,
          slug: "test-rollout",
          state: AddonRollouts.STATE_ACTIVE,
          extensionApiId: 1,
          addonId: FIXTURE_ADDON_ID,
          addonVersion: "1.0",
          xpiUrl: FIXTURE_ADDON_DETAILS["normandydriver-a-1.0"].url,
          xpiHash: FIXTURE_ADDON_DETAILS["normandydriver-a-1.0"].hash,
          xpiHashAlgorithm: "sha256",
        },
      ],
      "Rollout should be stored in db"
    );

    sendEventSpy.assertEvents([
      ["enroll", "addon_rollout", recipe.arguments.slug],
    ]);
    ok(
      setExperimentActiveStub.calledWithExactly("test-rollout", "active", {
        type: "normandy-addonrollout",
      }),
      "a telemetry experiment should be activated"
    );

    // cleanup installed addon
    await addon.uninstall();
  }
);

// Test that a rollout can update the addon
decorate_task(
  AddonRollouts.withTestMock(),
  ensureAddonCleanup(),
  withMockNormandyApi(),
  withSendEventSpy(),
  async function update_rollout({ mockNormandyApi, sendEventSpy }) {
    // first enrollment
    const recipe = {
      id: 1,
      arguments: {
        slug: "test-rollout",
        extensionApiId: 1,
      },
    };
    mockNormandyApi.extensionDetails = {
      [recipe.arguments.extensionApiId]: extensionDetailsFactory({
        id: recipe.arguments.extensionApiId,
      }),
      2: extensionDetailsFactory({
        id: 2,
        xpi: FIXTURE_ADDON_DETAILS["normandydriver-a-2.0"].url,
        version: "2.0",
        hash: FIXTURE_ADDON_DETAILS["normandydriver-a-2.0"].hash,
      }),
    };

    let webExtStartupPromise =
      AddonTestUtils.promiseWebExtensionStartup(FIXTURE_ADDON_ID);

    let action = new AddonRolloutAction();
    await action.processRecipe(recipe, BaseAction.suitability.FILTER_MATCH);
    is(action.lastError, null, "lastError should be null");

    await webExtStartupPromise;

    // addon was installed
    let addon = await AddonManager.getAddonByID(FIXTURE_ADDON_ID);
    is(addon.id, FIXTURE_ADDON_ID, "addon should be installed");
    is(addon.version, "1.0", "addon should be the correct version");

    // update existing enrollment
    recipe.arguments.extensionApiId = 2;
    webExtStartupPromise =
      AddonTestUtils.promiseWebExtensionStartup(FIXTURE_ADDON_ID);
    action = new AddonRolloutAction();
    await action.processRecipe(recipe, BaseAction.suitability.FILTER_MATCH);
    is(action.lastError, null, "lastError should be null");

    await webExtStartupPromise;

    addon = await AddonManager.getAddonByID(FIXTURE_ADDON_ID);
    is(addon.id, FIXTURE_ADDON_ID, "addon should still be installed");
    is(addon.version, "2.0", "addon should be the correct version");

    // rollout in the DB has been updated
    const rollouts = await AddonRollouts.getAll();
    Assert.deepEqual(
      rollouts,
      [
        {
          recipeId: recipe.id,
          slug: "test-rollout",
          state: AddonRollouts.STATE_ACTIVE,
          extensionApiId: 2,
          addonId: FIXTURE_ADDON_ID,
          addonVersion: "2.0",
          xpiUrl: FIXTURE_ADDON_DETAILS["normandydriver-a-2.0"].url,
          xpiHash: FIXTURE_ADDON_DETAILS["normandydriver-a-2.0"].hash,
          xpiHashAlgorithm: "sha256",
        },
      ],
      "Rollout should be stored in db"
    );

    sendEventSpy.assertEvents([
      ["enroll", "addon_rollout", "test-rollout"],
      ["update", "addon_rollout", "test-rollout"],
    ]);

    // Cleanup
    await addon.uninstall();
  }
);

// Re-running a recipe does nothing
decorate_task(
  AddonRollouts.withTestMock(),
  ensureAddonCleanup(),
  withMockNormandyApi(),
  withSendEventSpy(),
  async function rerun_recipe({ mockNormandyApi, sendEventSpy }) {
    const recipe = {
      id: 1,
      arguments: {
        slug: "test-rollout",
        extensionApiId: 1,
      },
    };
    mockNormandyApi.extensionDetails = {
      [recipe.arguments.extensionApiId]: extensionDetailsFactory({
        id: recipe.arguments.extensionApiId,
      }),
    };

    const webExtStartupPromise =
      AddonTestUtils.promiseWebExtensionStartup(FIXTURE_ADDON_ID);

    let action = new AddonRolloutAction();
    await action.processRecipe(recipe, BaseAction.suitability.FILTER_MATCH);
    is(action.lastError, null, "lastError should be null");

    await webExtStartupPromise;

    // addon was installed
    let addon = await AddonManager.getAddonByID(FIXTURE_ADDON_ID);
    is(addon.id, FIXTURE_ADDON_ID, "addon should be installed");
    is(addon.version, "1.0", "addon should be the correct version");

    // re-run the same recipe
    action = new AddonRolloutAction();
    await action.processRecipe(recipe, BaseAction.suitability.FILTER_MATCH);
    is(action.lastError, null, "lastError should be null");

    addon = await AddonManager.getAddonByID(FIXTURE_ADDON_ID);
    is(addon.id, FIXTURE_ADDON_ID, "addon should still be installed");
    is(addon.version, "1.0", "addon should be the correct version");

    // rollout in the DB has not been updated
    const rollouts = await AddonRollouts.getAll();
    Assert.deepEqual(
      rollouts,
      [
        {
          recipeId: recipe.id,
          slug: "test-rollout",
          state: AddonRollouts.STATE_ACTIVE,
          extensionApiId: 1,
          addonId: FIXTURE_ADDON_ID,
          addonVersion: "1.0",
          xpiUrl: FIXTURE_ADDON_DETAILS["normandydriver-a-1.0"].url,
          xpiHash: FIXTURE_ADDON_DETAILS["normandydriver-a-1.0"].hash,
          xpiHashAlgorithm: "sha256",
        },
      ],
      "Rollout should be stored in db"
    );

    sendEventSpy.assertEvents([["enroll", "addon_rollout", "test-rollout"]]);

    // Cleanup
    await addon.uninstall();
  }
);

// Conflicting rollouts
decorate_task(
  AddonRollouts.withTestMock(),
  ensureAddonCleanup(),
  withMockNormandyApi(),
  withSendEventSpy(),
  async function conflicting_rollout({ mockNormandyApi, sendEventSpy }) {
    const recipe = {
      id: 1,
      arguments: {
        slug: "test-rollout",
        extensionApiId: 1,
      },
    };
    mockNormandyApi.extensionDetails = {
      [recipe.arguments.extensionApiId]: extensionDetailsFactory({
        id: recipe.arguments.extensionApiId,
      }),
    };

    const webExtStartupPromise =
      AddonTestUtils.promiseWebExtensionStartup(FIXTURE_ADDON_ID);

    let action = new AddonRolloutAction();
    await action.processRecipe(recipe, BaseAction.suitability.FILTER_MATCH);
    is(action.lastError, null, "lastError should be null");

    await webExtStartupPromise;

    // addon was installed
    let addon = await AddonManager.getAddonByID(FIXTURE_ADDON_ID);
    is(addon.id, FIXTURE_ADDON_ID, "addon should be installed");
    is(addon.version, "1.0", "addon should be the correct version");

    // update existing enrollment
    action = new AddonRolloutAction();
    await action.processRecipe(
      {
        ...recipe,
        id: 2,
        arguments: {
          ...recipe.arguments,
          slug: "test-conflict",
        },
      },
      BaseAction.suitability.FILTER_MATCH
    );
    is(action.lastError, null, "lastError should be null");

    addon = await AddonManager.getAddonByID(FIXTURE_ADDON_ID);
    is(addon.id, FIXTURE_ADDON_ID, "addon should still be installed");
    is(addon.version, "1.0", "addon should be the correct version");

    // rollout in the DB has not been updated
    const rollouts = await AddonRollouts.getAll();
    Assert.deepEqual(
      rollouts,
      [
        {
          recipeId: recipe.id,
          slug: "test-rollout",
          state: AddonRollouts.STATE_ACTIVE,
          extensionApiId: 1,
          addonId: FIXTURE_ADDON_ID,
          addonVersion: "1.0",
          xpiUrl: FIXTURE_ADDON_DETAILS["normandydriver-a-1.0"].url,
          xpiHash: FIXTURE_ADDON_DETAILS["normandydriver-a-1.0"].hash,
          xpiHashAlgorithm: "sha256",
        },
      ],
      "Rollout should be stored in db"
    );

    sendEventSpy.assertEvents([
      [
        "enroll",
        "addon_rollout",
        "test-rollout",
        { addonId: FIXTURE_ADDON_ID },
      ],
      [
        "enrollFailed",
        "addon_rollout",
        "test-conflict",
        { reason: "conflict" },
      ],
    ]);

    // Cleanup
    await addon.uninstall();
  }
);

// Add-on ID changed
decorate_task(
  AddonRollouts.withTestMock(),
  ensureAddonCleanup(),
  withMockNormandyApi(),
  withSendEventSpy(),
  async function enroll_failed_addon_id_changed({
    mockNormandyApi,
    sendEventSpy,
  }) {
    const recipe = {
      id: 1,
      arguments: {
        slug: "test-rollout",
        extensionApiId: 1,
      },
    };
    mockNormandyApi.extensionDetails = {
      [recipe.arguments.extensionApiId]: extensionDetailsFactory({
        id: recipe.arguments.extensionApiId,
      }),
      2: extensionDetailsFactory({
        id: 2,
        extension_id: "normandydriver-b@example.com",
        xpi: FIXTURE_ADDON_DETAILS["normandydriver-b-1.0"].url,
        version: "1.0",
        hash: FIXTURE_ADDON_DETAILS["normandydriver-b-1.0"].hash,
      }),
    };

    const webExtStartupPromise =
      AddonTestUtils.promiseWebExtensionStartup(FIXTURE_ADDON_ID);

    let action = new AddonRolloutAction();
    await action.processRecipe(recipe, BaseAction.suitability.FILTER_MATCH);
    is(action.lastError, null, "lastError should be null");

    await webExtStartupPromise;

    // addon was installed
    let addon = await AddonManager.getAddonByID(FIXTURE_ADDON_ID);
    is(addon.id, FIXTURE_ADDON_ID, "addon should be installed");
    is(addon.version, "1.0", "addon should be the correct version");

    // update existing enrollment
    recipe.arguments.extensionApiId = 2;
    action = new AddonRolloutAction();
    await action.processRecipe(recipe, BaseAction.suitability.FILTER_MATCH);
    is(action.lastError, null, "lastError should be null");

    addon = await AddonManager.getAddonByID(FIXTURE_ADDON_ID);
    is(addon.id, FIXTURE_ADDON_ID, "addon should still be installed");
    is(addon.version, "1.0", "addon should be the correct version");

    // rollout in the DB has not been updated
    const rollouts = await AddonRollouts.getAll();
    Assert.deepEqual(
      rollouts,
      [
        {
          recipeId: recipe.id,
          slug: "test-rollout",
          state: AddonRollouts.STATE_ACTIVE,
          extensionApiId: 1,
          addonId: FIXTURE_ADDON_ID,
          addonVersion: "1.0",
          xpiUrl: FIXTURE_ADDON_DETAILS["normandydriver-a-1.0"].url,
          xpiHash: FIXTURE_ADDON_DETAILS["normandydriver-a-1.0"].hash,
          xpiHashAlgorithm: "sha256",
        },
      ],
      "Rollout should be stored in db"
    );

    sendEventSpy.assertEvents([
      ["enroll", "addon_rollout", "test-rollout"],
      [
        "updateFailed",
        "addon_rollout",
        "test-rollout",
        { reason: "addon-id-changed" },
      ],
    ]);

    // Cleanup
    await addon.uninstall();
  }
);

// Add-on upgrade required
decorate_task(
  AddonRollouts.withTestMock(),
  ensureAddonCleanup(),
  withMockNormandyApi(),
  withSendEventSpy(),
  async function enroll_failed_upgrade_required({
    mockNormandyApi,
    sendEventSpy,
  }) {
    const recipe = {
      id: 1,
      arguments: {
        slug: "test-rollout",
        extensionApiId: 1,
      },
    };
    mockNormandyApi.extensionDetails = {
      [recipe.arguments.extensionApiId]: extensionDetailsFactory({
        id: recipe.arguments.extensionApiId,
        xpi: FIXTURE_ADDON_DETAILS["normandydriver-a-2.0"].url,
        version: "2.0",
        hash: FIXTURE_ADDON_DETAILS["normandydriver-a-2.0"].hash,
      }),
      2: extensionDetailsFactory({
        id: 2,
      }),
    };

    const webExtStartupPromise =
      AddonTestUtils.promiseWebExtensionStartup(FIXTURE_ADDON_ID);

    let action = new AddonRolloutAction();
    await action.processRecipe(recipe, BaseAction.suitability.FILTER_MATCH);
    is(action.lastError, null, "lastError should be null");

    await webExtStartupPromise;

    // addon was installed
    let addon = await AddonManager.getAddonByID(FIXTURE_ADDON_ID);
    is(addon.id, FIXTURE_ADDON_ID, "addon should be installed");
    is(addon.version, "2.0", "addon should be the correct version");

    // update existing enrollment
    recipe.arguments.extensionApiId = 2;
    action = new AddonRolloutAction();
    await action.processRecipe(recipe, BaseAction.suitability.FILTER_MATCH);
    is(action.lastError, null, "lastError should be null");

    addon = await AddonManager.getAddonByID(FIXTURE_ADDON_ID);
    is(addon.id, FIXTURE_ADDON_ID, "addon should still be installed");
    is(addon.version, "2.0", "addon should be the correct version");

    // rollout in the DB has not been updated
    const rollouts = await AddonRollouts.getAll();
    Assert.deepEqual(
      rollouts,
      [
        {
          recipeId: recipe.id,
          slug: "test-rollout",
          state: AddonRollouts.STATE_ACTIVE,
          extensionApiId: 1,
          addonId: FIXTURE_ADDON_ID,
          addonVersion: "2.0",
          xpiUrl: FIXTURE_ADDON_DETAILS["normandydriver-a-2.0"].url,
          xpiHash: FIXTURE_ADDON_DETAILS["normandydriver-a-2.0"].hash,
          xpiHashAlgorithm: "sha256",
        },
      ],
      "Rollout should be stored in db"
    );

    sendEventSpy.assertEvents([
      ["enroll", "addon_rollout", "test-rollout"],
      [
        "updateFailed",
        "addon_rollout",
        "test-rollout",
        { reason: "upgrade-required" },
      ],
    ]);

    // Cleanup
    await addon.uninstall();
  }
);
