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

add_setup(async function () {
  await SpecialPowers.pushPrefEnv({
    set: [
      ["test.aboutconfig.a", "test value 1"],
      ["test.aboutconfig.ab", "test value 2"],
      ["test.aboutconfig.bc", "test value 3"],
      ["test2.aboutconfig.dotcheck", "test value 4"],
      ["test2Xaboutconfig.dotcheck", "test value 5"],
    ],
  });
});

add_task(async function test_search() {
  await AboutConfigTest.withNewTab(async function () {
    await this.document.l10n.translateFragment(this.document.documentElement);
    let prefArray = Services.prefs.getChildList("");

    // The total number of preferences may change at any time because of
    // operations running in the background, so we only test approximately.
    // The change in count would be because of one or two added preferences,
    // but we tolerate a difference of up to 50 preferences just to be safe.
    // We want thousands of prefs instead of a few dozen that are filtered.
    Assert.greater(this.rows.length, prefArray.length - 50);

    // Filter a subset of preferences. The "browser.download." branch is
    // chosen because it is very unlikely that its preferences would be
    // modified by other code during the execution of this test.
    this.search("Wser.down   ");

    let filteredPrefArray = prefArray.filter(pref =>
      pref.includes("wser.down")
    );
    // Adding +1 to the list since button does not match an exact
    // preference name then a row is added for the user to add a
    // new button preference if desired
    Assert.equal(this.rows.length, filteredPrefArray.length + 1);

    // Show all preferences again after filtering.
    this.showAll();
    Assert.equal(this.searchInput.value, "");

    // The total number of preferences may change at any time because of
    // operations running in the background, so we only test approximately.
    // The change in count would be because of one or two added preferences,
    // but we tolerate a difference of up to 50 preferences just to be safe.
    // We want thousands of prefs instead of a few dozen that are filtered.
    Assert.greater(this.rows.length, prefArray.length - 50);

    // Check if "Only show modified" feature works.
    EventUtils.sendMouseEvent({ type: "click" }, this.showOnlyModifiedCheckbox);
    Assert.ok(this.rows.every(r => r.hasClass("has-user-value")));

    // Uncheck checkbox
    EventUtils.sendMouseEvent({ type: "click" }, this.showOnlyModifiedCheckbox);
    Assert.ok(!this.rows.every(r => r.hasClass("has-user-value")));

    // Pressing ESC while showing all preferences returns to the initial page.
    EventUtils.sendKey("escape");
    Assert.equal(this.rows.length, 0);

    // Test invalid search returns no preferences.
    // Expecting 1 row to be returned since it offers the ability to add.
    this.search("aJunkValueasdf");
    Assert.equal(this.rows.length, 1);
    // The has-visible-prefs attribute is used to style the border of the add row.
    Assert.ok(!this.prefsTable.hasAttribute("has-visible-prefs"));
    let addRow = this.getRow("aJunkValueasdf");
    Assert.equal(getComputedStyle(addRow.valueCell)["border-top-width"], "0px");

    // Pressing ESC clears the field and returns to the initial page.
    EventUtils.sendKey("escape");
    Assert.equal(this.searchInput.value, "");
    Assert.equal(this.rows.length, 0);

    // Two preferences match this filter, and one of those matches exactly.
    this.search("test.aboutconfig.a");
    Assert.equal(this.rows.length, 2);

    // When searching case insensitively, there is an additional row to add a
    // new preference with the same name but a different case.
    this.search("TEST.aboutconfig.a");
    Assert.equal(this.rows.length, 3);
    // The has-visible-prefs attribute is used to style the border of the add row.
    Assert.ok(this.prefsTable.hasAttribute("has-visible-prefs"));
    addRow = this.getRow("TEST.aboutconfig.a");
    Assert.equal(getComputedStyle(addRow.valueCell)["border-top-width"], "1px");

    // Entering an empty string returns to the initial page.
    this.search("");
    Assert.equal(this.rows.length, 0);
    Assert.ok(!this.prefsTable.hasAttribute("has-visible-prefs"));
  });
});

