// Creates a new iframe in `doc`, calls `func` on it and appends it as a child
// of `doc`.
// Returns a promise that resolves to the iframe once loaded (successfully or
// not).
// The iframe is removed from `doc` once test `t` is done running.
//
// NOTE: There exists no interoperable way to check whether an iframe failed to
// load, so this should only be used when the iframe is expected to load. It
// also means we cannot wire the iframe's `error` event to a promise
// rejection. See: https://github.com/whatwg/html/issues/125
function appendIframeWith(t, doc, func) {
  return new Promise(resolve => {
      const child = doc.createElement("iframe");
      t.add_cleanup(() => child.remove());

      child.addEventListener("load", () => resolve(child), { once: true });
      func(child);
      doc.body.appendChild(child);
    });
}

// Appends a child iframe to `doc` sourced from `src`.
//
// See `appendIframeWith()` for more details.
function appendIframe(t, doc, src) {
  return appendIframeWith(t, doc, child => { child.src = src; });
}

// Registers an event listener that will resolve this promise when this
// window receives a message posted to it.
//
// `options` has the following shape:
//
//  {
//    source: If specified, this function waits for the first message from the
//      given source only, ignoring other messages.
//
//    filter: If specified, this function calls `filter` on each incoming
//      message, and resolves iff it returns true.
//  }
//
function futureMessage(options) {
  return new Promise(resolve => {
    window.addEventListener("message", (e) => {
      if (options?.source && options.source !== e.source) {
        return;
      }

      if (options?.filter && !options.filter(e.data)) {
        return;
      }

      resolve(e.data);
    });
  });
};

// Like `promise_test()`, but executes tests in parallel like `async_test()`.
//
// Cribbed from COEP tests.
function promise_test_parallel(promise, description) {
  async_test(test => {
    promise(test)
        .then(() => test.done())
        .catch(test.step_func(error => { throw error; }));
  }, description);
};

async function postMessageAndAwaitReply(target, message) {
  const reply = futureMessage({ source: target });
  target.postMessage(message, "*");
  return await reply;
}

// Maps protocol (without the trailing colon) and address space to port.
const SERVER_PORTS = {
  "http": {
    "local": {{ports[http][0]}},
    "private": {{ports[http-private][0]}},
    "public": {{ports[http-public][0]}},
  },
  "https": {
    "local": {{ports[https][0]}},
    "other-local": {{ports[https][1]}},
    "private": {{ports[https-private][0]}},
    "public": {{ports[https-public][0]}},
  },
  "ws": {
    "local": {{ports[ws][0]}},
  },
  "wss": {
    "local": {{ports[wss][0]}},
  },
};

// A `Server` is a web server accessible by tests. It has the following shape:
//
// {
//   addressSpace: the IP address space of the server ("local", "private" or
//     "public"),
//   name: a human-readable name for the server,
//   port: the port on which the server listens for connections,
//   protocol: the protocol (including trailing colon) spoken by the server,
// }
//
// Constants below define the available servers, which can also be accessed
// programmatically with `get()`.
class Server {
  // Maps the given `protocol` (without a trailing colon) and `addressSpace` to
  // a server. Returns null if no such server exists.
  static get(protocol, addressSpace) {
    const ports = SERVER_PORTS[protocol];
    if (ports === undefined) {
      return null;
    }

    const port = ports[addressSpace];
    if (port === undefined) {
      return null;
    }

    return {
      addressSpace,
      name: `${protocol}-${addressSpace}`,
      port,
      protocol: protocol + ':',
    };
  }

  static HTTP_LOCAL = Server.get("http", "local");
  static HTTP_PRIVATE = Server.get("http", "private");
  static HTTP_PUBLIC = Server.get("http", "public");
  static HTTPS_LOCAL = Server.get("https", "local");
  static OTHER_HTTPS_LOCAL = Server.get("https", "other-local");
  static HTTPS_PRIVATE = Server.get("https", "private");
  static HTTPS_PUBLIC = Server.get("https", "public");
  static WS_LOCAL = Server.get("ws", "local");
  static WSS_LOCAL = Server.get("wss", "local");
};

