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

"use strict";

requestLongerTimeout(2);

/**
 * Bug 1863280 - Testing the fingerprinting category of the protection panel
 *               shows the suspicious fingerpinter domain if the fingerprinting
 *               protection is enabled
 */

const TEST_PATH = getRootDirectory(gTestPath).replace(
  "chrome://mochitests/content",
  ""
);

const TEST_DOMAIN = "https://www.example.com";
const TEST_3RD_DOMAIN = "https://example.org";

const TEST_PAGE = TEST_DOMAIN + TEST_PATH + "benignPage.html";
const TEST_3RD_CANVAS_FP_PAGE =
  TEST_3RD_DOMAIN + TEST_PATH + "canvas-fingerprinter.html";
const TEST_3RD_FONT_FP_PAGE =
  TEST_3RD_DOMAIN + TEST_PATH + "font-fingerprinter.html";

const FINGERPRINT_BLOCKING_PREF =
  "privacy.trackingprotection.fingerprinting.enabled";
const FINGERPRINT_PROTECTION_PREF = "privacy.fingerprintingProtection";
const FINGERPRINT_PROTECTION_PBM_PREF =
  "privacy.fingerprintingProtection.pbmode";

/**
 * A helper function to check whether or not an element has "notFound" class.
 *
 * @param {String} id The id of the testing element.
 * @returns {Boolean} true when the element has "notFound" class.
 */
function notFound(id) {
  return document.getElementById(id).classList.contains("notFound");
}

/**
 * A helper function to wait until the suspicious fingerprinting content
 * blocking event is dispatched.
 *
 * @param {Window} win The window to listen content blocking events.
 * @returns {Promise} A promise that resolves when the event files.
 */
async function waitForSuspiciousFingerprintingEvent(win) {
  return new Promise(resolve => {
    let listener = {
      onContentBlockingEvent(webProgress, request, event) {
        info(`Received onContentBlockingEvent event: ${event}`);
        if (
          event &
          Ci.nsIWebProgressListener.STATE_BLOCKED_SUSPICIOUS_FINGERPRINTING
        ) {
          win.gBrowser.removeProgressListener(listener);
          resolve();
        }
      },
    };
    win.gBrowser.addProgressListener(listener);
  });
}

/**
 * A helper function that opens a tests page that embeds iframes with given
 * urls.
 *
 * @param {string[]} urls An array of urls that will be embedded in the test page.
 * @param {boolean} usePrivateWin true to indicate testing in a private window.
 * @param {Function} testFn The test function that will be called after the
 * iframes have been loaded.
 */
async function openTestPage(urls, usePrivateWin, testFn) {
  let win = window;

  if (usePrivateWin) {
    win = await BrowserTestUtils.openNewBrowserWindow({ private: true });
  }

  await BrowserTestUtils.withNewTab(
    { gBrowser: win.gBrowser, url: TEST_PAGE },
    async browser => {
      info("Loading all iframes");

      for (const url of urls) {
        await SpecialPowers.spawn(browser, [url], async testUrl => {
          await new content.Promise(resolve => {
            let ifr = content.document.createElement("iframe");
            ifr.onload = resolve;
            ifr.src = testUrl;

            content.document.body.appendChild(ifr);
          });
        });
      }

      info("Running the test");
      await testFn(win, browser);
    }
  );

  if (usePrivateWin) {
    await BrowserTestUtils.closeWindow(win);
  }
}

/**
 * A testing function verifies that fingerprinting category is not shown.
 *
 * @param {*} win The window to run the test.
 */
async function testCategoryNotShown(win) {
  await openProtectionsPanel(false, win);

  let categoryItem = win.document.getElementById(
    "protections-popup-category-fingerprinters"
  );

  // The fingerprinting category should have the 'notFound' class to indicate
  // that no fingerprinter was found in the page.
  ok(
    notFound("protections-popup-category-fingerprinters"),
    "Fingerprinting category is not found"
  );

  ok(
    !BrowserTestUtils.isVisible(categoryItem),
    "Fingerprinting category item is not visible"
  );

  await closeProtectionsPanel(win);
}

add_setup(async function () {
  await SpecialPowers.pushPrefEnv({
    set: [[FINGERPRINT_BLOCKING_PREF, true]],
  });
});

// Verify that fingerprinting category is not shown if fingerprinting protection
// is disabled.
add_task(async function testFPPDisabled() {
  await SpecialPowers.pushPrefEnv({
    set: [[FINGERPRINT_PROTECTION_PREF, false]],
  });

  await openTestPage(
    [TEST_3RD_CANVAS_FP_PAGE, TEST_3RD_FONT_FP_PAGE],
    false,
    testCategoryNotShown
  );
});

