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

"use strict";

const { PermissionTestUtils } = ChromeUtils.importESModule(
  "resource://testing-common/PermissionTestUtils.sys.mjs"
);

AddonTestUtils.init(this);
AddonTestUtils.overrideCertDB();
AddonTestUtils.usePrivilegedSignatures = false;
AddonTestUtils.createAppInfo(
  "xpcshell@tests.mozilla.org",
  "XPCShell",
  "42",
  "42"
);

// This pref is not set in Thunderbird, and needs to be true for the test to pass.
Services.prefs.setBoolPref("extensions.postDownloadThirdPartyPrompt", true);
// We test installations with http:-principals.
Services.prefs.setBoolPref("extensions.install.requireSecureOrigin", false);
Services.prefs.setBoolPref("dom.security.https_first", false);

const server = AddonTestUtils.createHttpServer({
  hosts: ["example.com", "example.org", "amo.example.com", "github.io"],
});

server.registerFile(
  `/addons/origins.xpi`,
  AddonTestUtils.createTempXPIFile({
    "manifest.json": {
      manifest_version: 2,
      name: "Install Origins test",
      version: "1.0",
      browser_specific_settings: {
        gecko: {
          id: "origins@example.com",
        },
      },
      install_origins: ["http://example.com"],
    },
  })
);

server.registerFile(
  `/addons/two_origins.xpi`,
  AddonTestUtils.createTempXPIFile({
    "manifest.json": {
      manifest_version: 2,
      name: "Install Origins test",
      version: "1.0",
      browser_specific_settings: {
        gecko: {
          id: "two_origins@example.com",
        },
      },
      install_origins: ["http://example.com", "http://example.org"],
    },
  })
);

server.registerFile(
  `/addons/no_origins.xpi`,
  AddonTestUtils.createTempXPIFile({
    "manifest.json": {
      manifest_version: 2,
      name: "Install Origins test",
      version: "1.0",
      browser_specific_settings: {
        gecko: {
          id: "no_origins@example.com",
        },
      },
    },
  })
);

server.registerFile(
  `/addons/empty_origins.xpi`,
  AddonTestUtils.createTempXPIFile({
    "manifest.json": {
      manifest_version: 2,
      name: "Install Origins test",
      version: "1.0",
      browser_specific_settings: {
        gecko: {
          id: "no_origins@example.com",
        },
      },
      install_origins: [],
    },
  })
);

server.registerFile(
  `/addons/v3_origins.xpi`,
  AddonTestUtils.createTempXPIFile({
    "manifest.json": {
      manifest_version: 3,
      name: "Install Origins test",
      version: "1.0",
      browser_specific_settings: {
        gecko: {
          id: "v3_origins@example.com",
        },
      },
      install_origins: ["http://example.com"],
    },
  })
);

server.registerFile(
  `/addons/v3_no_origins.xpi`,
  AddonTestUtils.createTempXPIFile({
    "manifest.json": {
      manifest_version: 3,
      name: "Install Origins test",
      version: "1.0",
      browser_specific_settings: {
        gecko: {
          id: "v3_no_origins@example.com",
        },
      },
    },
  })
);

let lastContentPage;

add_setup(() => {
  do_get_profile();
  Services.fog.initializeFOG();
  registerCleanupFunction(async () => lastContentPage?.close());
});

async function getBrowserWithPrincipal(principal) {
  lastContentPage = await ExtensionTestUtils.loadContentPage(
    principal.isNullPrincipal ? "about:blank" : principal.originNoSuffix + "/"
  );
  return lastContentPage.browser;
}

function testInstallEvent(expectTelemetry) {
  let gleanEvents = AddonTestUtils.getAMGleanEvents("install", {
    step: expectTelemetry.step,
  });
  Services.fog.testResetFOG();

  equal(gleanEvents.length, 1, "One glean event for install completion.");
  delete gleanEvents[0].addon_type;
  Assert.deepEqual(gleanEvents[0], expectTelemetry, "Glean telemetry matches.");
}

