function waitStoragePermission() {
  return new Promise(resolve => {
    let id = setInterval(async _ => {
      if (
        await SpecialPowers.testPermission(
          `3rdPartyStorage^${TEST_3RD_PARTY_DOMAIN.slice(0, -1)}`,
          SpecialPowers.Services.perms.ALLOW_ACTION,
          TEST_DOMAIN
        )
      ) {
        clearInterval(id);
        resolve();
      }
    }, 0);
  });
}

add_setup(async function () {
  info("Starting subResources test");

  await SpecialPowers.flushPrefEnv();
  await SpecialPowers.pushPrefEnv({
    set: [
      ["dom.storage_access.enabled", true],
      [
        "network.cookie.cookieBehavior",
        Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN,
      ],
      [
        "network.cookie.cookieBehavior.pbmode",
        Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN,
      ],
      ["privacy.trackingprotection.enabled", false],
      ["privacy.trackingprotection.pbmode.enabled", false],
      ["privacy.trackingprotection.annotate_channels", true],
      ["privacy.restrict3rdpartystorage.heuristic.window_open", true],
      [
        "privacy.restrict3rdpartystorage.userInteractionRequiredForHosts",
        "tracking.example.com,tracking.example.org",
      ],
      // Enable SA heuristics for trackers because the test depends on it.
      [
        "privacy.restrict3rdpartystorage.heuristic.exclude_third_party_trackers",
        false,
      ],
    ],
  });

  await UrlClassifierTestUtils.addTestTrackers();
});

async function runTestWindowOpenHeuristic(disableHeuristics) {
  info(
    `Starting window.open() heuristic test with heuristic ${
      disableHeuristics ? "disabled" : "enabled"
    }.`
  );

  await SpecialPowers.pushPrefEnv({
    set: [["privacy.antitracking.enableWebcompat", !disableHeuristics]],
  });

  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);

  info("Loading tracking scripts");
  await SpecialPowers.spawn(
    browser,
    [
      {
        page: TEST_3RD_PARTY_PAGE_WO,
        disableHeuristics,
      },
    ],
    async obj => {
      let msg = {};
      msg.blockingCallback = (async _ => {
        /* import-globals-from storageAccessAPIHelpers.js */
        await noStorageAccessInitially();
      }).toString();

      // If the heuristic is disabled, we won't get storage access.
      if (obj.disableHeuristics) {
        msg.nonBlockingCallback = (async _ => {
          /* import-globals-from storageAccessAPIHelpers.js */
          await stillNoStorageAccess();
        }).toString();
      } else {
        msg.nonBlockingCallback = (async _ => {
          /* import-globals-from storageAccessAPIHelpers.js */
          await hasStorageAccessInitially();
        }).toString();
      }

      info("Checking if storage access is denied");
      await new content.Promise(resolve => {
        let ifr = content.document.createElement("iframe");
        ifr.onload = function () {
          info("Sending code to the 3rd party content");
          ifr.contentWindow.postMessage(msg, "*");
        };

        content.addEventListener("message", function msg(event) {
          if (event.data.type == "finish") {
            content.removeEventListener("message", msg);
            resolve();
            return;
          }

          if (event.data.type == "ok") {
            ok(event.data.what, event.data.msg);
            return;
          }

          if (event.data.type == "info") {
            info(event.data.msg);
            return;
          }

          ok(false, "Unknown message");
        });

        content.document.body.appendChild(ifr);
        ifr.src = obj.page;
      });
    }
  );

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

  await SpecialPowers.popPrefEnv();

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

add_task(async function testWindowOpenHeuristic() {
  await runTestWindowOpenHeuristic(false);
});

add_task(async function testWindowOpenHeuristicDisabled() {
  await runTestWindowOpenHeuristic(true);
});

