function isIsolated(key) {
  return key.charAt(7) == "i";
}

function hasTopWindowOrigin(key, origin) {
  let tokenAtEnd = `{{${origin}}}`;
  let endPart = key.slice(-tokenAtEnd.length);
  return endPart == tokenAtEnd;
}

function hasAnyTopWindowOrigin(key) {
  return !!key.match(/{{[^}]+}}/);
}

function altSvcCacheKeyIsolated(parsed) {
  return parsed.length > 5 && parsed[5] == "I";
}

function altSvcTopWindowOrigin(key) {
  let index = -1;
  for (let i = 0; i < 6; ++i) {
    index = key.indexOf(":", index + 1);
  }
  let indexEnd = key.indexOf("|", index + 1);
  return key.substring(index + 1, indexEnd);
}

const gHttpHandler = Cc["@mozilla.org/network/protocol;1?name=http"].getService(
  Ci.nsIHttpProtocolHandler
);

add_task(async function() {
  info("Starting tlsSessionTickets test");

  await SpecialPowers.flushPrefEnv();
  await SpecialPowers.pushPrefEnv({
    set: [
      ["browser.cache.disk.enable", false],
      ["browser.cache.memory.enable", false],
      [
        "network.cookie.cookieBehavior",
        Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER,
      ],
      ["network.http.altsvc.proxy_checks", false],
      ["privacy.trackingprotection.enabled", false],
      ["privacy.trackingprotection.pbmode.enabled", false],
      ["privacy.trackingprotection.annotate_channels", true],
      ["privacy.partition.network_state", false],
    ],
  });

  await UrlClassifierTestUtils.addTestTrackers();

  info("Creating a new tab");
  let tab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE);
  gBrowser.selectedTab = tab;

  let browser = gBrowser.getBrowserForTab(tab);
  await BrowserTestUtils.browserLoaded(browser);

  const trackingURL =
    "https://tlsresumptiontest.example.org/browser/toolkit/components/antitracking/test/browser/empty-altsvc.js";
  const topWindowOrigin = TEST_DOMAIN.replace(/\/$/, "");
  const kTopic = "http-on-examine-response";

  let resumedState = [];
  let hashKeys = [];

  function observer(subject, topic, data) {
    if (topic != kTopic) {
      return;
    }

    subject.QueryInterface(Ci.nsIChannel);
    if (subject.URI.spec != trackingURL) {
      return;
    }

    resumedState.push(
      subject.securityInfo.QueryInterface(Ci.nsITransportSecurityInfo).resumed
    );
    hashKeys.push(
      subject.QueryInterface(Ci.nsIHttpChannelInternal).connectionInfoHashKey
    );
  }

  Services.obs.addObserver(observer, kTopic);
  registerCleanupFunction(() => Services.obs.removeObserver(observer, kTopic));

  function checkAltSvcCache(expectedCount, expectedIsolated) {
    let arr = gHttpHandler.altSvcCacheKeys;
    is(
      arr.length,
      expectedCount,
      "Found the expected number of items in the cache"
    );
    for (let i = 0; i < arr.length; ++i) {
      let key = arr[i];
      let parsed = key.split(":");
      if (altSvcCacheKeyIsolated(parsed)) {
        ok(expectedIsolated[i], "We expected to find an isolated item");
        is(
          altSvcTopWindowOrigin(key),
          topWindowOrigin,
          "Expected top window origin found in the Alt-Svc cache key"
        );
      } else {
        ok(!expectedIsolated[i], "We expected to find a non-isolated item");
      }
    }
  }

  checkAltSvcCache(0, []);

  info("Loading tracking scripts and tracking images");
  await SpecialPowers.spawn(browser, [{ trackingURL }], async function(obj) {
    let src = content.document.createElement("script");
    let p = new content.Promise(resolve => {
      src.onload = resolve;
    });
    content.document.body.appendChild(src);
    src.src = obj.trackingURL;
    await p;
  });

  checkAltSvcCache(1, [true]);

  // Load our tracking URL two more times, but this time in the first-party context.
  // The TLS session should be resumed the second time here.
  await fetch(trackingURL);
  // Wait a little bit before issuing the second load to ensure both don't happen
  // at the same time.
  // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
  await new Promise(resolve => setTimeout(resolve, 10));

  checkAltSvcCache(2, [false, true]);
  await fetch(trackingURL).then(() => {
    checkAltSvcCache(2, [false, true]);

    is(
      resumedState.length,
      3,
      "We should have observed 3 loads for " + trackingURL
    );
    ok(!resumedState[0], "The first load should NOT have been resumed");
    ok(!resumedState[1], "The second load should NOT have been resumed");
    ok(resumedState[2], "The third load SHOULD have been resumed");

    // We also verify that the hashKey of the first connection is different to
    // both the second and third connections, and that the hashKey of the
    // second and third connections are the same.  The reason why this check is
    // done is that the private bit on the connection info object is used to
    // construct the hash key, so when the connection is isolated because it
    // comes from a third-party tracker context, its hash key must be
    // different.
    is(
      hashKeys.length,
      3,
      "We should have observed 3 loads for " + trackingURL
    );
    is(hashKeys[1], hashKeys[2], "The second and third hash keys should match");
    isnot(
      hashKeys[0],
      hashKeys[1],
      "The first and second hash keys should not match"
    );

    ok(isIsolated(hashKeys[0]), "The first connection must have been isolated");
    ok(
      !isIsolated(hashKeys[1]),
      "The second connection must not have been isolated"
    );
    ok(
      !isIsolated(hashKeys[2]),
      "The third connection must not have been isolated"
    );
    ok(
      hasTopWindowOrigin(hashKeys[0], topWindowOrigin),
      "The first connection must be bound to its top-level window"
    );
    ok(
      !hasAnyTopWindowOrigin(hashKeys[1]),
      "The second connection must not be bound to a top-level window"
    );
    ok(
      !hasAnyTopWindowOrigin(hashKeys[2]),
      "The third connection must not be bound to a top-level window"
    );
  });

  info("Removing the tab");
  BrowserTestUtils.removeTab(tab);

  UrlClassifierTestUtils.cleanupTestTrackers();
});

add_task(async function() {
  info("Cleaning up.");
  await new Promise(resolve => {
    Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
      resolve()
    );
  });
});
