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

"use strict";

/**
 * Tests the nsIUrlClassifierExceptionList interface.
 */

const ALLOW_LIST_BASELINE_PREF =
  "privacy.trackingprotection.allow_list.baseline.enabled";
const ALLOW_LIST_CONVENIENCE_PREF =
  "privacy.trackingprotection.allow_list.convenience.enabled";

/**
 * Convert a JS object from RemoteSettings to an nsIUrlClassifierExceptionListEntry.
 * Copied from UrlClassifierExceptionListService.sys.mjs with modifications.
 *
 * @param {object} rsObject - The JS object from RemoteSettings to convert.
 * @returns {nsIUrlClassifierExceptionListEntry} The converted nsIUrlClassifierExceptionListEntry.
 */
function rsObjectToEntry(rsObject) {
  let entry = Cc[
    "@mozilla.org/url-classifier/exception-list-entry;1"
  ].createInstance(Ci.nsIUrlClassifierExceptionListEntry);

  let {
    category: categoryStr,
    urlPattern,
    topLevelUrlPattern = "",
    isPrivateBrowsingOnly = false,
    filterContentBlockingCategories = [],
    classifierFeatures = [],
  } = rsObject;

  const CATEGORY_STR_TO_ENUM = {
    "internal-pref":
      Ci.nsIUrlClassifierExceptionListEntry.CATEGORY_INTERNAL_PREF,
    baseline: Ci.nsIUrlClassifierExceptionListEntry.CATEGORY_BASELINE,
    convenience: Ci.nsIUrlClassifierExceptionListEntry.CATEGORY_CONVENIENCE,
  };

  let category = CATEGORY_STR_TO_ENUM[categoryStr];

  entry.init(
    category,
    urlPattern,
    topLevelUrlPattern,
    isPrivateBrowsingOnly,
    filterContentBlockingCategories,
    classifierFeatures
  );

  return entry;
}

/**
 * Test the exception list with manually imported allow-list entries.
 */
add_task(async function test_exception_list_lookups() {
  let list = Cc["@mozilla.org/url-classifier/exception-list;1"].createInstance(
    Ci.nsIUrlClassifierExceptionList
  );

  Assert.ok(
    !list.matches(
      Services.io.newURI("https://tracker.com/"),
      Services.io.newURI("https://example.com/"),
      false
    ),
    "Exception list with no entries should not match tracker.com"
  );

  // An entry that allow-lists tracker.com under example.com.
  list.addEntry(
    rsObjectToEntry({
      category: "internal-pref",
      urlPattern: "*://tracker.com/*",
      topLevelUrlPattern: "*://example.com/*",
    })
  );

  // An entry that allow-lists *.sub.tracker.com under *.example.org.
  list.addEntry(
    rsObjectToEntry({
      category: "internal-pref",
      urlPattern: "*://*.sub.tracker.com/*",
      topLevelUrlPattern: "*://*.example.org/*",
    })
  );

  // An entry that allow-lists tracker.org globally.
  list.addEntry(
    rsObjectToEntry({
      category: "internal-pref",
      urlPattern: "*://tracker.org/*",
    })
  );
  // An entry that allow-lists foo.tracker.org globally.
  list.addEntry(
    rsObjectToEntry({
      category: "internal-pref",
      urlPattern: "*://foo.tracker.org/*",
    })
  );
  // An entry that allow-lists foo.bar.tracker.org globally.
  list.addEntry(
    rsObjectToEntry({
      category: "internal-pref",
      urlPattern: "*://foo.bar.tracker.org/*",
    })
  );

  Assert.ok(
    list.matches(
      Services.io.newURI("https://tracker.com/bar"),
      Services.io.newURI("https://example.com/foo"),
      false
    ),
    "Exception list should match tracker.com under example.com."
  );

  Assert.ok(
    !list.matches(
      Services.io.newURI("https://tracker.com/bar"),
      Services.io.newURI("https://example.org/foo"),
      false
    ),
    "Exception list should not match tracker.com under example.org."
  );

  Assert.ok(
    list.matches(
      Services.io.newURI("https://foo.sub.tracker.com/bar"),
      Services.io.newURI("https://foo.bar.example.org/foo/bar"),
      false
    ),
    "Exception list should match foo.sub.tracker.com under foo.bar.example.org."
  );

  Assert.ok(
    list.matches(
      Services.io.newURI("https://tracker.org/bar"),
      Services.io.newURI("https://mozilla.org/foo/bar"),
      false
    ),
    "Exception list should match tracker.org under mozilla.org."
  );

  Assert.ok(
    list.matches(
      Services.io.newURI("https://foo.bar.tracker.org/bar"),
      Services.io.newURI("https://mozilla.org/foo/bar"),
      false
    ),
    "Exception list should match foo.bar.tracker.org under mozilla.org."
  );

  Assert.ok(
    !list.matches(
      Services.io.newURI("https://sub.foo.bar.tracker.org/bar"),
      Services.io.newURI("https://mozilla.org/foo/bar"),
      false
    ),
    "Exception list should not match sub.foo.bar.tracker.org under mozilla.org."
  );
});

/**
 * Test the exception list with allow-list categories enabled/disabled via prefs.
 */