async function promiseCompleteWebInstall(
  install,
  triggeringPrincipal,
  expectPrompts = true
) {
  const browser = await getBrowserWithPrincipal(triggeringPrincipal);
  let listener;
  return new Promise(_resolve => {
    let resolve = () => {
      install.removeListener(listener);
      _resolve();
    };

    listener = {
      onDownloadFailed: resolve,
      onDownloadCancelled: resolve,
      onInstallFailed: resolve,
      onInstallCancelled: resolve,
      onInstallEnded: resolve,
      onInstallPostponed: resolve,
    };

    install.addListener(listener);

    // Observers to bypass panels and continue install.
    if (expectPrompts) {
      TestUtils.topicObserved("addon-install-blocked").then(([subject]) => {
        let installInfo = subject.wrappedJSObject;
        info(`==== test got addon-install-blocked ${subject} ${installInfo}`);
        installInfo.install();
      });

      TestUtils.topicObserved("addon-install-confirmation").then(subject => {
        info(`==== test got addon-install-confirmation`);
        let installInfo = subject.wrappedJSObject;
        for (let installer of installInfo.installs) {
          installer.install();
        }
      });
      TestUtils.topicObserved("webextension-permission-prompt").then(
        ([subject]) => {
          const { info } = subject.wrappedJSObject || {};
          info.resolve();
        }
      );
    }

    AddonManager.installAddonFromWebpage(
      "application/x-xpinstall",
      browser,
      triggeringPrincipal,
      install
    );
  });
}

async function testAddonInstall(test) {
  let { name, xpiUrl, installPrincipal, expectState, expectTelemetry } = test;
  info(`testAddonInstall: ${name}`);
  let expectInstall = expectState == AddonManager.STATE_INSTALLED;
  let install = await AddonManager.getInstallForURL(xpiUrl, {
    triggeringPrincipal: installPrincipal,
  });
  await promiseCompleteWebInstall(install, installPrincipal, expectInstall);

  // Test origins telemetry
  testInstallEvent(expectTelemetry);

  if (expectInstall) {
    equal(
      install.state,
      expectState,
      `${name} ${install.addon.id} install was completed`
    );
    // Wait the extension startup to ensure manifest.json has been read,
    // otherwise we get NS_ERROR_FILE_NOT_FOUND log spam.
    await WebExtensionPolicy.getByID(install.addon.id)?.readyPromise;
    await install.addon.uninstall();
  } else {
    equal(
      install.state,
      expectState,
      `${name} ${install.addon?.id} install failed`
    );
  }
}

let ssm = Services.scriptSecurityManager;
const PRINCIPAL_AMO = ssm.createContentPrincipalFromOrigin(
  "http://amo.example.com"
);
const PRINCIPAL_COM =
  ssm.createContentPrincipalFromOrigin("http://example.com");
const PRINCIPAL_ORG =
  ssm.createContentPrincipalFromOrigin("http://example.org");