add_task(async function testDoublyNestedWindowOpenHeuristic() {
  info("Starting doubly nested window.open() heuristic test...");

  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);

  info("Loading tracking scripts");
  await SpecialPowers.spawn(
    browser,
    [
      {
        page: TEST_3RD_PARTY_PAGE_RELAY + "?" + TEST_3RD_PARTY_PAGE_WO,
      },
    ],
    async obj => {
      let msg = {};
      msg.blockingCallback = (async _ => {
        /* import-globals-from storageAccessAPIHelpers.js */
        await noStorageAccessInitially();
      }).toString();

      msg.nonBlockingCallback = (async _ => {
        /* import-globals-from storageAccessAPIHelpers.js */
        await hasStorageAccessInitially();
      }).toString();

      info("Checking if storage access is denied");
      await new content.Promise(resolve => {
        let ifr = content.document.createElement("iframe");
        ifr.onload = function () {
          info("Sending code to the 3rd party content");
          ifr.contentWindow.postMessage(msg, "*");
        };

        content.addEventListener("message", function msg(event) {
          if (event.data.type == "finish") {
            content.removeEventListener("message", msg);
            resolve();
            return;
          }

          if (event.data.type == "ok") {
            ok(event.data.what, event.data.msg);
            return;
          }

          if (event.data.type == "info") {
            info(event.data.msg);
            return;
          }

          ok(false, "Unknown message");
        });

        content.document.body.appendChild(ifr);
        ifr.src = obj.page;
      });
    }
  );

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

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

async function runTestUserInteractionHeuristic(disableHeuristics) {
  info(
    `Starting user interaction heuristic test with heuristic ${
      disableHeuristics ? "disabled" : "enabled"
    }.`
  );
  await SpecialPowers.pushPrefEnv({
    set: [["privacy.antitracking.enableWebcompat", !disableHeuristics]],
  });

  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);

  info("Loading tracking scripts");
  await SpecialPowers.spawn(
    browser,
    [
      {
        page: TEST_3RD_PARTY_PAGE_UI,
        popup: TEST_POPUP_PAGE,
      },
    ],
    async obj => {
      let msg = {};
      msg.blockingCallback = (async _ => {
        /* import-globals-from storageAccessAPIHelpers.js */
        await noStorageAccessInitially();
      }).toString();

      info("Checking if storage access is denied");

      let ifr = content.document.createElement("iframe");
      let loading = new content.Promise(resolve => {
        ifr.onload = resolve;
      });
      content.document.body.appendChild(ifr);
      ifr.src = obj.page;
      await loading;

      info(
        "The 3rd party content should not have access to first party storage."
      );
      await new content.Promise(resolve => {
        content.addEventListener("message", function msg(event) {
          if (event.data.type == "finish") {
            content.removeEventListener("message", msg);
            resolve();
            return;
          }

          if (event.data.type == "ok") {
            ok(event.data.what, event.data.msg);
            return;
          }

          if (event.data.type == "info") {
            info(event.data.msg);
            return;
          }

          ok(false, "Unknown message");
        });
        ifr.contentWindow.postMessage({ callback: msg.blockingCallback }, "*");
      });

      info("Opening a window from the iframe.");
      await SpecialPowers.spawn(ifr, [obj.popup], async popup => {
        let windowClosed = new content.Promise(resolve => {
          Services.ww.registerNotification(
            function notification(aSubject, aTopic) {
              // We need to check the document URI for Fission. It's because the
              // 'domwindowclosed' would be triggered twice, one for the
              // 'about:blank' page and another for the tracker page.
              if (
                aTopic == "domwindowclosed" &&
                aSubject.document.documentURI ==
                  "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/3rdPartyOpenUI.html"
              ) {
                Services.ww.unregisterNotification(notification);
                resolve();
              }
            }
          );
        });

        content.open(popup);

        info("Let's wait for the window to be closed");
        await windowClosed;
      });

      info("The 3rd party content should have access to first party storage.");
      await new content.Promise(resolve => {
        content.addEventListener("message", function msg(event) {
          if (event.data.type == "finish") {
            content.removeEventListener("message", msg);
            resolve();
            return;
          }

          if (event.data.type == "ok") {
            ok(event.data.what, event.data.msg);
            return;
          }

          if (event.data.type == "info") {
            info(event.data.msg);
            return;
          }

          ok(false, "Unknown message");
        });
        ifr.contentWindow.postMessage({ callback: msg.blockingCallback }, "*");
      });
    }
  );

  await AntiTracking.interactWithTracker();

  info("Loading tracking scripts");
  await SpecialPowers.spawn(
    browser,
    [
      {
        page: TEST_3RD_PARTY_PAGE_UI,
        popup: TEST_POPUP_PAGE,
        disableHeuristics,
      },
    ],
    async obj => {
      let msg = {};

      msg.blockingCallback = (async _ => {
        await noStorageAccessInitially();
      }).toString();

      // If the heuristic is disabled, we won't get storage access.
      if (obj.disableHeuristics) {
        msg.nonBlockingCallback = (async _ => {
          /* import-globals-from storageAccessAPIHelpers.js */
          await stillNoStorageAccess();
        }).toString();
      } else {
        msg.nonBlockingCallback = (async _ => {
          /* import-globals-from storageAccessAPIHelpers.js */
          await hasStorageAccessInitially();
        }).toString();
      }

      info("Checking if storage access is denied");

      let ifr = content.document.createElement("iframe");
      let loading = new content.Promise(resolve => {
        ifr.onload = resolve;
      });
      content.document.body.appendChild(ifr);
      ifr.src = obj.page;
      await loading;

      info(
        "The 3rd party content should not have access to first party storage."
      );
      await new content.Promise(resolve => {
        content.addEventListener("message", function msg(event) {
          if (event.data.type == "finish") {
            content.removeEventListener("message", msg);
            resolve();
            return;
          }

          if (event.data.type == "ok") {
            ok(event.data.what, event.data.msg);
            return;
          }

          if (event.data.type == "info") {
            info(event.data.msg);
            return;
          }

          ok(false, "Unknown message");
        });
        ifr.contentWindow.postMessage({ callback: msg.blockingCallback }, "*");
      });

      info("Opening a window from the iframe.");
      await SpecialPowers.spawn(ifr, [obj.popup], async popup => {
        let windowClosed = new content.Promise(resolve => {
          Services.ww.registerNotification(
            function notification(aSubject, aTopic) {
              // We need to check the document URI here as well for the same
              // reason above.
              if (
                aTopic == "domwindowclosed" &&
                aSubject.document.documentURI ==
                  "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/3rdPartyOpenUI.html"
              ) {
                Services.ww.unregisterNotification(notification);
                resolve();
              }
            }
          );
        });

        content.open(popup);

        info("Let's wait for the window to be closed");
        await windowClosed;
      });

      info("The 3rd party content should have access to first party storage.");
      await new content.Promise(resolve => {
        content.addEventListener("message", function msg(event) {
          if (event.data.type == "finish") {
            content.removeEventListener("message", msg);
            resolve();
            return;
          }

          if (event.data.type == "ok") {
            ok(event.data.what, event.data.msg);
            return;
          }

          if (event.data.type == "info") {
            info(event.data.msg);
            return;
          }

          ok(false, "Unknown message");
        });
        ifr.contentWindow.postMessage(
          { callback: msg.nonBlockingCallback },
          "*"
        );
      });
    }
  );

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

  if (!disableHeuristics) {
    info("Wait until the storage permission is ready before cleaning up.");
    await waitStoragePermission();
  }

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

  await SpecialPowers.popPrefEnv();
}