add_task(async function test_search_wildcard() {
  await AboutConfigTest.withNewTab(async function () {
    const extra = 1; // "Add" row

    // A trailing wildcard
    this.search("test.about*");
    Assert.equal(this.rows.length, 3 + extra);

    // A wildcard in middle
    this.search("test.about*a");
    Assert.equal(this.rows.length, 2 + extra);
    this.search("test.about*ab");
    Assert.equal(this.rows.length, 1 + extra);
    this.search("test.aboutcon*fig");
    Assert.equal(this.rows.length, 3 + extra);

    // Multiple wildcards in middle
    this.search("test.about*fig*ab");
    Assert.equal(this.rows.length, 1 + extra);
    this.search("test.about*config*ab");
    Assert.equal(this.rows.length, 1 + extra);
  });
});

add_task(async function test_search_multiword() {
  await AboutConfigTest.withNewTab(async function () {
    const extra = 1; // "Add" row

    // Two-word search term
    this.search("test. aboutconfig ");
    Assert.equal(this.rows.length, 3 + extra);

    // Two-word search term with wildcard
    this.search("aboutconfig*a  test.");
    Assert.equal(this.rows.length, 2 + extra);

    // Intersection of multiple wildcard search terms
    this.search("  test.  aboutconfig*a   aboutconfig*b ");
    Assert.equal(this.rows.length, 1 + extra);
  });
});

add_task(async function test_search_delayed() {
  await AboutConfigTest.withNewTab(async function () {
    // Start with the initial empty page.
    this.search("");

    // We need to wait more than the search typing timeout to make sure that
    // nothing happens when entering a short string.
    EventUtils.synthesizeKey("t");
    EventUtils.synthesizeKey("e");
    // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
    await new Promise(resolve => setTimeout(resolve, 500));
    Assert.equal(this.rows.length, 0);

    // Pressing Enter will force a search to occur anyways.
    EventUtils.sendKey("return");
    Assert.greater(this.rows.length, 0);

    // Prepare the table and the search field for the next test.
    this.search("test.aboutconfig.a");
    Assert.equal(this.rows.length, 2);

    // The table is updated in a single microtask, so we don't need to wait for
    // specific mutations, we can just continue when any of the children or
    // their "hidden" attributes are updated.
    let prefsTableChanged = new Promise(resolve => {
      let observer = new MutationObserver(() => {
        observer.disconnect();
        resolve();
      });
      observer.observe(this.prefsTable, { childList: true });
      for (let element of this.prefsTable.children) {
        observer.observe(element, { attributes: true });
      }
    });

    // Add a character and test that the table is not updated immediately.
    EventUtils.synthesizeKey("b");
    Assert.equal(this.rows.length, 2);

    // The table will eventually be updated after a delay.
    await prefsTableChanged;
    Assert.equal(this.rows.length, 1);
  });
});

add_task(async function test_search_add_row_color() {
  await AboutConfigTest.withNewTab(async function () {
    // When the row is the only one displayed, it doesn't have the "odd" class.
    this.search("test.aboutconfig.add");
    Assert.equal(this.rows.length, 1);
    Assert.ok(!this.getRow("test.aboutconfig.add").hasClass("odd"));

    // When displayed with one other preference, the "odd" class is present.
    this.search("test.aboutconfig.b");
    Assert.equal(this.rows.length, 2);
    Assert.ok(this.getRow("test.aboutconfig.b").hasClass("odd"));
  });
});

add_task(async function test_regex_escape() {
  await AboutConfigTest.withNewTab(async function () {
    const extra = 1; // "Add" row
    // '.' in 'test.aboutconfig' should be escaped when creating regex pattern
    this.search("test2.a*outconfig dotcheck");
    Assert.equal(this.rows.length, 1 + extra);
    Assert.ok(this.getRow("test2.aboutconfig.dotcheck"));
    Assert.ok(!this.getRow("test2Xaboutconfig.dotcheck"));
  });
});

add_task(async function test_regex_escape_invalid() {
  await AboutConfigTest.withNewTab(async function () {
    const extra = 1; // "Add" row
    // '?' should be escaped instead of throwing due to invalid regex
    this.search("?*");
    Assert.equal(this.rows.length, 0 + extra);
  });
});
