/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */

const { E10SUtils } = ChromeUtils.importESModule(
  "resource://gre/modules/E10SUtils.sys.mjs"
);

const URI_SECURE_COM = Services.io.newURI("https://example.com");
const URI_SECURE_ORG = Services.io.newURI("https://example.org");
const URI_INSECURE_ORG = Services.io.newURI("http://example.org");
const URI_FILE = Services.io.newURI("file:///path/to/dir");
const URI_EXTENSION = Services.io.newURI("moz-extension://fake-uuid");
const URI_EXT_PROTOCOL = Services.io.newURI("ext+custom://fake-url");
const URI_WEB_PROTOCOL = Services.io.newURI("web+custom://fake-url");
const URI_PRIVILEGEDMOZILLA = Services.io.newURI("https://addons.mozilla.org");

const fakeContentScriptSandbox = Cu.Sandbox(
  ["https://example.org", "moz-extension://fake-uuid"],
  {}
);

const ssm = Services.scriptSecurityManager;
const systemPrincipal = ssm.getSystemPrincipal();
const nullPrincipal = ssm.createNullPrincipal({});
const principalSecureCom = ssm.createContentPrincipal(URI_SECURE_COM, {});
const principalSecureOrg = ssm.createContentPrincipal(URI_SECURE_ORG, {});
const principalInsecureOrg = ssm.createContentPrincipal(URI_INSECURE_ORG, {});
const principalFile = ssm.createContentPrincipal(URI_FILE, {});
const principalExtension = ssm.createContentPrincipal(URI_EXTENSION, {});
const principalExpanded = Cu.getObjectPrincipal(fakeContentScriptSandbox);
const principalExtProtocol = ssm.createContentPrincipal(URI_EXT_PROTOCOL, {});
const principalWebProtocol = ssm.createContentPrincipal(URI_WEB_PROTOCOL, {});
const principalPrivilegedMozilla = ssm.createContentPrincipal(
  URI_PRIVILEGEDMOZILLA,
  {}
);

const {
  EXTENSION_REMOTE_TYPE,
  FILE_REMOTE_TYPE,
  FISSION_WEB_REMOTE_TYPE,
  NOT_REMOTE,
  PRIVILEGEDABOUT_REMOTE_TYPE,
  PRIVILEGEDMOZILLA_REMOTE_TYPE,
  SERVICEWORKER_REMOTE_TYPE,
  WEB_REMOTE_COOP_COEP_TYPE_PREFIX,
  WEB_REMOTE_TYPE,
} = E10SUtils;

const { REMOTE_WORKER_TYPE_SHARED, REMOTE_WORKER_TYPE_SERVICE } =
  Ci.nsIE10SUtils;

// Test ServiceWorker remoteType selection with multiprocess and/or site
// isolation enabled.
add_task(function test_get_remote_type_for_service_worker() {
  // ServiceWorkers with system or null principal are unexpected and we expect
  // the method call to throw.
  for (const principal of [systemPrincipal, nullPrincipal]) {
    Assert.throws(
      () =>
        E10SUtils.getRemoteTypeForWorkerPrincipal(
          principal,
          REMOTE_WORKER_TYPE_SERVICE,
          true,
          false
        ),
      /Unexpected system or null principal/,
      `Did raise an exception on "${principal.origin}" principal ServiceWorker`
    );
  }

  // ServiceWorker test cases:
  // - e10s + fission disabled:
  //   - extension principal + any preferred remote type => extension remote type
  //   - content principal + any preferred remote type => web remote type
  // - fission enabled:
  //   - extension principal + any preferred remote type => extension remote type
  //   - content principal + any preferred remote type => webServiceWorker=siteOrigin remote type
  function* getTestCase(fission = false) {
    const TEST_PRINCIPALS = [
      principalSecureCom,
      principalSecureOrg,
      principalExtension,
    ];

    const PREFERRED_REMOTE_TYPES = [
      E10SUtils.DEFAULT_REMOTE_TYPE,
      E10SUtils.WEB_REMOTE_TYPE,
      "fakeRemoteType",
    ];

    for (const principal of TEST_PRINCIPALS) {
      for (const preferred of PREFERRED_REMOTE_TYPES) {
        const msg = `ServiceWorker, principal=${
          principal.origin
        }, preferredRemoteType=${preferred}, ${fission ? "fission" : "e10s"}`;
        yield [
          msg,
          principal,
          REMOTE_WORKER_TYPE_SERVICE,
          true,
          fission,
          preferred,
        ];
      }
    }
  }

  // Test cases for e10s mode + fission disabled.
  for (const testCase of getTestCase(false)) {
    const [msg, principal, ...args] = testCase;
    let expected = E10SUtils.WEB_REMOTE_TYPE;

    if (principal == principalExtension) {
      expected = WebExtensionPolicy.useRemoteWebExtensions
        ? E10SUtils.EXTENSION_REMOTE_TYPE
        : E10SUtils.NOT_REMOTE;
    }

    equal(
      E10SUtils.getRemoteTypeForWorkerPrincipal(principal, ...args),
      expected,
      msg
    );
  }

  // Test cases for e10s mode + fission enabled.
  for (const testCase of getTestCase(true)) {
    const [msg, principal, ...args] = testCase;
    let expected = `${SERVICEWORKER_REMOTE_TYPE}=${principal.siteOrigin}`;

    if (principal == principalExtension) {
      expected = WebExtensionPolicy.useRemoteWebExtensions
        ? E10SUtils.EXTENSION_REMOTE_TYPE
        : E10SUtils.NOT_REMOTE;
    }

    equal(
      E10SUtils.getRemoteTypeForWorkerPrincipal(principal, ...args),
      expected,
      msg
    );
  }
});

