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

"use strict";

let bounceTrackingProtection;

add_setup(async function () {
  await SpecialPowers.pushPrefEnv({
    set: [
      [
        "privacy.bounceTrackingProtection.mode",
        Ci.nsIBounceTrackingProtection.MODE_ENABLED,
      ],
      ["privacy.bounceTrackingProtection.requireStatefulBounces", true],
      ["privacy.bounceTrackingProtection.bounceTrackingGracePeriodSec", 0],
    ],
  });
  bounceTrackingProtection = Cc[
    "@mozilla.org/bounce-tracking-protection;1"
  ].getService(Ci.nsIBounceTrackingProtection);
});

async function runTest(spawnWindowType) {
  if (!spawnWindowType || !["newTab", "popup"].includes(spawnWindowType)) {
    throw new Error(`Invalid option '${spawnWindowType}' for spawnWindowType`);
  }

  Assert.equal(
    bounceTrackingProtection.testGetBounceTrackerCandidateHosts({}).length,
    0,
    "No bounce tracker hosts initially."
  );
  Assert.equal(
    bounceTrackingProtection.testGetUserActivationHosts({}).length,
    0,
    "No user activation hosts initially."
  );

  // Spawn a tab with A, the start of the bounce chain.
  await BrowserTestUtils.withNewTab(
    getBaseUrl(ORIGIN_A) + "file_start.html",
    async browser => {
      // The destination site C to navigate to after the bounce.
      let finalURL = new URL(getBaseUrl(ORIGIN_B) + "file_start.html");
      // The middle hop in the bounce chain B that redirects to finalURL C.
      let bounceURL = getBounceURL({
        bounceType: "client",
        targetURL: finalURL,
        setState: "cookie-client",
      });

      // Register a promise for the new popup window. This resolves once the popup
      // has opened and the final url (C) has been loaded.
      let openPromise;

      if (spawnWindowType == "newTab") {
        openPromise = BrowserTestUtils.waitForNewTab(
          gBrowser,
          finalURL.href,
          true
        );
      } else {
        openPromise = BrowserTestUtils.waitForNewWindow({ url: finalURL.href });
      }

      // Navigate through the bounce chain by opening a popup to the bounce URL.
      await navigateLinkClick(browser, bounceURL, {
        spawnWindow: spawnWindowType,
      });

      let tabOrWindow = await openPromise;

      let tabOrWindowBrowser;
      if (spawnWindowType == "newTab") {
        tabOrWindowBrowser = tabOrWindow.linkedBrowser;
      } else {
        tabOrWindowBrowser = tabOrWindow.gBrowser.selectedBrowser;
      }

      let promiseRecordBounces = waitForRecordBounces(tabOrWindowBrowser);

      // Navigate again with user gesture which triggers
      // BounceTrackingProtection::RecordStatefulBounces. We could rely on the
      // timeout (mClientBounceDetectionTimeout) here but that can cause races
      // in debug where the load is quite slow.
      await navigateLinkClick(
        tabOrWindowBrowser,
        new URL(getBaseUrl(ORIGIN_C) + "file_start.html")
      );

      info("Wait for bounce trackers to be recorded.");
      await promiseRecordBounces;

      // Cleanup popup or tab.
      if (spawnWindowType == "newTab") {
        await BrowserTestUtils.removeTab(tabOrWindow);
      } else {
        await BrowserTestUtils.closeWindow(tabOrWindow);
      }
    }
  );

  // Check that the bounce tracker was detected.
  Assert.deepEqual(
    bounceTrackingProtection
      .testGetBounceTrackerCandidateHosts({})
      .map(entry => entry.siteHost),
    [SITE_TRACKER],
    "Bounce tracker in popup detected."
  );

  // Cleanup.
  bounceTrackingProtection.clearAll();
  await SiteDataTestUtils.clear();
}

/**
 * Tests that bounce trackers which use popups as the first hop in the bounce
 * chain can not bypass detection.
 *
 * A -> popup -> B -> C
 *
 * A opens a popup and loads B in it. B is the tracker that performs a
 * short-lived redirect and C is the final destination.
 */

add_task(async function test_popup() {
  await runTest("popup");
});

add_task(async function test_new_tab() {
  await runTest("newTab");
});