add_task(async function testUserInteractionHeuristic() {
  await runTestUserInteractionHeuristic(false);
});

add_task(async function testUserInteractionHeuristicWithoutTracker() {
  UrlClassifierTestUtils.cleanupTestTrackers();
  await runTestUserInteractionHeuristic(false);
  await UrlClassifierTestUtils.addTestTrackers();
});

add_task(async function testUserInteractionHeuristicDisabled() {
  await runTestUserInteractionHeuristic(true);
});

add_task(async function testDoublyNestedUserInteractionHeuristic() {
  info("Starting doubly nested user interaction heuristic test...");

  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);

  info("Loading tracking scripts");
  await SpecialPowers.spawn(
    browser,
    [
      {
        page: TEST_3RD_PARTY_PAGE_RELAY + "?" + TEST_3RD_PARTY_PAGE_UI,
        popup: TEST_POPUP_PAGE,
      },
    ],
    async obj => {
      let msg = {};
      msg.blockingCallback = (async _ => {
        /* import-globals-from storageAccessAPIHelpers.js */
        await noStorageAccessInitially();
      }).toString();

      msg.openWindowCallback = (async url => {
        open(url);
      }).toString();

      info("Checking if storage access is denied");

      let ifr = content.document.createElement("iframe");
      let loading = new content.Promise(resolve => {
        ifr.onload = resolve;
      });
      content.document.body.appendChild(ifr);
      ifr.src = obj.page;
      await loading;

      info(
        "The 3rd party content should not have access to first party storage."
      );
      await new content.Promise(resolve => {
        content.addEventListener("message", function msg(event) {
          if (event.data.type == "finish") {
            content.removeEventListener("message", msg);
            resolve();
            return;
          }

          if (event.data.type == "ok") {
            ok(event.data.what, event.data.msg);
            return;
          }

          if (event.data.type == "info") {
            info(event.data.msg);
            return;
          }

          ok(false, "Unknown message");
        });
        ifr.contentWindow.postMessage({ callback: msg.blockingCallback }, "*");
      });

      let windowClosed = new content.Promise(resolve => {
        Services.ww.registerNotification(
          function notification(aSubject, aTopic) {
            if (aTopic == "domwindowclosed") {
              Services.ww.unregisterNotification(notification);
              resolve();
            }
          }
        );
      });

      info("Opening a window from the iframe.");
      ifr.contentWindow.postMessage(
        { callback: msg.openWindowCallback, arg: obj.popup },
        "*"
      );

      info("Let's wait for the window to be closed");
      await windowClosed;

      info("The 3rd party content should have access to first party storage.");
      await new content.Promise(resolve => {
        content.addEventListener("message", function msg(event) {
          if (event.data.type == "finish") {
            content.removeEventListener("message", msg);
            resolve();
            return;
          }

          if (event.data.type == "ok") {
            ok(event.data.what, event.data.msg);
            return;
          }

          if (event.data.type == "info") {
            info(event.data.msg);
            return;
          }

          ok(false, "Unknown message");
        });
        ifr.contentWindow.postMessage({ callback: msg.blockingCallback }, "*");
      });
    }
  );

  await AntiTracking.interactWithTracker();

  info("Loading tracking scripts");
  await SpecialPowers.spawn(
    browser,
    [
      {
        page: TEST_3RD_PARTY_PAGE_RELAY + "?" + TEST_3RD_PARTY_PAGE_UI,
        popup: TEST_POPUP_PAGE,
      },
    ],
    async obj => {
      let msg = {};
      msg.blockingCallback = (async _ => {
        await noStorageAccessInitially();
      }).toString();

      msg.nonBlockingCallback = (async _ => {
        /* import-globals-from storageAccessAPIHelpers.js */
        await hasStorageAccessInitially();
      }).toString();

      msg.openWindowCallback = (async url => {
        open(url);
      }).toString();

      info("Checking if storage access is denied");

      let ifr = content.document.createElement("iframe");
      let loading = new content.Promise(resolve => {
        ifr.onload = resolve;
      });
      content.document.body.appendChild(ifr);
      ifr.src = obj.page;
      await loading;

      info(
        "The 3rd party content should not have access to first party storage."
      );
      await new content.Promise(resolve => {
        content.addEventListener("message", function msg(event) {
          if (event.data.type == "finish") {
            content.removeEventListener("message", msg);
            resolve();
            return;
          }

          if (event.data.type == "ok") {
            ok(event.data.what, event.data.msg);
            return;
          }

          if (event.data.type == "info") {
            info(event.data.msg);
            return;
          }

          ok(false, "Unknown message");
        });
        ifr.contentWindow.postMessage({ callback: msg.blockingCallback }, "*");
      });

      let windowClosed = new content.Promise(resolve => {
        Services.ww.registerNotification(
          function notification(aSubject, aTopic) {
            if (aTopic == "domwindowclosed") {
              Services.ww.unregisterNotification(notification);
              resolve();
            }
          }
        );
      });

      info("Opening a window from the iframe.");
      ifr.contentWindow.postMessage(
        { callback: msg.openWindowCallback, arg: obj.popup },
        "*"
      );

      info("Let's wait for the window to be closed");
      await windowClosed;

      info("The 3rd party content should have access to first party storage.");
      await new content.Promise(resolve => {
        content.addEventListener("message", function msg(event) {
          if (event.data.type == "finish") {
            content.removeEventListener("message", msg);
            resolve();
            return;
          }

          if (event.data.type == "ok") {
            ok(event.data.what, event.data.msg);
            return;
          }

          if (event.data.type == "info") {
            info(event.data.msg);
            return;
          }

          ok(false, "Unknown message");
        });
        ifr.contentWindow.postMessage(
          { callback: msg.nonBlockingCallback },
          "*"
        );
      });
    }
  );

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

add_task(async function () {
  info("Wait until the storage permission is ready before cleaning up.");
  await waitStoragePermission();

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

async function runTestFirstPartyWindowOpenHeuristic(disableHeuristics) {
  info(
    `Starting first-party window.open() heuristic test with heuristic ${
      disableHeuristics ? "disabled" : "enabled"
    }.`
  );

  await SpecialPowers.pushPrefEnv({
    set: [["privacy.antitracking.enableWebcompat", !disableHeuristics]],
  });

  // Interact with the tracker first before testing window.open heuristic
  await AntiTracking.interactWithTracker();

  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);

  info("Loading tracking scripts");
  await SpecialPowers.spawn(
    browser,
    [
      {
        page: TEST_3RD_PARTY_PAGE,
      },
    ],
    async obj => {
      info("Tracker shouldn't have storage access initially");
      let msg = {};
      msg.blockingCallback = (async _ => {
        await noStorageAccessInitially();
      }).toString();

      await new content.Promise(resolve => {
        let ifr = content.document.createElement("iframe");
        ifr.onload = function () {
          info("Sending code to the 3rd party content");
          ifr.contentWindow.postMessage(msg.blockingCallback, "*");
        };

        content.addEventListener("message", function msg(event) {
          if (event.data.type == "finish") {
            content.removeEventListener("message", msg);
            resolve();
            return;
          }

          if (event.data.type == "ok") {
            ok(event.data.what, event.data.msg);
            return;
          }

          if (event.data.type == "info") {
            info(event.data.msg);
            return;
          }

          ok(false, "Unknown message");
        });

        content.document.body.appendChild(ifr);
        ifr.id = "ifr";
        ifr.src = obj.page;
      });
    }
  );

  info("Calling window.open in a first-party iframe");
  await SpecialPowers.spawn(
    browser,
    [
      {
        page: TEST_IFRAME_PAGE,
        popup: TEST_3RD_PARTY_DOMAIN + TEST_PATH + "3rdPartyOpen.html",
      },
    ],
    async obj => {
      let ifr = content.document.createElement("iframe");
      let loading = new content.Promise(resolve => {
        ifr.onload = resolve;
      });
      content.document.body.appendChild(ifr);
      ifr.src = obj.page;
      await loading;

      info("Opening a window from the iframe.");
      await SpecialPowers.spawn(ifr, [obj.popup], async popup => {
        await new content.Promise(resolve => {
          content.open(popup);
          content.addEventListener("message", function msg(event) {
            if (event.data == "hello!") {
              resolve();
            }
          });
        });
      });
    }
  );

  await SpecialPowers.spawn(browser, [{ disableHeuristics }], async obj => {
    info(
      "If the heuristic is enabled, the tracker should have storage access now."
    );
    let msg = {};

    // If the heuristic is disabled, we won't get storage access.
    if (obj.disableHeuristics) {
      msg.nonBlockingCallback = (async _ => {
        /* import-globals-from storageAccessAPIHelpers.js */
        await stillNoStorageAccess();
      }).toString();
    } else {
      msg.nonBlockingCallback = (async _ => {
        /* import-globals-from storageAccessAPIHelpers.js */
        await hasStorageAccessInitially();
      }).toString();
    }

    await new content.Promise(resolve => {
      let ifr = content.document.getElementById("ifr");
      info("Sending code to the 3rd party content");
      ifr.contentWindow.postMessage(msg.nonBlockingCallback, "*");

      content.addEventListener("message", function msg(event) {
        if (event.data.type == "finish") {
          content.removeEventListener("message", msg);
          resolve();
          return;
        }

        if (event.data.type == "ok") {
          ok(event.data.what, event.data.msg);
          return;
        }

        if (event.data.type == "info") {
          info(event.data.msg);
          return;
        }

        ok(false, "Unknown message");
      });
    });
  });

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

  await SpecialPowers.popPrefEnv();

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

add_task(async function testFirstPartyWindowOpenHeuristic() {
  await runTestFirstPartyWindowOpenHeuristic(false);
});

add_task(async function testFirstPartyWindowOpenHeuristicDisabled() {
  await runTestFirstPartyWindowOpenHeuristic(true);
});

add_task(async function () {
  UrlClassifierTestUtils.cleanupTestTrackers();
});