// Resolves a URL relative to the current location, returning an absolute URL.
//
// `url` specifies the relative URL, e.g. "foo.html" or "http://foo.example".
// `options`, if defined, should have the following shape:
//
//   {
//     // Optional. Overrides the protocol of the returned URL.
//     protocol,
//
//     // Optional. Overrides the port of the returned URL.
//     port,
//
//     // Extra headers.
//     headers,
//
//     // Extra search params.
//     searchParams,
//   }
//
function resolveUrl(url, options) {
  const result = new URL(url, window.location);
  if (options === undefined) {
    return result;
  }

  const { port, protocol, headers, searchParams } = options;
  if (port !== undefined) {
    result.port = port;
  }
  if (protocol !== undefined) {
    result.protocol = protocol;
  }
  if (headers !== undefined) {
    const pipes = [];
    for (key in headers) {
      pipes.push(`header(${key},${headers[key]})`);
    }
    result.searchParams.append("pipe", pipes.join("|"));
  }
  if (searchParams !== undefined) {
    for (key in searchParams) {
      result.searchParams.append(key, searchParams[key]);
    }
  }

  return result;
}

// Computes options to pass to `resolveUrl()` for a source document's URL.
//
// `server` identifies the server from which to load the document.
// `treatAsPublic`, if set to true, specifies that the source document should
// be artificially placed in the `public` address space using CSP.
function sourceResolveOptions({ server, treatAsPublic }) {
  const options = {...server};
  if (treatAsPublic) {
    options.headers = { "Content-Security-Policy": "treat-as-public-address" };
  }
  return options;
}

// Computes the URL of a preflight handler configured with the given options.
//
// `server` identifies the server from which to load the resource.
// `behavior` specifies the behavior of the target server. It may contain:
//   - `preflight`: The result of calling one of `PreflightBehavior`'s methods.
//   - `response`: The result of calling one of `ResponseBehavior`'s methods.
//   - `redirect`: A URL to which the target should redirect GET requests.
function preflightUrl({ server, behavior }) {
  assert_not_equals(server, undefined, 'server');
  const options = {...server};
  if (behavior) {
    const { preflight, response, redirect } = behavior;
    options.searchParams = {
      ...preflight,
      ...response,
    };
    if (redirect !== undefined) {
      options.searchParams.redirect = redirect;
    }
  }

  return resolveUrl("resources/preflight.py", options);
}

// Methods generate behavior specifications for how `resources/preflight.py`
// should behave upon receiving a preflight request.
const PreflightBehavior = {
  // The preflight response should fail with a non-2xx code.
  failure: () => ({}),

  // The preflight response should be missing CORS headers.
  // `uuid` should be a UUID that uniquely identifies the preflight request.
  noCorsHeader: (uuid) => ({
    "preflight-uuid": uuid,
  }),

  // The preflight response should be missing PNA headers.
  // `uuid` should be a UUID that uniquely identifies the preflight request.
  noPnaHeader: (uuid) => ({
    "preflight-uuid": uuid,
    "preflight-headers": "cors",
  }),

  // The preflight response should succeed.
  // `uuid` should be a UUID that uniquely identifies the preflight request.
  success: (uuid) => ({
    "preflight-uuid": uuid,
    "preflight-headers": "cors+pna",
  }),

  optionalSuccess: (uuid) => ({
    "preflight-uuid": uuid,
    "preflight-headers": "cors+pna",
    "is-preflight-optional": true,
  }),

  // The preflight response should succeed and allow service-worker header.
  // `uuid` should be a UUID that uniquely identifies the preflight request.
  serviceWorkerSuccess: (uuid) => ({
    "preflight-uuid": uuid,
    "preflight-headers": "cors+pna+sw",
  }),

  // The preflight response should succeed only if it is the first preflight.
  // `uuid` should be a UUID that uniquely identifies the preflight request.
  singlePreflight: (uuid) => ({
    "preflight-uuid": uuid,
    "preflight-headers": "cors+pna",
    "expect-single-preflight": true,
  }),

  // The preflight response should succeed and allow origins and headers for
  // navigations.
  navigation: (uuid) => ({
    "preflight-uuid": uuid,
    "preflight-headers": "navigation",
  }),
};

