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

"use strict";

const FIRST_PARTY_A = "http://example.com";
const FIRST_PARTY_B = "http://example.org";
const THIRD_PARTY = "http://example.net";
const WS_ENDPOINT_HOST = "mochi.test:8888";

function getWSTestUrlForHost(host) {
  return (
    getRootDirectory(gTestPath).replace(
      "chrome://mochitests/content",
      `ws://${host}`
    ) + `file_ws_handshake_delay`
  );
}

function connect(browsingContext, host, protocol) {
  let url = getWSTestUrlForHost(host);
  info("Creating websocket with endpoint " + url);

  // Create websocket connection in third party iframe.
  let createPromise = SpecialPowers.spawn(
    browsingContext.children[0],
    [url, protocol],
    (url, protocol) => {
      let ws = new content.WebSocket(url, [protocol]);
      ws.addEventListener("error", () => {
        ws._testError = true;
      });
      if (!content.ws) {
        content.ws = {};
      }
      content.ws[protocol] = ws;
    }
  );

  let openPromise = createPromise.then(() =>
    SpecialPowers.spawn(
      browsingContext.children[0],
      [protocol],
      async protocol => {
        let ws = content.ws[protocol];
        if (ws.readyState != 0) {
          return !ws._testError;
        }
        // Still connecting.
        let result = await Promise.race([
          ContentTaskUtils.waitForEvent(ws, "open"),
          ContentTaskUtils.waitForEvent(ws, "error"),
        ]);
        return result.type != "error";
      }
    )
  );

  let result = { createPromise, openPromise };
  return result;
}

// Open 3 websockets which target the same ip/port combination, but have
// different principals. We send a protocol identifier to the server to signal
// how long the request should be delayed.
//
// When partitioning is disabled A blocks B and B blocks C. The timeline will
// look like this:
// A________
//          B____
//               C_
//
// When partitioning is enabled, connection handshakes for A and B will run
// (semi-) parallel since they have different origin attributes. B and C share
// origin attributes and therefore still run serially.
// A________
// B____
//      C_
//
// By observing the order of the handshakes we can ensure that the queue
// partitioning is working correctly.
async function runTest() {
  let tabA = BrowserTestUtils.addTab(gBrowser, FIRST_PARTY_A);
  await BrowserTestUtils.browserLoaded(tabA.linkedBrowser);
  let tabB = BrowserTestUtils.addTab(gBrowser, FIRST_PARTY_B);
  await BrowserTestUtils.browserLoaded(tabB.linkedBrowser);

  for (let tab of [tabA, tabB]) {
    await SpecialPowers.spawn(tab.linkedBrowser, [THIRD_PARTY], async src => {
      let frame = content.document.createElement("iframe");
      frame.src = src;
      let loadPromise = ContentTaskUtils.waitForEvent(frame, "load");
      content.document.body.appendChild(frame);
      await loadPromise;
    });
  }

  // First ensure that we can open websocket connections to the test endpoint.
  let { openPromise, createPromise } = await connect(
    tabA.linkedBrowser.browsingContext,
    WS_ENDPOINT_HOST,
    false
  );
  await createPromise;
  let openPromiseResult = await openPromise;
  ok(openPromiseResult, "Websocket endpoint accepts connections.");

  let openedA;
  let openedB;
  let openedC;

  let { createPromise: createPromiseA, openPromise: openPromiseA } = connect(
    tabA.linkedBrowser.browsingContext,
    WS_ENDPOINT_HOST,
    "test-6"
  );

  openPromiseA = openPromiseA.then(opened => {
    openedA = opened;
    info("Completed WS connection A");
    ok(openedA, "Should have opened A");
    ok(openedB, "Should have opened B");
  });
  await createPromiseA;

  // The frame of connection B is embedded in a different first party as A.
  let { createPromise: createPromiseB, openPromise: openPromiseB } = connect(
    tabB.linkedBrowser.browsingContext,
    WS_ENDPOINT_HOST,
    "test-3"
  );
  openPromiseB = openPromiseB.then(opened => {
    openedB = opened;
    info("Completed WS connection B");
    ok(openedA == null, "A should be pending");
    ok(openedB, "Should have opened B");
    ok(openedC == null, "C should be pending");
  });
  await createPromiseB;

  // The frame of connection C is embedded in the same first party as B.
  let { createPromise: createPromiseC, openPromise: openPromiseC } = connect(
    tabB.linkedBrowser.browsingContext,
    WS_ENDPOINT_HOST,
    "test-0"
  );
  openPromiseC = openPromiseC.then(opened => {
    openedC = opened;
    info("Completed WS connection C");
    ok(openedB, "Should have opened B");
    ok(openedC, "Should have opened C");
  });
  await createPromiseC;

  // Wait for all connections to complete before closing the tabs.
  await Promise.all([openPromiseA, openPromiseB, openPromiseC]);

  BrowserTestUtils.removeTab(tabA);
  BrowserTestUtils.removeTab(tabB);

  await SpecialPowers.popPrefEnv();
}

add_setup(async function () {
  // This test relies on a WS connection timeout > 6 seconds.
  await SpecialPowers.pushPrefEnv({
    set: [["network.websocket.timeout.open", 20]],
  });
});

add_task(async function test_partitioned() {
  await runTest();
});
