/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */

"use strict";

const { RemoteSettings } = ChromeUtils.importESModule(
  "resource://services-settings/remote-settings.sys.mjs"
);

const COLLECTION_NAME = "fingerprinting-protection-overrides";

// The javascript bitwise operator only support 32bits. So, we only test
// RFPTargets that is under low 32 bits.
const TARGET_DEFAULT = extractLow32Bits(
  Services.rfp.enabledFingerprintingProtections
);
const TARGET_PointerEvents = 0x00000002;
const TARGET_CanvasRandomization = 0x000000100;
const TARGET_WindowOuterSize = 0x002000000;
const TARGET_Gamepad = 0x00800000;

const TEST_PAGE =
  getRootDirectory(gTestPath).replace(
    "chrome://mochitests/content",
    "https://example.com"
  ) + "empty.html";

const TEST_ANOTHER_PAGE =
  getRootDirectory(gTestPath).replace(
    "chrome://mochitests/content",
    "https://example.net"
  ) + "empty.html";

// A helper function to filter high 32 bits.
function extractLow32Bits(value) {
  return value & 0xffffffff;
}

const TEST_CASES = [
  // Test simple addition.
  {
    entires: [
      {
        id: "1",
        last_modified: 1000000000000001,
        overrides: "+WindowOuterSize",
        firstPartyDomain: "example.org",
      },
    ],
    expects: [
      {
        domain: "example.org",
        overrides: TARGET_DEFAULT | TARGET_WindowOuterSize,
      },
      { domain: "example.com", noEntry: true },
    ],
  },
  // Test simple subtraction
  {
    entires: [
      {
        id: "1",
        last_modified: 1000000000000001,
        overrides: "-WindowOuterSize",
        firstPartyDomain: "example.org",
      },
    ],
    expects: [
      {
        domain: "example.org",
        overrides: TARGET_DEFAULT & ~TARGET_WindowOuterSize,
      },
      { domain: "example.com", noEntry: true },
    ],
  },
  // Test simple subtraction for default targets.
  {
    entires: [
      {
        id: "1",
        last_modified: 1000000000000001,
        overrides: "-CanvasRandomization",
        firstPartyDomain: "example.org",
      },
    ],
    expects: [
      {
        domain: "example.org",
        overrides: TARGET_DEFAULT & ~TARGET_CanvasRandomization,
      },
      { domain: "example.com", noEntry: true },
    ],
  },
  // Test multiple targets
  {
    entires: [
      {
        id: "1",
        last_modified: 1000000000000001,
        overrides: "+WindowOuterSize,+PointerEvents,-Gamepad",
        firstPartyDomain: "example.org",
      },
    ],
    expects: [
      {
        domain: "example.org",
        overrides:
          (TARGET_DEFAULT | TARGET_WindowOuterSize | TARGET_PointerEvents) &
          ~TARGET_Gamepad,
      },
      { domain: "example.com", noEntry: true },
    ],
  },
  // Test multiple entries
  {
    entires: [
      {
        id: "1",
        last_modified: 1000000000000001,
        overrides: "+WindowOuterSize,+PointerEvents,-Gamepad",
        firstPartyDomain: "example.org",
      },
      {
        id: "2",
        last_modified: 1000000000000001,
        overrides: "+Gamepad",
        firstPartyDomain: "example.com",
      },
    ],
    expects: [
      {
        domain: "example.org",
        overrides:
          (TARGET_DEFAULT | TARGET_WindowOuterSize | TARGET_PointerEvents) &
          ~TARGET_Gamepad,
      },
      {
        domain: "example.com",
        overrides: TARGET_DEFAULT | TARGET_Gamepad,
      },
    ],
  },
  // Test an entry with both first-party and third-party domains.
  {
    entires: [
      {
        id: "1",
        last_modified: 1000000000000001,
        overrides: "+WindowOuterSize,+PointerEvents,-Gamepad",
        firstPartyDomain: "example.org",
        thirdPartyDomain: "example.com",
      },
    ],
    expects: [
      {
        domain: "example.org,example.com",
        overrides:
          (TARGET_DEFAULT | TARGET_WindowOuterSize | TARGET_PointerEvents) &
          ~TARGET_Gamepad,
      },
    ],
  },
  // Test an entry with the first-party set to a wildcard.
  {
    entires: [
      {
        id: "1",
        last_modified: 1000000000000001,
        overrides: "+WindowOuterSize,+PointerEvents,-Gamepad",
        firstPartyDomain: "*",
      },
    ],
    expects: [
      {
        domain: "*",
        overrides:
          (TARGET_DEFAULT | TARGET_WindowOuterSize | TARGET_PointerEvents) &
          ~TARGET_Gamepad,
      },
    ],
  },
  // Test a entry with the first-party set to a wildcard and the third-party set
  // to a domain.
  {
    entires: [
      {
        id: "1",
        last_modified: 1000000000000001,
        overrides: "+WindowOuterSize,+PointerEvents,-Gamepad",
        firstPartyDomain: "*",
        thirdPartyDomain: "example.com",
      },
    ],
    expects: [
      {
        domain: "*,example.com",
        overrides:
          (TARGET_DEFAULT | TARGET_WindowOuterSize | TARGET_PointerEvents) &
          ~TARGET_Gamepad,
      },
    ],
  },
  // Test an entry with all targets disabled using '-AllTargets' but only enable one target.
  {
    entires: [
      {
        id: "1",
        last_modified: 1000000000000001,
        overrides: "-AllTargets,+WindowOuterSize",
        firstPartyDomain: "*",
        thirdPartyDomain: "example.com",
      },
    ],
    expects: [
      {
        domain: "*,example.com",
        overrides: TARGET_WindowOuterSize,
      },
    ],
  },
  // Test an entry with all targets disabled using '-AllTargets' but enable some targets.
  {
    entires: [
      {
        id: "1",
        last_modified: 1000000000000001,
        overrides: "-AllTargets,+WindowOuterSize,+Gamepad",
        firstPartyDomain: "*",
        thirdPartyDomain: "example.com",
      },
    ],
    expects: [
      {
        domain: "*,example.com",
        overrides: TARGET_WindowOuterSize | TARGET_Gamepad,
      },
    ],
  },
  // Test an invalid entry with only third party domain.
  {
    entires: [
      {
        id: "1",
        last_modified: 1000000000000001,
        overrides: "+WindowOuterSize,+PointerEvents,-Gamepad",
        thirdPartyDomain: "example.com",
      },
    ],
    expects: [
      {
        domain: "example.com",
        noEntry: true,
      },
    ],
  },
  // Test an invalid entry with both wildcards.
  {
    entires: [
      {
        id: "1",
        last_modified: 1000000000000001,
        overrides: "+WindowOuterSize,+PointerEvents,-Gamepad",
        firstPartyDomain: "*",
        thirdPartyDomain: "*",
      },
    ],
    expects: [
      {
        domain: "example.org",
        noEntry: true,
      },
    ],
  },
];