// Methods generate behavior specifications for how `resources/preflight.py`
// should behave upon receiving a regular (non-preflight) request.
const ResponseBehavior = {
  // The response should succeed without CORS headers.
  default: () => ({}),

  // The response should succeed with CORS headers.
  allowCrossOrigin: () => ({ "final-headers": "cors" }),
};

const FetchTestResult = {
  SUCCESS: {
    ok: true,
    body: "success",
  },
  OPAQUE: {
    ok: false,
    type: "opaque",
    body: "",
  },
  FAILURE: {
    error: "TypeError: Failed to fetch",
  },
};

// Runs a fetch test. Tries to fetch a given subresource from a given document.
//
// Main argument shape:
//
//   {
//     // Optional. Passed to `sourceResolveOptions()`.
//     source,
//
//     // Optional. Passed to `preflightUrl()`.
//     target,
//
//     // Optional. Passed to `fetch()`.
//     fetchOptions,
//
//     // Required. One of the values in `FetchTestResult`.
//     expected,
//   }
//
async function fetchTest(t, { source, target, fetchOptions, expected }) {
  const sourceUrl =
      resolveUrl("resources/fetcher.html", sourceResolveOptions(source));

  const targetUrl = preflightUrl(target);

  const iframe = await appendIframe(t, document, sourceUrl);
  const reply = futureMessage({ source: iframe.contentWindow });

  const message = {
    url: targetUrl.href,
    options: fetchOptions,
  };
  iframe.contentWindow.postMessage(message, "*");

  const { error, ok, type, body } = await reply;

  assert_equals(error, expected.error, "error");

  assert_equals(ok, expected.ok, "response ok");
  assert_equals(body, expected.body, "response body");

  if (expected.type !== undefined) {
    assert_equals(type, expected.type, "response type");
  }
}

// Similar to `fetchTest`, but replaced iframes with fenced frames.
async function fencedFrameFetchTest(t, { source, target, fetchOptions, expected }) {
  const fetcher_url =
      resolveUrl("resources/fenced-frame-fetcher.https.html", sourceResolveOptions(source));

  const target_url = preflightUrl(target);
  target_url.searchParams.set("is-loaded-in-fenced-frame", true);

  fetcher_url.searchParams.set("mode", fetchOptions.mode);
  fetcher_url.searchParams.set("method", fetchOptions.method);
  fetcher_url.searchParams.set("url", target_url);

  const error_token = token();
  const ok_token = token();
  const body_token = token();
  const type_token = token();
  const source_url = generateURL(fetcher_url, [error_token, ok_token, body_token, type_token]);

  const urn = await generateURNFromFledge(source_url, []);
  attachFencedFrame(urn);

  const error = await nextValueFromServer(error_token);
  const ok = await nextValueFromServer(ok_token);
  const body = await nextValueFromServer(body_token);
  const type = await nextValueFromServer(type_token);

  assert_equals(error, expected.error || "" , "error");
  assert_equals(body, expected.body || "", "response body");
  assert_equals(ok, expected.ok !== undefined ? expected.ok.toString() : "", "response ok");
  if (expected.type !== undefined) {
    assert_equals(type, expected.type, "response type");
  }
}

const XhrTestResult = {
  SUCCESS: {
    loaded: true,
    status: 200,
    body: "success",
  },
  FAILURE: {
    loaded: false,
    status: 0,
  },
};

// Runs an XHR test. Tries to fetch a given subresource from a given document.
//
// Main argument shape:
//
//   {
//     // Optional. Passed to `sourceResolveOptions()`.
//     source,
//
//     // Optional. Passed to `preflightUrl()`.
//     target,
//
//     // Optional. Method to use when sending the request. Defaults to "GET".
//     method,
//
//     // Required. One of the values in `XhrTestResult`.
//     expected,
//   }
//
async function xhrTest(t, { source, target, method, expected }) {
  const sourceUrl =
      resolveUrl("resources/xhr-sender.html", sourceResolveOptions(source));

  const targetUrl = preflightUrl(target);

  const iframe = await appendIframe(t, document, sourceUrl);
  const reply = futureMessage();

  const message = {
    url: targetUrl.href,
    method: method,
  };
  iframe.contentWindow.postMessage(message, "*");

  const { loaded, status, body } = await reply;

  assert_equals(loaded, expected.loaded, "response loaded");
  assert_equals(status, expected.status, "response status");
  assert_equals(body, expected.body, "response body");
}