// Verify that fingerprinting category is not shown if no fingerprinting
// activity is detected.
add_task(async function testFPPEnabledWithoutFingerprintingActivity() {
  await SpecialPowers.pushPrefEnv({
    set: [[FINGERPRINT_PROTECTION_PREF, true]],
  });

  // Test the case where the page doesn't load any fingerprinter.
  await openTestPage([], false, testCategoryNotShown);

  // Test the case where the page loads only one fingerprinter. We don't treat
  // this case as suspicious fingerprinting.
  await openTestPage([TEST_3RD_FONT_FP_PAGE], false, testCategoryNotShown);

  // Test the case where the page loads the same fingerprinter multiple times.
  // We don't treat this case as suspicious fingerprinting.
  await openTestPage(
    [TEST_3RD_FONT_FP_PAGE, TEST_3RD_FONT_FP_PAGE],
    false,
    testCategoryNotShown
  );
});

// Verify that fingerprinting category is not shown if no fingerprinting
// activity is detected.
add_task(
  async function testFPPEnabledWithoutSuspiciousFingerprintingActivity() {
    await SpecialPowers.pushPrefEnv({
      set: [[FINGERPRINT_PROTECTION_PREF, true]],
    });

    // Test the case where the page doesn't load any fingerprinter.
    await openTestPage([], false, testCategoryNotShown);

    // Test the case where the page loads only one fingerprinter. We don't treat
    // this case as suspicious fingerprinting.
    await openTestPage([TEST_3RD_FONT_FP_PAGE], false, testCategoryNotShown);

    // Test the case where the page loads the same fingerprinter multiple times.
    // We don't treat this case as suspicious fingerprinting.
    await openTestPage(
      [TEST_3RD_FONT_FP_PAGE, TEST_3RD_FONT_FP_PAGE],
      false,
      testCategoryNotShown
    );
  }
);

// Verify that fingerprinting category is properly shown and the fingerprinting
// subview displays the origin of the suspicious fingerprinter.
add_task(async function testFingerprintingSubview() {
  await SpecialPowers.pushPrefEnv({
    set: [[FINGERPRINT_PROTECTION_PREF, true]],
  });

  await openTestPage(
    [TEST_3RD_CANVAS_FP_PAGE, TEST_3RD_FONT_FP_PAGE],
    false,
    async (win, _) => {
      await openProtectionsPanel(false, win);

      let categoryItem = win.document.getElementById(
        "protections-popup-category-fingerprinters"
      );

      // Explicitly waiting for the category item becoming visible.
      await BrowserTestUtils.waitForMutationCondition(categoryItem, {}, () =>
        BrowserTestUtils.isVisible(categoryItem)
      );

      ok(
        BrowserTestUtils.isVisible(categoryItem),
        "Fingerprinting category item is visible"
      );

      // Click the fingerprinting category and wait until the fingerprinter view is
      // shown.
      let fingerprintersView = win.document.getElementById(
        "protections-popup-fingerprintersView"
      );
      let viewShown = BrowserTestUtils.waitForEvent(
        fingerprintersView,
        "ViewShown"
      );
      categoryItem.click();
      await viewShown;

      ok(true, "Fingerprinter view was shown");

      // Ensure the fingerprinter is listed on the tracker list.
      let listItems = Array.from(
        fingerprintersView.querySelectorAll(".protections-popup-list-item")
      );
      is(listItems.length, 1, "We have 1 fingerprinter in the list");

      let listItem = listItems.find(
        item => item.querySelector("label").value == "https://example.org"
      );
      ok(listItem, "Has an item for example.org");
      ok(BrowserTestUtils.isVisible(listItem), "List item is visible");

      // Back to the popup main view.
      let mainView = win.document.getElementById("protections-popup-mainView");
      viewShown = BrowserTestUtils.waitForEvent(mainView, "ViewShown");
      let backButton = fingerprintersView.querySelector(".subviewbutton-back");
      backButton.click();
      await viewShown;

      ok(true, "Main view was shown");

      await closeProtectionsPanel(win);
    }
  );
});