add_setup(async function () {
  await SpecialPowers.pushPrefEnv({
    set: [["privacy.fingerprintingProtection.remoteOverrides.testing", true]],
  });

  registerCleanupFunction(() => {
    Services.rfp.cleanAllOverrides();
  });
});

add_task(async function test_remote_settings() {
  // Add initial empty record.
  let db = RemoteSettings(COLLECTION_NAME).db;
  await db.importChanges({}, Date.now(), []);

  for (let test of TEST_CASES) {
    info(`Testing with entry ${JSON.stringify(test.entires)}`);

    // Create a promise for waiting the overrides get updated.
    let promise = promiseObserver("fpp-test:set-overrides-finishes");

    // Trigger the fingerprinting overrides update by a remote settings sync.
    await RemoteSettings(COLLECTION_NAME).emit("sync", {
      data: {
        current: test.entires,
      },
    });
    await promise;

    ok(true, "Got overrides update");

    for (let expect of test.expects) {
      // Get the addition and subtraction flags for the domain.
      try {
        let overrides = extractLow32Bits(
          Services.rfp.getFingerprintingOverrides(expect.domain)
        );

        // Verify if the flags are matching to expected values.
        is(overrides, expect.overrides, "The override value is correct.");
      } catch (e) {
        ok(expect.noEntry, "The override entry doesn't exist.");
      }
    }
  }

  db.clear();
});