const FrameTestResult = {
  SUCCESS: "loaded",
  FAILURE: "timeout",
};

async function iframeTest(t, { source, target, expected }) {
  // Allows running tests in parallel.
  const uuid = token();

  const targetUrl = preflightUrl(target);
  targetUrl.searchParams.set("file", "iframed.html");
  targetUrl.searchParams.set("iframe-uuid", uuid);
  targetUrl.searchParams.set(
    "file-if-no-preflight-received",
    "iframed-no-preflight-received.html",
  );

  const sourceUrl =
      resolveUrl("resources/iframer.html", sourceResolveOptions(source));
  sourceUrl.searchParams.set("url", targetUrl);

  const messagePromise = futureMessage({
    filter: (data) => data.uuid === uuid,
  });
  const iframe = await appendIframe(t, document, sourceUrl);

  // The grandchild frame posts a message iff it loads successfully.
  // There exists no interoperable way to check whether an iframe failed to
  // load, so we use a timeout.
  // See: https://github.com/whatwg/html/issues/125
  const result = await Promise.race([
      messagePromise.then((data) => data.message),
      new Promise((resolve) => {
        t.step_timeout(() => resolve("timeout"), 2000 /* ms */);
      }),
  ]);

  assert_equals(result, expected);
}

const NavigationTestResult = {
  SUCCESS: "success",
  FAILURE: "timeout",
};

async function windowOpenTest(t, { source, target, expected }) {
  const targetUrl = preflightUrl(target);
  targetUrl.searchParams.set("file", "openee.html");
  targetUrl.searchParams.set(
    "file-if-no-preflight-received",
    "no-preflight-received.html",
  );

  const sourceUrl =
      resolveUrl("resources/opener.html", sourceResolveOptions(source));
  sourceUrl.searchParams.set("url", targetUrl);

  const iframe = await appendIframe(t, document, sourceUrl);
  const reply = futureMessage({ source: iframe.contentWindow });

  iframe.contentWindow.postMessage({ url: targetUrl.href }, "*");

  const result = await Promise.race([
      reply,
      new Promise((resolve) => {
        t.step_timeout(() => resolve("timeout"), 10000 /* ms */);
      }),
  ]);

  assert_equals(result, expected);
}

async function windowOpenExistingTest(t, { source, target, expected }) {
  const targetUrl = preflightUrl(target);
  targetUrl.searchParams.set("file", "openee.html");
  targetUrl.searchParams.set(
    "file-if-no-preflight-received",
    "no-preflight-received.html",
  );

  const sourceUrl = resolveUrl(
      'resources/open-to-existing-window.html', sourceResolveOptions(source));
  sourceUrl.searchParams.set("url", targetUrl);
  sourceUrl.searchParams.set("token", token());

  const iframe = await appendIframe(t, document, sourceUrl);
  const reply = futureMessage({ source: iframe.contentWindow });

  iframe.contentWindow.postMessage({ url: targetUrl.href }, "*");

  const result = await Promise.race([
      reply,
      new Promise((resolve) => {
        t.step_timeout(() => resolve("timeout"), 10000 /* ms */);
      }),
  ]);

  assert_equals(result, expected);
}

async function anchorTest(t, { source, target, expected }) {
  const targetUrl = preflightUrl(target);
  targetUrl.searchParams.set("file", "openee.html");
  targetUrl.searchParams.set(
    "file-if-no-preflight-received",
    "no-preflight-received.html",
  );

  const sourceUrl =
      resolveUrl("resources/anchor.html", sourceResolveOptions(source));
  sourceUrl.searchParams.set("url", targetUrl);

  const iframe = await appendIframe(t, document, sourceUrl);
  const reply = futureMessage({ source: iframe.contentWindow });

  iframe.contentWindow.postMessage({ url: targetUrl.href }, "*");

  const result = await Promise.race([
      reply,
      new Promise((resolve) => {
        t.step_timeout(() => resolve("timeout"), 10000 /* ms */);
      }),
  ]);

  assert_equals(result, expected);
}

