function createSandbox() {
  const uri = Services.io.newURI("https://example.com");
  const principal = Services.scriptSecurityManager.createContentPrincipal(
    uri,
    {}
  );
  return new Cu.Sandbox(principal, {});
}

add_task(async function testReactionJob() {
  const sandbox = createSandbox();

  sandbox.eval(`
var testPromise = Promise.resolve(10);
`);

  // Calling `Promise.prototype.then` from sandbox performs GetFunctionRealm
  // on wrapped `resolve` in sandbox realm, and it fails to unwrap the security
  // wrapper. The reaction job should be created with sandbox realm.
  const p = new Promise(resolve => {
    sandbox.resolve = resolve;

    sandbox.eval(`
testPromise.then(resolve);
`);
  });

  const result = await p;

  equal(result, 10);
});

add_task(async function testReactionJobNuked() {
  const sandbox = createSandbox();

  sandbox.eval(`
var testPromise = Promise.resolve(10);
`);

  // Calling `Promise.prototype.then` from sandbox performs GetFunctionRealm
  // on wrapped `resolve` in sandbox realm, and it fails to unwrap the security
  // wrapper. The reaction job should be created with sandbox realm.
  const p1 = new Promise(resolve => {
    sandbox.resolve = resolve;

    sandbox.eval(`
testPromise.then(resolve);
`);

    // Given the reaction job is created with the sandbox realm, nuking the
    // sandbox prevents the job gets executed.
    Cu.nukeSandbox(sandbox);
  });

  const p2 = Promise.resolve(11);

  // Given the p1 doesn't get resolved, p2 should win.
  const result = await Promise.race([p1, p2]);

  equal(result, 11);
});

add_task(async function testReactionJobWithXray() {
  const sandbox = createSandbox();

  sandbox.eval(`
var testPromise = Promise.resolve(10);
`);

  // Calling `Promise.prototype.then` from privileged realm via Xray uses
  // privileged `Promise.prototype.then` function, and GetFunctionRealm
  // performed there successfully gets top-level realm. The reaction job
  // should be created with top-level realm.
  const result = await new Promise(resolve => {
    sandbox.testPromise.then(resolve);

    // Given the reaction job is created with the top-level realm, nuking the
    // sandbox doesn't affect the reaction job.
    Cu.nukeSandbox(sandbox);
  });

  equal(result, 10);
});

add_task(async function testBoundReactionJob() {
  const sandbox = createSandbox();

  sandbox.eval(`
var resolve = undefined;
var callbackPromise = new Promise(r => { resolve = r; });
var callback = function (v) { resolve(v + 1); };
`);

  // Create a bound function where its realm is privileged realm, and
  // its target is from sandbox realm.
  sandbox.bound_callback = Function.prototype.bind.call(
    sandbox.callback,
    sandbox
  );

  // Calling `Promise.prototype.then` from sandbox performs GetFunctionRealm
  // and it fails. The reaction job should be created with sandbox realm.
  sandbox.eval(`
Promise.resolve(10).then(bound_callback);
`);

  const result = await sandbox.callbackPromise;
  equal(result, 11);
});

add_task(async function testThenableJob() {
  const sandbox = createSandbox();

  const p = new Promise(resolve => {
    // Create a bound function where its realm is privileged realm, and
    // its target is from sandbox realm.
    sandbox.then = function (onFulfilled, onRejected) {
      resolve(10);
    };
  });

  // Creating a promise thenable job in the following `Promise.resolve` performs
  // GetFunctionRealm on the bound thenable.then and fails. The reaction job
  // should be created with sandbox realm.
  sandbox.eval(`
var thenable = {
  then: then,
};

Promise.resolve(thenable);
`);

  const result = await p;
  equal(result, 10);
});

add_task(async function testThenableJobNuked() {
  const sandbox = createSandbox();

  let called = false;
  sandbox.then = function (onFulfilled, onRejected) {
    called = true;
  };

  // Creating a promise thenable job in the following `Promise.resolve` performs
  // GetFunctionRealm on the bound thenable.then and fails. The reaction job
  // should be created with sandbox realm.
  sandbox.eval(`
var thenable = {
  then: then,
};

Promise.resolve(thenable);
`);

  Cu.nukeSandbox(sandbox);

  // Drain the job queue, to make sure we hit dead object error inside the
  // thenable job.
  await Promise.resolve(10);

  equal(
    Services.console.getMessageArray().find(x => {
      return x.toString().includes("can't access dead object");
    }) !== undefined,
    true
  );
  equal(called, false);
});

add_task(async function testThenableJobAccessError() {
  const sandbox = createSandbox();

  let accessed = false;
  sandbox.thenable = {
    get then() {
      accessed = true;
    },
  };

  // The following operation silently fails when accessing `then` property.
  sandbox.eval(`
var x = typeof thenable.then;

Promise.resolve(thenable);
`);

  equal(accessed, false);
});

add_task(async function testBoundThenableJob() {
  const sandbox = createSandbox();

  sandbox.eval(`
var resolve = undefined;
var callbackPromise = new Promise(r => { resolve = r; });
var callback = function (v) { resolve(v + 1); };

var then = function(onFulfilled, onRejected) {
  onFulfilled(10);
};
`);

  // Create a bound function where its realm is privileged realm, and
  // its target is from sandbox realm.
  sandbox.bound_then = Function.prototype.bind.call(sandbox.then, sandbox);

  // Creating a promise thenable job in the following `Promise.resolve` performs
  // GetFunctionRealm on the bound thenable.then and fails. The reaction job
  // should be created with sandbox realm.
  sandbox.eval(`
var thenable = {
  then: bound_then,
};

Promise.resolve(thenable).then(callback);
`);

  const result = await sandbox.callbackPromise;
  equal(result, 11);
});