add_task(async function test_remote_settings_pref() {
  // Add initial empty record.
  let db = RemoteSettings(COLLECTION_NAME).db;
  await db.importChanges({}, Date.now(), []);

  for (let test of TEST_CASES) {
    info(`Testing with entry ${JSON.stringify(test.entires)}`);

    // Disable remote overrides
    await SpecialPowers.pushPrefEnv({
      set: [
        ["privacy.fingerprintingProtection.remoteOverrides.enabled", false],
      ],
    });

    // Create a promise for waiting the overrides get updated.
    let promise = promiseObserver("fpp-test:set-overrides-finishes");

    // Trigger the fingerprinting overrides update by a remote settings sync.
    await RemoteSettings(COLLECTION_NAME).emit("sync", {
      data: {
        current: test.entires,
      },
    });
    await promise;

    ok(true, "Got overrides update");

    for (let expect of test.expects) {
      try {
        // Check for the existance of RFP overrides
        Services.rfp.getFingerprintingOverrides(expect.domain);
        ok(
          false,
          "This line should never run as the override should not exist and the previous line would throw an exception"
        );
      } catch (e) {
        ok(true, "Received an exception as expected");
      }
    }
  }

  db.clear();
});

add_task(async function test_pref() {
  for (let test of TEST_CASES) {
    info(`Testing with entry ${JSON.stringify(test.entires)}`);

    // Create a promise for waiting the overrides get updated.
    let promise = promiseObserver("fpp-test:set-overrides-finishes");

    // Trigger the fingerprinting overrides update by setting the pref.
    await SpecialPowers.pushPrefEnv({
      set: [
        [
          "privacy.fingerprintingProtection.granularOverrides",
          JSON.stringify(test.entires),
        ],
      ],
    });

    await promise;
    ok(true, "Got overrides update");

    for (let expect of test.expects) {
      try {
        // Get the addition and subtraction flags for the domain.
        let overrides = extractLow32Bits(
          Services.rfp.getFingerprintingOverrides(expect.domain)
        );

        // Verify if the flags are matching to expected values.
        is(overrides, expect.overrides, "The override value is correct.");
      } catch (e) {
        ok(expect.noEntry, "The override entry doesn't exist.");
      }
    }
  }
});

// Verify if the pref overrides the remote settings.
add_task(async function test_pref_override_remote_settings() {
  // Add initial empty record.
  let db = RemoteSettings(COLLECTION_NAME).db;
  await db.importChanges({}, Date.now(), []);

  // Trigger a remote settings sync.
  let promise = promiseObserver("fpp-test:set-overrides-finishes");
  await RemoteSettings(COLLECTION_NAME).emit("sync", {
    data: {
      current: [
        {
          id: "1",
          last_modified: 1000000000000001,
          overrides: "+WindowOuterSize",
          firstPartyDomain: "example.org",
        },
      ],
    },
  });
  await promise;

  // Then, setting the pref.
  promise = promiseObserver("fpp-test:set-overrides-finishes");
  await SpecialPowers.pushPrefEnv({
    set: [
      [
        "privacy.fingerprintingProtection.granularOverrides",
        JSON.stringify([
          {
            id: "1",
            last_modified: 1000000000000001,
            overrides: "+PointerEvents,-WindowOuterSize,-Gamepad",
            firstPartyDomain: "example.org",
          },
        ]),
      ],
    ],
  });
  await promise;

  // Get the addition and subtraction flags for the domain.
  let overrides = extractLow32Bits(
    Services.rfp.getFingerprintingOverrides("example.org")
  );

  // Verify if the flags are matching to the pref settings.
  is(
    overrides,
    (TARGET_DEFAULT | TARGET_PointerEvents) &
      ~TARGET_Gamepad &
      ~TARGET_WindowOuterSize,
    "The override addition value is correct."
  );

  db.clear();
});

// Bug 1873682 - Verify that a third-party beacon request won't hit the
// assertion in nsRFPService::GetOverriddenFingerprintingSettingsForChannel().
add_task(async function test_beacon_request() {
  // Open an empty page.
  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_PAGE);

  await SpecialPowers.spawn(
    tab.linkedBrowser,
    [TEST_ANOTHER_PAGE],
    async url => {
      // Create a third-party iframe
      let ifr = content.document.createElement("iframe");

      await new content.Promise(resolve => {
        ifr.onload = resolve;
        content.document.body.appendChild(ifr);
        ifr.src = url;
      });

      await SpecialPowers.spawn(ifr, [url], url => {
        // Sending the beacon request right before the tab navigates away.
        content.addEventListener("unload", _ => {
          let value = ["text"];
          let blob = new Blob(value, {
            type: "application/x-www-form-urlencoded",
          });
          content.navigator.sendBeacon(url, blob);
        });
      });

      // Navigate the tab to another page.
      content.location = url;
    }
  );

  await BrowserTestUtils.browserLoaded(
    tab.linkedBrowser,
    false,
    TEST_ANOTHER_PAGE
  );

  ok(true, "Successfully navigates away.");

  BrowserTestUtils.removeTab(tab);
});