// Verify the case where the fingerprinting protection is only enabled in PBM.
add_task(async function testFingerprintingSubviewInPBM() {
  // Only enabled fingerprinting protection in PBM.
  await SpecialPowers.pushPrefEnv({
    set: [
      [FINGERPRINT_PROTECTION_PREF, false],
      [FINGERPRINT_PROTECTION_PBM_PREF, true],
    ],
  });

  // Verify that fingerprinting category isn't shown on a normal window.
  await openTestPage(
    [TEST_3RD_CANVAS_FP_PAGE, TEST_3RD_FONT_FP_PAGE],
    false,
    testCategoryNotShown
  );

  // Verify that fingerprinting category is shown on a private window.
  await openTestPage(
    [TEST_3RD_CANVAS_FP_PAGE, TEST_3RD_FONT_FP_PAGE],
    true,
    async (win, _) => {
      await openProtectionsPanel(false, win);

      let categoryItem = win.document.getElementById(
        "protections-popup-category-fingerprinters"
      );

      // Explicitly waiting for the category item becoming visible.
      await BrowserTestUtils.waitForMutationCondition(categoryItem, {}, () =>
        BrowserTestUtils.isVisible(categoryItem)
      );

      ok(
        BrowserTestUtils.isVisible(categoryItem),
        "Fingerprinting category item is visible"
      );

      // Click the fingerprinting category and wait until the fingerprinter view is
      // shown.
      let fingerprintersView = win.document.getElementById(
        "protections-popup-fingerprintersView"
      );
      let viewShown = BrowserTestUtils.waitForEvent(
        fingerprintersView,
        "ViewShown"
      );
      categoryItem.click();
      await viewShown;

      ok(true, "Fingerprinter view was shown");

      // Ensure the fingerprinter is listed on the tracker list.
      let listItems = Array.from(
        fingerprintersView.querySelectorAll(".protections-popup-list-item")
      );
      is(listItems.length, 1, "We have 1 fingerprinter in the list");

      let listItem = listItems.find(
        item => item.querySelector("label").value == "https://example.org"
      );
      ok(listItem, "Has an item for example.org");
      ok(BrowserTestUtils.isVisible(listItem), "List item is visible");

      // Back to the popup main view.
      let mainView = win.document.getElementById("protections-popup-mainView");
      viewShown = BrowserTestUtils.waitForEvent(mainView, "ViewShown");
      let backButton = fingerprintersView.querySelector(".subviewbutton-back");
      backButton.click();
      await viewShown;

      ok(true, "Main view was shown");

      await closeProtectionsPanel(win);
    }
  );
});

// Verify that fingerprinting category will be shown after loading
// fingerprinters.
add_task(async function testDynamicallyLoadFingerprinter() {
  await SpecialPowers.pushPrefEnv({
    set: [[FINGERPRINT_PROTECTION_PREF, true]],
  });

  await openTestPage([TEST_3RD_FONT_FP_PAGE], false, async (win, browser) => {
    await openProtectionsPanel(false, win);

    let categoryItem = win.document.getElementById(
      "protections-popup-category-fingerprinters"
    );

    // The fingerprinting category should have the 'notFound' class to indicate
    // that no suspicious fingerprinter was found in the page.
    ok(
      notFound("protections-popup-category-fingerprinters"),
      "Fingerprinting category is not found"
    );

    ok(
      !BrowserTestUtils.isVisible(categoryItem),
      "Fingerprinting category item is not visible"
    );

    // Add an iframe that triggers suspicious fingerprinting and wait until the
    // content event files.

    let contentBlockingEventPromise = waitForSuspiciousFingerprintingEvent(win);
    await SpecialPowers.spawn(browser, [TEST_3RD_CANVAS_FP_PAGE], test_url => {
      let ifr = content.document.createElement("iframe");

      content.document.body.appendChild(ifr);
      ifr.src = test_url;
    });
    await contentBlockingEventPromise;

    // Click the fingerprinting category and wait until the fingerprinter view
    // is shown.
    let fingerprintersView = win.document.getElementById(
      "protections-popup-fingerprintersView"
    );
    let viewShown = BrowserTestUtils.waitForEvent(
      fingerprintersView,
      "ViewShown"
    );
    categoryItem.click();
    await viewShown;

    ok(true, "Fingerprinter view was shown");

    // Ensure the fingerprinter is listed on the tracker list.
    let listItems = Array.from(
      fingerprintersView.querySelectorAll(".protections-popup-list-item")
    );
    is(listItems.length, 1, "We have 1 fingerprinter in the list");

    let listItem = listItems.find(
      item => item.querySelector("label").value == "https://example.org"
    );
    ok(listItem, "Has an item for example.org");
    ok(BrowserTestUtils.isVisible(listItem), "List item is visible");

    // Back to the popup main view.
    let mainView = win.document.getElementById("protections-popup-mainView");
    viewShown = BrowserTestUtils.waitForEvent(mainView, "ViewShown");
    let backButton = fingerprintersView.querySelector(".subviewbutton-back");
    backButton.click();
    await viewShown;

    ok(true, "Main view was shown");

    await closeProtectionsPanel(win);
  });
});
