/* 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 http://mozilla.org/MPL/2.0/. */

"use strict";

/* Unit tests for the nsIUrlClassifierExceptionListService implementation. */

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

const COLLECTION_NAME = "url-classifier-exceptions";
const FEATURE_TRACKING_NAME = "tracking-annotation-test";
const FEATURE_TRACKING_PREF_NAME = "urlclassifier.tracking-annotation-test";
const FEATURE_SOCIAL_NAME = "socialtracking-annotation-test";
const FEATURE_SOCIAL_PREF_NAME = "urlclassifier.socialtracking-annotation-test";
const FEATURE_FINGERPRINTING_NAME = "fingerprinting-annotation-test";
const FEATURE_FINGERPRINTING_PREF_NAME =
  "urlclassifier.fingerprinting-annotation-test";

do_get_profile();

class UpdateEvent extends EventTarget {}
function waitForEvent(element, eventName) {
  return new Promise(function (resolve) {
    element.addEventListener(eventName, e => resolve(e.detail), { once: true });
  });
}

add_task(async function test_list_changes() {
  let exceptionListService = Cc[
    "@mozilla.org/url-classifier/exception-list-service;1"
  ].getService(Ci.nsIUrlClassifierExceptionListService);

  // Make sure we have a pref initially, since the exception list service
  // requires it.
  Services.prefs.setStringPref(FEATURE_TRACKING_PREF_NAME, "");

  let updateEvent = new UpdateEvent();
  let obs = data => {
    let event = new CustomEvent("update", { detail: data });
    updateEvent.dispatchEvent(event);
  };

  let records = [
    {
      id: "1",
      last_modified: 1000000000000001,
      category: "internal-pref",
      classifierFeatures: [FEATURE_TRACKING_NAME],
      urlPattern: "*://example.com/*",
    },
  ];

  // Add some initial data.
  let db = RemoteSettings(COLLECTION_NAME).db;
  await db.importChanges({}, Date.now(), records);
  let promise = waitForEvent(updateEvent, "update");

  exceptionListService.registerAndRunExceptionListObserver(
    FEATURE_TRACKING_NAME,
    FEATURE_TRACKING_PREF_NAME,
    obs
  );

  let list = await promise;
  Assert.equal(list.testGetEntries().length, 0, "No items in the list");

  // Second event is from the RemoteSettings record.
  list = await waitForEvent(updateEvent, "update");

  Assert.equal(list.testGetEntries().length, 1, "Has one item in the list");
  Assert.equal(
    list.testGetEntries()[0].urlPattern,
    "*://example.com/*",
    "First item is example.com"
  );
  records.push(
    // An entry which populates all fields of
    // nsIUrlClassifierExceptionListEntry.
    {
      id: "2",
      last_modified: 1000000000000002,
      category: "baseline",
      classifierFeatures: [FEATURE_TRACKING_NAME],
      urlPattern: "*://MOZILLA.ORG/*",
      topLevelUrlPattern: "*://example.com/*",
      isPrivateBrowsingOnly: true,
      filterContentBlockingCategories: ["standard"],
    },
    {
      id: "3",
      last_modified: 1000000000000003,
      category: "convenience",
      classifierFeatures: ["some-other-feature"],
      urlPattern: "*://noinclude.com/*",
    },
    {
      last_modified: 1000000000000004,
      category: "baseline",
      classifierFeatures: [FEATURE_TRACKING_NAME],
      urlPattern: "*://*.example.org/*",
    }
  );

  promise = waitForEvent(updateEvent, "update");

  await RemoteSettings(COLLECTION_NAME).emit("sync", {
    data: { current: records },
  });

  list = await promise;

  let entries = list.testGetEntries();
  Assert.equal(entries.length, 3, "Has three items in the list");
  entries = entries.sort((a, b) => a.urlPattern.localeCompare(b.urlPattern));

  Assert.equal(
    entries[1].urlPattern,
    "*://example.com/*",
    "First item is example.com"
  );
  Assert.equal(
    entries[2].urlPattern,
    "*://MOZILLA.ORG/*",
    "Second item is mozilla.org"
  );
  Assert.equal(
    entries[2].topLevelUrlPattern,
    "*://example.com/*",
    "Top level url pattern of second item is correctly set."
  );
  Assert.equal(
    entries[2].isPrivateBrowsingOnly,
    true,
    "isPrivateBrowsingOnly flag of second item is correctly set."
  );
  Assert.deepEqual(
    entries[2].filterContentBlockingCategories,
    ["standard"],
    "filterContentBlockingCategories of second item is correctly set."
  );
  Assert.equal(
    entries[0].urlPattern,
    "*://*.example.org/*",
    "Third item is *.example.org"
  );

  promise = waitForEvent(updateEvent, "update");

  Services.prefs.setStringPref(FEATURE_TRACKING_PREF_NAME, "*://test.com/*");

  list = await promise;

  entries = list.testGetEntries();

  Assert.equal(entries.length, 4, "Has four items in the list");

  entries = entries.sort((a, b) => a.urlPattern.localeCompare(b.urlPattern));

  Assert.equal(
    entries[1].urlPattern,
    "*://example.com/*",
    "First item is example.com"
  );
  Assert.equal(
    entries[2].urlPattern,
    "*://MOZILLA.ORG/*",
    "Second item is mozilla.org"
  );
  Assert.equal(
    entries[0].urlPattern,
    "*://*.example.org/*",
    "Third item is *.example.org"
  );
  Assert.equal(
    entries[3].urlPattern,
    "*://test.com/*",
    "Fourth item is test.com"
  );

  promise = waitForEvent(updateEvent, "update");

  Services.prefs.setStringPref(
    FEATURE_TRACKING_PREF_NAME,
    "*://test.com/*,*://whatever.com/*,*://*.abc.com/*"
  );

  list = await promise;

  entries = list.testGetEntries();
  Assert.equal(entries.length, 6, "Has six items in the list");

  entries = entries.sort((a, b) => a.urlPattern.localeCompare(b.urlPattern));

  Assert.equal(
    entries[4].urlPattern,
    "*://test.com/*",
    "First item is test.com"
  );
  Assert.equal(
    entries[5].urlPattern,
    "*://whatever.com/*",
    "Second item is whatever.com"
  );
  Assert.equal(
    entries[0].urlPattern,
    "*://*.abc.com/*",
    "Third item is *.abc.com"
  );
  Assert.equal(
    entries[2].urlPattern,
    "*://example.com/*",
    "Fourth item is example.com"
  );
  Assert.equal(
    entries[3].urlPattern,
    "*://MOZILLA.ORG/*",
    "Fifth item is mozilla.org"
  );
  Assert.equal(
    entries[1].urlPattern,
    "*://*.example.org/*",
    "Sixth item is *.example.org"
  );

  exceptionListService.unregisterExceptionListObserver(
    FEATURE_TRACKING_NAME,
    obs
  );
  exceptionListService.clear();

  await db.clear();
});