add_task(async function test_exception_list_lookups_with_category_prefs() {
  let list = Cc["@mozilla.org/url-classifier/exception-list;1"].createInstance(
    Ci.nsIUrlClassifierExceptionList
  );

  // An entry that allow-lists tracker.com.
  list.addEntry(
    rsObjectToEntry({
      category: "internal-pref",
      urlPattern: "*://tracker.com/*",
    })
  );

  // An entry that allow-lists foo.tracker.com for the baseline category.
  list.addEntry(
    rsObjectToEntry({
      category: "baseline",
      urlPattern: "*://foo.tracker.com/*",
    })
  );

  // An entry that allow-lists bar.tracker.com for the convenience category.
  list.addEntry(
    rsObjectToEntry({
      category: "convenience",
      urlPattern: "*://bar.tracker.com/*",
    })
  );

  info("Start with both allow-list categories disabled.");
  Services.prefs.setBoolPref(ALLOW_LIST_BASELINE_PREF, false);
  Services.prefs.setBoolPref(ALLOW_LIST_CONVENIENCE_PREF, false);

  Assert.ok(
    list.matches(
      Services.io.newURI("https://tracker.com/bar"),
      Services.io.newURI("https://example.org/foo"),
      false
    ),
    "Exception list should match tracker.com when both allow-list categories are disabled."
  );

  Assert.ok(
    !list.matches(
      Services.io.newURI("https://foo.tracker.com/bar"),
      Services.io.newURI("https://example.org/foo"),
      false
    ),
    "Exception list should not match foo.tracker.com because baseline allow-list is disabled."
  );

  Assert.ok(
    !list.matches(
      Services.io.newURI("https://bar.tracker.com/bar"),
      Services.io.newURI("https://example.org/foo"),
      false
    ),
    "Exception list should not match bar.tracker.com because convenience allow-list is disabled."
  );

  info("Enable only the baseline allow-list.");
  Services.prefs.setBoolPref(ALLOW_LIST_BASELINE_PREF, true);
  Services.prefs.setBoolPref(ALLOW_LIST_CONVENIENCE_PREF, false);

  Assert.ok(
    list.matches(
      Services.io.newURI("https://tracker.com/bar"),
      Services.io.newURI("https://example.org/foo"),
      false
    ),
    "Exception list should match tracker.com when only the baseline allow-list is enabled."
  );

  Assert.ok(
    list.matches(
      Services.io.newURI("https://foo.tracker.com/bar"),
      Services.io.newURI("https://example.org/foo"),
      false
    ),
    "Exception list should match foo.tracker.com because baseline allow-list is enabled."
  );

  Assert.ok(
    !list.matches(
      Services.io.newURI("https://bar.tracker.com/bar"),
      Services.io.newURI("https://example.org/foo"),
      false
    ),
    "Exception list should not match bar.tracker.com because convenience allow-list is disabled."
  );

  info("Enable both allow-list categories.");
  Services.prefs.setBoolPref(ALLOW_LIST_BASELINE_PREF, true);
  Services.prefs.setBoolPref(ALLOW_LIST_CONVENIENCE_PREF, true);

  Assert.ok(
    list.matches(
      Services.io.newURI("https://tracker.com/bar"),
      Services.io.newURI("https://example.org/foo"),
      false
    ),
    "Exception list should match tracker.com when both allow-list categories are enabled."
  );

  Assert.ok(
    list.matches(
      Services.io.newURI("https://foo.tracker.com/bar"),
      Services.io.newURI("https://example.org/foo"),
      false
    ),
    "Exception list should match foo.tracker.com because baseline allow-list is enabled."
  );

  Assert.ok(
    list.matches(
      Services.io.newURI("https://bar.tracker.com/bar"),
      Services.io.newURI("https://example.org/foo"),
      false
    ),
    "Exception list should not match bar.tracker.com because convenience allow-list is enabled."
  );

  info(
    "Enable only the convenience allow-list. Convenience should not apply because baseline is disabled."
  );
  Services.prefs.setBoolPref(ALLOW_LIST_BASELINE_PREF, false);
  Services.prefs.setBoolPref(ALLOW_LIST_CONVENIENCE_PREF, true);

  Assert.ok(
    !list.matches(
      Services.io.newURI("https://foo.tracker.com/bar"),
      Services.io.newURI("https://example.org/foo"),
      false
    ),
    "Exception list should not match foo.tracker.com because baseline allow-list is disabled."
  );

  Assert.ok(
    !list.matches(
      Services.io.newURI("https://bar.tracker.com/bar"),
      Services.io.newURI("https://example.org/foo"),
      false
    ),
    "Exception list should not match bar.tracker.com even when convenience allow-list is enabled because baseline is disabled."
  );

  Services.prefs.clearUserPref(ALLOW_LIST_BASELINE_PREF);
  Services.prefs.clearUserPref(ALLOW_LIST_CONVENIENCE_PREF);
});

/**
 * Test exception list allows specifying URL patterns that match more
 * than a site.
 */
add_task(async function test_exception_site_specific_pattern() {
  let list = Cc["@mozilla.org/url-classifier/exception-list;1"].createInstance(
    Ci.nsIUrlClassifierExceptionList
  );

  info("Add multi-layer public suffix list entry to exception");
  list.addEntry(
    rsObjectToEntry({
      category: "internal-pref",
      urlPattern: "*://*.co.uk/*",
      topLevelUrlPattern: "*://example.com/*",
    })
  );

  info("Add normal tld to exception");
  list.addEntry(
    rsObjectToEntry({
      category: "internal-pref",
      urlPattern: "*://*.org/*",
      topLevelUrlPattern: "*://*.example.com/*",
    })
  );

  Assert.ok(
    list.matches(
      Services.io.newURI("https://tracker.org/bar"),
      Services.io.newURI("https://example.com/foo"),
      false
    ),
    "Exception list should match tracker.org under example.com."
  );
  Assert.ok(
    list.matches(
      Services.io.newURI("https://sub.tracker.co.uk/bar"),
      Services.io.newURI("https://example.com/foo"),
      false
    ),
    "Exception list should match sub.tracker.co.uk under example.com."
  );
});