// Similar to `iframeTest`, but replaced iframes with fenced frames.
async function fencedFrameTest(t, { source, target, expected }) {
  // Allows running tests in parallel.
  const target_url = preflightUrl(target);
  target_url.searchParams.set("file", "fenced-frame-private-network-access-target.https.html");
  target_url.searchParams.set("is-loaded-in-fenced-frame", true);

  const frame_loaded_key = token();
  const child_frame_target = generateURL(target_url, [frame_loaded_key]);

  const source_url =
      resolveUrl("resources/fenced-frame-private-network-access.https.html", sourceResolveOptions(source));
  source_url.searchParams.set("fenced_frame_url", child_frame_target);

  const urn = await generateURNFromFledge(source_url, []);
  attachFencedFrame(urn);

  // The grandchild fenced frame writes a value to the server iff it loads
  // successfully.
  const result = (expected == FrameTestResult.SUCCESS) ?
    await nextValueFromServer(frame_loaded_key) :
    await Promise.race([
      nextValueFromServer(frame_loaded_key),
      new Promise((resolve) => {
        t.step_timeout(() => resolve("timeout"), 10000 /* ms */);
      }),
    ]);

  assert_equals(result, expected);
}

const iframeGrandparentTest = ({
  name,
  grandparentServer,
  child,
  grandchild,
  expected,
}) => promise_test_parallel(async (t) => {
  // Allows running tests in parallel.
  const grandparentUuid = token();
  const childUuid = token();
  const grandchildUuid = token();

  const grandparentUrl =
      resolveUrl("resources/executor.html", grandparentServer);
  grandparentUrl.searchParams.set("executor-uuid", grandparentUuid);

  const childUrl = preflightUrl(child);
  childUrl.searchParams.set("file", "executor.html");
  childUrl.searchParams.set("executor-uuid", childUuid);

  const grandchildUrl = preflightUrl(grandchild);
  grandchildUrl.searchParams.set("file", "iframed.html");
  grandchildUrl.searchParams.set("iframe-uuid", grandchildUuid);

  const iframe = await appendIframe(t, document, grandparentUrl);

  const addChild = (url) => new Promise((resolve) => {
    const child = document.createElement("iframe");
    child.src = url;
    child.addEventListener("load", () => resolve(), { once: true });
    document.body.appendChild(child);
  });

  const grandparentCtx = new RemoteContext(grandparentUuid);
  await grandparentCtx.execute_script(addChild, [childUrl]);

  // Add a blank grandchild frame inside the child.
  // Apply a timeout to this step so that failures at this step do not block the
  // execution of other tests.
  const childCtx = new RemoteContext(childUuid);
  await Promise.race([
      childCtx.execute_script(addChild, ["about:blank"]),
      new Promise((resolve, reject) => t.step_timeout(
          () => reject("timeout adding grandchild"),
          2000 /* ms */
      )),
  ]);

  const messagePromise = futureMessage({
    filter: (data) => data.uuid === grandchildUuid,
  });
  await grandparentCtx.execute_script((url) => {
    const child = window.frames[0];
    const grandchild = child.frames[0];
    grandchild.location = url;
  }, [grandchildUrl]);

  // The great-grandchild frame posts a message iff it loads successfully.
  // There exists no interoperable way to check whether an iframe failed to
  // load, so we use a timeout.
  // See: https://github.com/whatwg/html/issues/125
  const result = await Promise.race([
      messagePromise.then((data) => data.message),
      new Promise((resolve) => {
        t.step_timeout(() => resolve("timeout"), 2000 /* ms */);
      }),
  ]);

  assert_equals(result, expected);
}, name);