const TESTS = [
  {
    name: "Install MV2 with install_origins",
    xpiUrl: "http://example.com/addons/origins.xpi",
    installPrincipal: PRINCIPAL_COM,
    expectState: AddonManager.STATE_INSTALLED,
    expectTelemetry: {
      step: "completed",
      addon_id: "origins@example.com",
      install_origins: "1",
    },
  },
  {
    name: "Install MV2 without install_origins",
    xpiUrl: "http://example.com/addons/no_origins.xpi",
    installPrincipal: PRINCIPAL_COM,
    expectState: AddonManager.STATE_INSTALLED,
    expectTelemetry: {
      step: "completed",
      addon_id: "no_origins@example.com",
      install_origins: "0",
    },
  },
  {
    name: "Install valid xpi location from invalid website",
    xpiUrl: "http://example.com/addons/origins.xpi",
    installPrincipal: PRINCIPAL_ORG,
    expectState: AddonManager.STATE_INSTALL_FAILED,
    expectTelemetry: {
      step: "failed",
      addon_id: "origins@example.com",
      error: "ERROR_INVALID_DOMAIN",
      install_origins: "1",
    },
  },
  {
    name: "Install invalid xpi location from valid website",
    xpiUrl: "http://example.org/addons/origins.xpi",
    installPrincipal: PRINCIPAL_COM,
    expectState: AddonManager.STATE_INSTALL_FAILED,
    expectTelemetry: {
      step: "failed",
      addon_id: "origins@example.com",
      error: "ERROR_INVALID_DOMAIN",
      install_origins: "1",
    },
  },
  {
    name: "Install MV3 with install_origins",
    xpiUrl: "http://example.com/addons/v3_origins.xpi",
    installPrincipal: PRINCIPAL_COM,
    expectState: AddonManager.STATE_INSTALLED,
    expectTelemetry: {
      step: "completed",
      addon_id: "v3_origins@example.com",
      install_origins: "1",
    },
  },
  {
    name: "Install MV3 with install_origins from AMO",
    xpiUrl: "http://example.com/addons/v3_origins.xpi",
    installPrincipal: PRINCIPAL_AMO,
    expectState: AddonManager.STATE_INSTALLED,
    expectTelemetry: {
      step: "completed",
      addon_id: "v3_origins@example.com",
      install_origins: "1",
    },
  },
  {
    name: "Install MV3 without install_origins",
    xpiUrl: "http://example.com/addons/v3_no_origins.xpi",
    installPrincipal: PRINCIPAL_COM,
    expectState: AddonManager.STATE_INSTALL_FAILED,
    expectTelemetry: {
      step: "failed",
      addon_id: "v3_no_origins@example.com",
      error: "ERROR_INVALID_DOMAIN",
      install_origins: "0",
    },
  },
  {
    // An installing principal with install permission is
    // considered "AMO" in code, and will always be allowed.
    name: "Install MV3 without install_origins from AMO",
    xpiUrl: "http://example.com/addons/v3_no_origins.xpi",
    installPrincipal: PRINCIPAL_AMO,
    expectState: AddonManager.STATE_INSTALLED,
    expectTelemetry: {
      step: "completed",
      addon_id: "v3_no_origins@example.com",
      install_origins: "0",
    },
  },
  {
    name: "Install MV3 without install_origins from null principal",
    xpiUrl: "http://example.com/addons/v3_no_origins.xpi",
    installPrincipal: Services.scriptSecurityManager.createNullPrincipal({}),
    expectState: AddonManager.STATE_CANCELLED,
    expectTelemetry: { step: "site_blocked", install_origins: "0" },
  },
  {
    name: "Install addon with two install_origins",
    xpiUrl: "http://example.com/addons/two_origins.xpi",
    installPrincipal: PRINCIPAL_ORG,
    expectState: AddonManager.STATE_INSTALLED,
    expectTelemetry: {
      step: "completed",
      addon_id: "two_origins@example.com",
      install_origins: "1",
    },
  },
  {
    name: "Install addon with two install_origins",
    xpiUrl: "http://example.com/addons/two_origins.xpi",
    installPrincipal: PRINCIPAL_COM,
    expectState: AddonManager.STATE_INSTALLED,
    expectTelemetry: {
      step: "completed",
      addon_id: "two_origins@example.com",
      install_origins: "1",
    },
  },
  {
    name: "Install from site with empty install_origins",
    xpiUrl: "http://example.com/addons/empty_origins.xpi",
    installPrincipal: PRINCIPAL_COM,
    expectState: AddonManager.STATE_INSTALL_FAILED,
    expectTelemetry: {
      step: "failed",
      addon_id: "no_origins@example.com",
      error: "ERROR_INVALID_DOMAIN",
      install_origins: "1",
    },
  },
  {
    name: "Install from site with empty install_origins",
    xpiUrl: "http://example.com/addons/empty_origins.xpi",
    installPrincipal: PRINCIPAL_ORG,
    expectState: AddonManager.STATE_INSTALL_FAILED,
    expectTelemetry: {
      step: "failed",
      addon_id: "no_origins@example.com",
      error: "ERROR_INVALID_DOMAIN",
      install_origins: "1",
    },
  },
  {
    name: "Install with empty install_origins from AMO",
    xpiUrl: "http://amo.example.com/addons/empty_origins.xpi",
    installPrincipal: PRINCIPAL_AMO,
    expectState: AddonManager.STATE_INSTALLED,
    expectTelemetry: {
      step: "completed",
      addon_id: "no_origins@example.com",
      install_origins: "1",
    },
  },
];

add_task(async function test_install_url() {
  Services.prefs.setBoolPref("extensions.install_origins.enabled", true);
  PermissionTestUtils.add(
    PRINCIPAL_AMO,
    "install",
    Services.perms.ALLOW_ACTION
  );
  await promiseStartupManager();

  for (let test of TESTS) {
    await testAddonInstall(test);
  }
});

add_task(async function test_install_origins_disabled() {
  Services.prefs.setBoolPref("extensions.install_origins.enabled", false);
  await testAddonInstall({
    name: "Install MV3 without install_origins, verification disabled",
    xpiUrl: "http://example.com/addons/v3_no_origins.xpi",
    installPrincipal: PRINCIPAL_COM,
    expectState: AddonManager.STATE_INSTALLED,
    expectTelemetry: {
      step: "completed",
      addon_id: "v3_no_origins@example.com",
    },
  });
});