// Test SharedWorker remoteType selection with multiprocess and/or site
// isolation enabled.
add_task(function test_get_remote_type_for_shared_worker() {
  // Verify that for shared worker registered from a web coop+coep remote type
  // we are going to select a web or fission remote type.
  for (const [principal, preferredRemoteType] of [
    [
      principalSecureCom,
      `${WEB_REMOTE_COOP_COEP_TYPE_PREFIX}=${principalSecureCom.siteOrigin}`,
    ],
  ]) {
    equal(
      E10SUtils.getRemoteTypeForWorkerPrincipal(
        principal,
        REMOTE_WORKER_TYPE_SHARED,
        true,
        false,
        preferredRemoteType
      ),
      WEB_REMOTE_TYPE,
      `Got WEB_REMOTE_TYPE on preferred ${preferredRemoteType} and fission disabled`
    );

    equal(
      E10SUtils.getRemoteTypeForWorkerPrincipal(
        principal,
        REMOTE_WORKER_TYPE_SHARED,
        true,
        true,
        preferredRemoteType
      ),
      `${FISSION_WEB_REMOTE_TYPE}=${principal.siteOrigin}`,
      `Got WEB_REMOTE_TYPE on preferred ${preferredRemoteType} and fission enabled`
    );
  }

  // For System principal shared worker we do select NOT_REMOTE or the preferred
  // remote type if is one of the explicitly allowed (NOT_REMOTE and
  // PRIVILEGEDABOUT_REMOTE_TYPE).
  for (const [principal, preferredRemoteType] of [
    [systemPrincipal, NOT_REMOTE],
    [systemPrincipal, PRIVILEGEDABOUT_REMOTE_TYPE],
  ]) {
    equal(
      E10SUtils.getRemoteTypeForWorkerPrincipal(
        principal,
        REMOTE_WORKER_TYPE_SHARED,
        true,
        true,
        preferredRemoteType
      ),
      preferredRemoteType,
      `Selected the preferred ${preferredRemoteType} on system principal shared worker`
    );
  }

  Assert.throws(
    () =>
      E10SUtils.getRemoteTypeForWorkerPrincipal(
        systemPrincipal,
        REMOTE_WORKER_TYPE_SHARED,
        true,
        true,
        "fakeRemoteType"
      ),
    /Failed to get a remoteType/,
    "Does fail explicitly on system worker for arbitrary preferredRemoteType"
  );

  // Behavior NOT_REMOTE preferredRemoteType for content principals with
  // multiprocess enabled.
  for (const [principal, expectedRemoteType] of [
    [
      principalSecureCom,
      `${FISSION_WEB_REMOTE_TYPE}=${principalSecureCom.siteOrigin}`,
    ],
    [
      principalExtension,
      WebExtensionPolicy.useRemoteWebExtensions
        ? EXTENSION_REMOTE_TYPE
        : NOT_REMOTE,
    ],
  ]) {
    equal(
      E10SUtils.getRemoteTypeForWorkerPrincipal(
        principal,
        REMOTE_WORKER_TYPE_SHARED,
        true,
        true,
        NOT_REMOTE
      ),
      expectedRemoteType,
      `Got ${expectedRemoteType} for content principal ${principal.siteOrigin}`
    );
  }

  // Shared worker registered for web+custom urls.
  for (const [preferredRemoteType, expectedRemoteType] of [
    [WEB_REMOTE_TYPE, WEB_REMOTE_TYPE],
    ["fakeRemoteType", "fakeRemoteType"],
    // This seems to be actually failing with a SecurityError
    // as soon as the SharedWorker constructor is being called with
    // a web+...:// url from an extension principal:
    //
    // [EXTENSION_REMOTE_TYPE, EXTENSION_REMOTE_TYPE],
  ]) {
    equal(
      E10SUtils.getRemoteTypeForWorkerPrincipal(
        principalWebProtocol,
        REMOTE_WORKER_TYPE_SHARED,
        true,
        true,
        preferredRemoteType
      ),
      expectedRemoteType,
      "Selected expected process for web+custom:// shared worker"
    );
  }

  // Shared worker registered for ext+custom urls.
  for (const [preferredRemoteType, expectedRemoteType] of [
    [WEB_REMOTE_TYPE, WEB_REMOTE_TYPE],
    ["fakeRemoteType", "fakeRemoteType"],
    // This seems to be actually prevented by failing a ClientIsValidPrincipalInfo
    // check (but only when the remote worker is being launched in the child process
    // and so after a remote Type has been selected).
    // [EXTENSION_REMOTE_TYPE, EXTENSION_REMOTE_TYPE],
  ]) {
    equal(
      E10SUtils.getRemoteTypeForWorkerPrincipal(
        principalExtProtocol,
        REMOTE_WORKER_TYPE_SHARED,
        true,
        true,
        preferredRemoteType
      ),
      expectedRemoteType,
      "Selected expected process for ext+custom:// shared worker"
    );
  }

  // Shared worker with a file principal.
  // NOTE: on android useSeparateFileUriProcess will be false and file uri are
  // going to run in the default web process.
  const expectedFileRemoteType = Services.prefs.getBoolPref(
    "browser.tabs.remote.separateFileUriProcess",
    false
  )
    ? FILE_REMOTE_TYPE
    : WEB_REMOTE_TYPE;

  for (const [preferredRemoteType, expectedRemoteType] of [
    [WEB_REMOTE_TYPE, expectedFileRemoteType],
    ["fakeRemoteType", expectedFileRemoteType],
  ]) {
    equal(
      E10SUtils.getRemoteTypeForWorkerPrincipal(
        principalFile,
        REMOTE_WORKER_TYPE_SHARED,
        true,
        true,
        preferredRemoteType
      ),
      expectedRemoteType,
      "Got expected remote type on file principal shared worker"
    );
  }

  // Shared worker related to a privilegedmozilla domain.
  // NOTE: separatePrivilegedMozilla will be false on android builds.
  const usePrivilegedMozilla = Services.prefs.getBoolPref(
    "browser.tabs.remote.separatePrivilegedMozillaWebContentProcess",
    false
  );
  const expectedRemoteType = usePrivilegedMozilla
    ? PRIVILEGEDMOZILLA_REMOTE_TYPE
    : `${FISSION_WEB_REMOTE_TYPE}=https://mozilla.org`;
  for (const preferredRemoteType of [
    PRIVILEGEDMOZILLA_REMOTE_TYPE,
    "fakeRemoteType",
  ]) {
    equal(
      E10SUtils.getRemoteTypeForWorkerPrincipal(
        principalPrivilegedMozilla,
        REMOTE_WORKER_TYPE_SHARED,
        true,
        true,
        preferredRemoteType
      ),
      expectedRemoteType,
      "Got expected remote type on privilegedmozilla principal shared worker"
    );
  }
});