/**
 * This test make sure when a feature registers itself to exceptionlist service,
 * it can get the correct initial data.
 */
add_task(async function test_list_init_data() {
  let exceptionListService = Cc[
    "@mozilla.org/url-classifier/exception-list-service;1"
  ].getService(Ci.nsIUrlClassifierExceptionListService);

  // Make sure we have a pref initially, since the exception list service
  // requires it.
  Services.prefs.setStringPref(FEATURE_TRACKING_PREF_NAME, "");

  let updateEvent = new UpdateEvent();

  let records = [
    {
      id: "1",
      last_modified: 1000000000000001,
      category: "baseline",
      classifierFeatures: [FEATURE_TRACKING_NAME],
      urlPattern: "*://tracking.example.com/*",
    },
    {
      id: "2",
      last_modified: 1000000000000002,
      category: "convenience",
      classifierFeatures: [FEATURE_SOCIAL_NAME],
      urlPattern: "*://social.example.com/*",
    },
    {
      id: "3",
      last_modified: 1000000000000003,
      category: "baseline",
      classifierFeatures: [FEATURE_TRACKING_NAME],
      urlPattern: "*://*.tracking.org/*",
    },
    {
      id: "4",
      last_modified: 1000000000000004,
      category: "convenience",
      classifierFeatures: [FEATURE_SOCIAL_NAME],
      urlPattern: "*://MOZILLA.ORG/*",
    },
  ];

  // Add some initial data.
  let db = RemoteSettings(COLLECTION_NAME).db;
  await db.importChanges({}, Date.now(), records);

  // The first registered feature make ExceptionListService get the initial data
  // from remote setting.
  let promise = waitForEvent(updateEvent, "update");

  let obs = data => {
    let event = new CustomEvent("update", { detail: data });
    updateEvent.dispatchEvent(event);
  };
  exceptionListService.registerAndRunExceptionListObserver(
    FEATURE_TRACKING_NAME,
    FEATURE_TRACKING_PREF_NAME,
    obs
  );

  let list = await promise;
  Assert.equal(list.testGetEntries().length, 0, "Empty list initially");

  list = await waitForEvent(updateEvent, "update");
  let entries = list.testGetEntries();
  entries = entries.sort((a, b) => a.urlPattern.localeCompare(b.urlPattern));

  Assert.equal(entries.length, 2, "Has two items in the list");

  Assert.equal(
    entries[0].urlPattern,
    "*://*.tracking.org/*",
    "First item is *.tracking.org"
  );
  Assert.equal(
    entries[1].urlPattern,
    "*://tracking.example.com/*",
    "Second item is tracking.example.com"
  );

  // Register another feature after ExceptionListService got the initial data.
  promise = waitForEvent(updateEvent, "update");

  exceptionListService.registerAndRunExceptionListObserver(
    FEATURE_SOCIAL_NAME,
    FEATURE_SOCIAL_PREF_NAME,
    obs
  );

  list = await promise;
  entries = list.testGetEntries();
  entries = entries.sort((a, b) => a.urlPattern.localeCompare(b.urlPattern));

  Assert.equal(entries.length, 2, "Has two items in the list");
  Assert.equal(
    entries[0].urlPattern,
    "*://MOZILLA.ORG/*",
    "First item is mozilla.org"
  );
  Assert.equal(
    entries[1].urlPattern,
    "*://social.example.com/*",
    "Second item is social.example.com"
  );

  // Test registering a feature after ExceptionListService recieved the synced data.
  records.push(
    {
      id: "5",
      last_modified: 1000000000000002,
      category: "baseline",
      classifierFeatures: [FEATURE_FINGERPRINTING_NAME],
      urlPattern: "*://fingerprinting.example.com/*",
    },
    {
      id: "6",
      last_modified: 1000000000000002,
      category: "convenience",
      classifierFeatures: ["other-feature"],
      urlPattern: "*://not-a-fingerprinting.example.com/*",
    },
    {
      id: "7",
      last_modified: 1000000000000002,
      category: "baseline",
      classifierFeatures: [FEATURE_FINGERPRINTING_NAME],
      urlPattern: "*://*.fingerprinting.org/*",
    }
  );

  await RemoteSettings(COLLECTION_NAME).emit("sync", {
    data: { current: records },
  });

  promise = waitForEvent(updateEvent, "update");

  exceptionListService.registerAndRunExceptionListObserver(
    FEATURE_FINGERPRINTING_NAME,
    FEATURE_FINGERPRINTING_PREF_NAME,
    obs
  );

  list = await promise;

  entries = list.testGetEntries();
  entries = entries.sort((a, b) => a.urlPattern.localeCompare(b.urlPattern));
  Assert.equal(entries.length, 2, "Has two items in the list");
  Assert.equal(
    entries[0].urlPattern,
    "*://*.fingerprinting.org/*",
    "First item is *.fingerprinting.org"
  );
  Assert.equal(
    entries[1].urlPattern,
    "*://fingerprinting.example.com/*",
    "Second item is fingerprinting.example.com"
  );

  exceptionListService.unregisterExceptionListObserver(
    FEATURE_TRACKING_NAME,
    obs
  );
  exceptionListService.unregisterExceptionListObserver(
    FEATURE_SOCIAL_NAME,
    obs
  );
  exceptionListService.unregisterExceptionListObserver(
    FEATURE_FINGERPRINTING_NAME,
    obs
  );
  exceptionListService.clear();

  await db.clear();
});