const WebsocketTestResult = {
  SUCCESS: "open",

  // The code is a best guess. It is not yet entirely specified, so it may need
  // to be changed in the future based on implementation experience.
  FAILURE: "close: code 1006",
};

// Runs a websocket test. Attempts to open a websocket from `source` (in an
// iframe) to `target`, then checks that the result is as `expected`.
//
// Argument shape:
//
// {
//   // Required. Passed to `sourceResolveOptions()`.
//   source,
//
//   // Required.
//   target: {
//     // Required. Target server.
//     server,
//   }
//
//   // Required. Should be one of the values in `WebsocketTestResult`.
//   expected,
// }
//
async function websocketTest(t, { source, target, expected }) {
  const sourceUrl =
      resolveUrl("resources/socket-opener.html", sourceResolveOptions(source));

  const targetUrl = resolveUrl("/echo", target.server);

  const iframe = await appendIframe(t, document, sourceUrl);

  const reply = futureMessage();
  iframe.contentWindow.postMessage(targetUrl.href, "*");

  assert_equals(await reply, expected);
}

const WorkerScriptTestResult = {
  SUCCESS: { loaded: true },
  FAILURE: { error: "unknown error" },
};

function workerScriptUrl(target) {
  const url = preflightUrl(target);

  url.searchParams.append("body", "postMessage({ loaded: true })")
  url.searchParams.append("mime-type", "application/javascript")

  return url;
}

async function workerScriptTest(t, { source, target, expected }) {
  const sourceUrl =
      resolveUrl("resources/worker-fetcher.html", sourceResolveOptions(source));

  const targetUrl = workerScriptUrl(target);

  const iframe = await appendIframe(t, document, sourceUrl);
  const reply = futureMessage();

  iframe.contentWindow.postMessage({ url: targetUrl.href }, "*");

  const { error, loaded } = await reply;

  assert_equals(error, expected.error, "worker error");
  assert_equals(loaded, expected.loaded, "response loaded");
}

async function nestedWorkerScriptTest(t, { source, target, expected }) {
  const targetUrl = workerScriptUrl(target);

  const sourceUrl = resolveUrl(
      "resources/worker-fetcher.js", sourceResolveOptions(source));
  sourceUrl.searchParams.append("url", targetUrl);

  // Iframe must be same-origin with the parent worker.
  const iframeUrl = new URL("worker-fetcher.html", sourceUrl);

  const iframe = await appendIframe(t, document, iframeUrl);
  const reply = futureMessage();

  iframe.contentWindow.postMessage({ url: sourceUrl.href }, "*");

  const { error, loaded } = await reply;

  assert_equals(error, expected.error, "worker error");
  assert_equals(loaded, expected.loaded, "response loaded");
}

async function sharedWorkerScriptTest(t, { source, target, expected }) {
  const sourceUrl = resolveUrl("resources/shared-worker-fetcher.html",
                               sourceResolveOptions(source));
  const targetUrl = preflightUrl(target);
  targetUrl.searchParams.append(
      "body", "onconnect = (e) => e.ports[0].postMessage({ loaded: true })")
  targetUrl.searchParams.append("mime-type", "application/javascript")

  const iframe = await appendIframe(t, document, sourceUrl);
  const reply = futureMessage();

  iframe.contentWindow.postMessage({ url: targetUrl.href }, "*");

  const { error, loaded } = await reply;

  assert_equals(error, expected.error, "worker error");
  assert_equals(loaded, expected.loaded, "response loaded");
}

// Results that may be expected in tests.
const WorkerFetchTestResult = {
  SUCCESS: { status: 200, body: "success" },
  FAILURE: { error: "TypeError" },
};

async function workerFetchTest(t, { source, target, expected }) {
  const targetUrl = preflightUrl(target);

  const sourceUrl =
      resolveUrl("resources/fetcher.js", sourceResolveOptions(source));
  sourceUrl.searchParams.append("url", targetUrl.href);

  const fetcherUrl = new URL("worker-fetcher.html", sourceUrl);

  const reply = futureMessage();
  const iframe = await appendIframe(t, document, fetcherUrl);

  iframe.contentWindow.postMessage({ url: sourceUrl.href }, "*");

  const { error, status, body } = await reply;
  assert_equals(error, expected.error, "fetch error");
  assert_equals(status, expected.status, "response status");
  assert_equals(body, expected.body, "response body");
}