// Test that we do throw on expanded principals.
add_task(function test_get_remote_type_throws_on_expanded_principals() {
  for (const workerType of [
    REMOTE_WORKER_TYPE_SHARED,
    REMOTE_WORKER_TYPE_SERVICE,
  ]) {
    Assert.throws(
      () =>
        E10SUtils.getRemoteTypeForWorkerPrincipal(
          principalExpanded,
          workerType,
          true,
          false
        ),
      /Unexpected expanded principal/,
      "Did raise an exception as expected"
    );
  }
});

// Test that NO_REMOTE is the remote type selected when multiprocess is disabled,
// there is no other checks special behaviors on particular principal or worker type.
add_task(function test_get_remote_type_multiprocess_disabled() {
  function* getTestCase() {
    const TEST_PRINCIPALS = [
      systemPrincipal,
      nullPrincipal,
      principalSecureCom,
      principalSecureOrg,
      principalInsecureOrg,
      principalFile,
      principalExtension,
    ];

    const PREFERRED_REMOTE_TYPES = [
      E10SUtils.DEFAULT_REMOTE_TYPE,
      E10SUtils.WEB_REMOTE_TYPE,
      "fakeRemoteType",
    ];

    for (const principal of TEST_PRINCIPALS) {
      for (const preferred of PREFERRED_REMOTE_TYPES) {
        const msg = `SharedWorker, principal=${principal.origin}, preferredRemoteType=${preferred}`;
        yield [
          msg,
          principal,
          REMOTE_WORKER_TYPE_SHARED,
          false,
          false,
          preferred,
        ];
      }
    }

    for (const principal of TEST_PRINCIPALS) {
      // system and null principals are disallowed for service workers, we throw
      // if passed to the E10SUtils method and we cover this scenario with a
      // separate test.
      if (principal.isSystemPrincipal || principal.isNullPrincipal) {
        continue;
      }
      for (const preferred of PREFERRED_REMOTE_TYPES) {
        const msg = `ServiceWorker with principal ${principal.origin} and preferredRemoteType ${preferred}`;
        yield [
          msg,
          principal,
          REMOTE_WORKER_TYPE_SERVICE,
          false,
          false,
          preferred,
        ];
      }
    }
  }

  for (const testCase of getTestCase()) {
    const [msg, ...args] = testCase;
    equal(
      E10SUtils.getRemoteTypeForWorkerPrincipal(...args),
      E10SUtils.NOT_REMOTE,
      `Expect NOT_REMOTE on disabled multiprocess: ${msg}`
    );
  }
});