async function workerBlobFetchTest(t, { source, target, expected }) {
  const targetUrl = preflightUrl(target);

  const fetcherUrl = resolveUrl(
      'resources/worker-blob-fetcher.html', sourceResolveOptions(source));

  const reply = futureMessage();
  const iframe = await appendIframe(t, document, fetcherUrl);

  iframe.contentWindow.postMessage({ url: targetUrl.href }, "*");

  const { error, status, body } = await reply;
  assert_equals(error, expected.error, "fetch error");
  assert_equals(status, expected.status, "response status");
  assert_equals(body, expected.body, "response body");
}

async function sharedWorkerFetchTest(t, { source, target, expected }) {
  const targetUrl = preflightUrl(target);

  const sourceUrl =
      resolveUrl("resources/shared-fetcher.js", sourceResolveOptions(source));
  sourceUrl.searchParams.append("url", targetUrl.href);

  const fetcherUrl = new URL("shared-worker-fetcher.html", sourceUrl);

  const reply = futureMessage();
  const iframe = await appendIframe(t, document, fetcherUrl);

  iframe.contentWindow.postMessage({ url: sourceUrl.href }, "*");

  const { error, status, body } = await reply;
  assert_equals(error, expected.error, "fetch error");
  assert_equals(status, expected.status, "response status");
  assert_equals(body, expected.body, "response body");
}

async function sharedWorkerBlobFetchTest(t, { source, target, expected }) {
  const targetUrl = preflightUrl(target);

  const fetcherUrl = resolveUrl(
      'resources/shared-worker-blob-fetcher.html',
      sourceResolveOptions(source));

  const reply = futureMessage();
  const iframe = await appendIframe(t, document, fetcherUrl);

  iframe.contentWindow.postMessage({ url: targetUrl.href }, "*");

  const { error, status, body } = await reply;
  assert_equals(error, expected.error, "fetch error");
  assert_equals(status, expected.status, "response status");
  assert_equals(body, expected.body, "response body");
}

async function makeServiceWorkerTest(t, { source, target, expected, fetch_document=false }) {
  const bridgeUrl = resolveUrl(
      "resources/service-worker-bridge.html",
      sourceResolveOptions({ server: source.server }));

  const scriptUrl = fetch_document?
      resolveUrl("resources/service-worker-fetch-all.js", sourceResolveOptions(source)):
      resolveUrl("resources/service-worker.js", sourceResolveOptions(source));

  const realTargetUrl = preflightUrl(target);

  // Fetch a URL within the service worker's scope, but tell it which URL to
  // really fetch.
  const targetUrl = new URL("service-worker-proxy", scriptUrl);
  targetUrl.searchParams.append("proxied-url", realTargetUrl.href);

  const iframe = await appendIframe(t, document, bridgeUrl);

  const request = (message) => {
    const reply = futureMessage();
    iframe.contentWindow.postMessage(message, "*");
    return reply;
  };

  {
    const { error, loaded } = await request({
      action: "register",
      url: scriptUrl.href,
    });

    assert_equals(error, undefined, "register error");
    assert_true(loaded, "response loaded");
  }

  try {
    const { controlled, numControllerChanges } = await request({
      action: "wait",
      numControllerChanges: 1,
    });

    assert_equals(numControllerChanges, 1, "controller change");
    assert_true(controlled, "bridge script is controlled");

    const { error, ok, body } = await request({
      action: "fetch",
      url: targetUrl.href,
    });

    assert_equals(error, expected.error, "fetch error");
    assert_equals(ok, expected.ok, "response ok");
    assert_equals(body, expected.body, "response body");
  } finally {
    // Always unregister the service worker.
    const { error, unregistered } = await request({
      action: "unregister",
      scope: new URL("./", scriptUrl).href,
    });

    assert_equals(error, undefined, "unregister error");
    assert_true(unregistered, "unregistered");
  }
}
