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

const TEST_PAGE_BASENAME = "contentSearchUI.html";

const TEST_ENGINE1 = {
  name: "searchSuggestionEngine1",
  id: "other-searchSuggestionEngine1",
  loadPath: "[addon]searchsuggestionengine1@tests.mozilla.org",
};
const TEST_ENGINE2 = {
  name: "searchSuggestionEngine2",
  id: "other-searchSuggestionEngine2",
  loadPath: "[addon]searchsuggestionengine2@tests.mozilla.org",
};

const TEST_MSG = "ContentSearchUIControllerTest";

ChromeUtils.defineESModuleGetters(this, {
  ContentSearch: "resource:///actors/ContentSearchParent.sys.mjs",
  FormHistoryTestUtils:
    "resource://testing-common/FormHistoryTestUtils.sys.mjs",
  SearchSuggestionController:
    "resource://gre/modules/SearchSuggestionController.sys.mjs",
});

const pageURL = getRootDirectory(gTestPath) + TEST_PAGE_BASENAME;
BrowserTestUtils.registerAboutPage(
  registerCleanupFunction,
  "test-about-content-search-ui",
  pageURL,
  Ci.nsIAboutModule.URI_SAFE_FOR_UNTRUSTED_CONTENT |
    Ci.nsIAboutModule.URI_MUST_LOAD_IN_CHILD |
    Ci.nsIAboutModule.ALLOW_SCRIPT |
    Ci.nsIAboutModule.URI_CAN_LOAD_IN_PRIVILEGEDABOUT_PROCESS
);

requestLongerTimeout(2);

function waitForSuggestions() {
  return SpecialPowers.spawn(gBrowser.selectedBrowser, [], () =>
    ContentTaskUtils.waitForCondition(
      () =>
        Cu.waiveXrays(content).gController.input.getAttribute(
          "aria-expanded"
        ) == "true",
      "Waiting for suggestions",
      200 // Increased interval to support long textruns.
    )
  );
}

async function waitForSearch() {
  await BrowserTestUtils.waitForContentEvent(
    gBrowser.selectedBrowser,
    "ContentSearchClient",
    true,
    event => {
      if (event.detail.type == "Search") {
        event.target._eventDetail = event.detail.data;
        return true;
      }
      return false;
    },
    true
  );
  return SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
    let eventDetail = content._eventDetail;
    delete content._eventDetail;
    return eventDetail;
  });
}

async function waitForSearchSettings() {
  await BrowserTestUtils.waitForContentEvent(
    gBrowser.selectedBrowser,
    "ContentSearchClient",
    true,
    event => {
      if (event.detail.type == "ManageEngines") {
        event.target._eventDetail = event.detail.data;
        return true;
      }
      return false;
    },
    true
  );
  return SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
    let eventDetail = content._eventDetail;
    delete content._eventDetail;
    return eventDetail;
  });
}

function getCurrentState() {
  return SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
    let controller = Cu.waiveXrays(content).gController;
    let state = {
      selectedIndex: controller.selectedIndex,
      selectedButtonIndex: controller.selectedButtonIndex,
      numSuggestions: controller._table.hidden ? 0 : controller.numSuggestions,
      suggestionAtIndex: [],
      isFormHistorySuggestionAtIndex: [],

      tableHidden: controller._table.hidden,

      inputValue: controller.input.value,
      ariaExpanded: controller.input.getAttribute("aria-expanded"),
    };

    if (state.numSuggestions) {
      for (let i = 0; i < controller.numSuggestions; i++) {
        state.suggestionAtIndex.push(controller.suggestionAtIndex(i));
        state.isFormHistorySuggestionAtIndex.push(
          controller.isFormHistorySuggestionAtIndex(i)
        );
      }
    }

    return state;
  });
}

async function msg(type, data = null) {
  switch (type) {
    case "reset":
      // Reset both the input and suggestions by select all + delete. If there was
      // no text entered, this won't have any effect, so also escape to ensure the
      // suggestions table is closed.
      await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
        Cu.waiveXrays(content).gController.input.focus();
        EventUtils.synthesizeKey("a", { accelKey: true }, content);
        EventUtils.synthesizeKey("KEY_Delete", {}, content);
        EventUtils.synthesizeKey("KEY_Escape", {}, content);
      });
      break;

    case "key": {
      let keyName = typeof data == "string" ? data : data.key;
      await BrowserTestUtils.synthesizeKey(
        keyName,
        data.modifiers || {},
        gBrowser.selectedBrowser
      );
      if (data?.waitForSuggestions) {
        await waitForSuggestions();
      }
      break;
    }
    case "text": {
      await SpecialPowers.spawn(
        gBrowser.selectedBrowser,
        [data.value],
        text => {
          Cu.waiveXrays(content).gController.input.value = text.substring(
            0,
            text.length - 1
          );
          EventUtils.synthesizeKey(
            text.substring(text.length - 1),
            {},
            content
          );
        }
      );
      if (data?.waitForSuggestions) {
        await waitForSuggestions();
      }
      break;
    }
    case "startComposition":
      await BrowserTestUtils.synthesizeComposition(
        "compositionstart",
        gBrowser.selectedBrowser
      );
      break;
    case "changeComposition": {
      await BrowserTestUtils.synthesizeCompositionChange(
        {
          composition: {
            string: data.data,
            clauses: [
              {
                length: data.length,
                attr: Ci.nsITextInputProcessor.ATTR_RAW_CLAUSE,
              },
            ],
          },
          caret: { start: data.length, length: 0 },
        },
        gBrowser.selectedBrowser
      );
      if (data?.waitForSuggestions) {
        await waitForSuggestions();
      }
      break;
    }
    case "commitComposition":
      await BrowserTestUtils.synthesizeComposition(
        "compositioncommitasis",
        gBrowser.selectedBrowser
      );
      break;
    case "mousemove":
    case "click": {
      let event;
      let index;
      if (type == "mousemove") {
        event = {
          type: "mousemove",
          clickcount: 0,
        };
        index = data;
      } else {
        event = data.modifiers || null;
        index = data.eltIdx;
      }

      await SpecialPowers.spawn(
        gBrowser.selectedBrowser,
        [type, event, index],
        (eventType, eventArgs, itemIndex) => {
          let controller = Cu.waiveXrays(content).gController;
          return new Promise(resolve => {
            let row;
            if (itemIndex == -1) {
              row = controller._table.firstChild;
            } else {
              let allElts = [
                ...controller._suggestionsList.children,
                ...controller._oneOffButtons,
                content.document.getElementById("contentSearchSettingsButton"),
              ];
              row = allElts[itemIndex];
            }
            row.addEventListener(eventType, () => resolve(), { once: true });
            EventUtils.synthesizeMouseAtCenter(row, eventArgs, content);
          });
        }
      );
      break;
    }
  }

  return getCurrentState();
}

/**
 * Focusses the in-content search bar.
 *
 * @returns {Promise}
 *   A promise that is resolved once the focus is complete.
 */
function focusContentSearchBar() {
  return SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
    Cu.waiveXrays(content).input.focus();
  });
}

let extension1;
let extension2;

add_setup(async function () {
  let originalOnMessageSearch = ContentSearch._onMessageSearch;
  let originalOnMessageManageEngines = ContentSearch._onMessageManageEngines;

  ContentSearch._onMessageSearch = () => {};
  ContentSearch._onMessageManageEngines = () => {};

  let currentEngines = await Services.search.getVisibleEngines();

  extension1 = await SearchTestUtils.installSearchExtension(
    {
      name: TEST_ENGINE1.name,
      suggest_url:
        "https://example.com/browser/browser/components/search/test/browser/searchSuggestionEngine.sjs",
      suggest_url_get_params: "query={searchTerms}",
    },
    { setAsDefault: true }
  );
  extension2 = await SearchTestUtils.installSearchExtension({
    name: TEST_ENGINE2.name,
    suggest_url:
      "https://example.com/browser/browser/components/search/test/browser/searchSuggestionEngine.sjs",
    suggest_url_get_params: "query={searchTerms}",
  });

  for (let engine of currentEngines) {
    await Services.search.removeEngine(engine);
  }

  registerCleanupFunction(async () => {
    ContentSearch._onMessageSearch = originalOnMessageSearch;
    ContentSearch._onMessageManageEngines = originalOnMessageManageEngines;
  });

  await promiseTab();
});

add_task(async function emptyInput() {
  await focusContentSearchBar();

  let state = await msg("key", { key: "x", waitForSuggestions: true });
  checkState(state, "x", ["xfoo", "xbar"], -1);

  state = await msg("key", "VK_BACK_SPACE");
  checkState(state, "", [], -1);

  await msg("reset");
});

add_task(async function blur() {
  await focusContentSearchBar();

  let state = await msg("key", { key: "x", waitForSuggestions: true });
  checkState(state, "x", ["xfoo", "xbar"], -1);

  await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
    Cu.waiveXrays(content).gController.input.blur();
  });
  state = await getCurrentState();
  checkState(state, "x", [], -1);

  await msg("reset");
});

add_task(async function upDownKeys() {
  await focusContentSearchBar();

  let state = await msg("key", { key: "x", waitForSuggestions: true });
  checkState(state, "x", ["xfoo", "xbar"], -1);

  // Cycle down the suggestions starting from no selection.
  state = await msg("key", "VK_DOWN");
  checkState(state, "xfoo", ["xfoo", "xbar"], 0);

  state = await msg("key", "VK_DOWN");
  checkState(state, "xbar", ["xfoo", "xbar"], 1);

  state = await msg("key", "VK_DOWN");
  checkState(state, "x", ["xfoo", "xbar"], 2);

  state = await msg("key", "VK_DOWN");
  checkState(state, "x", ["xfoo", "xbar"], 3);

  state = await msg("key", "VK_DOWN");
  checkState(state, "x", ["xfoo", "xbar"], -1);

  // Cycle up starting from no selection.
  state = await msg("key", "VK_UP");
  checkState(state, "x", ["xfoo", "xbar"], 3);

  state = await msg("key", "VK_UP");
  checkState(state, "x", ["xfoo", "xbar"], 2);

  state = await msg("key", "VK_UP");
  checkState(state, "xbar", ["xfoo", "xbar"], 1);

  state = await msg("key", "VK_UP");
  checkState(state, "xfoo", ["xfoo", "xbar"], 0);

  state = await msg("key", "VK_UP");
  checkState(state, "x", ["xfoo", "xbar"], -1);

  await msg("reset");
});

add_task(async function rightLeftKeys() {
  await focusContentSearchBar();

  let state = await msg("key", { key: "x", waitForSuggestions: true });
  checkState(state, "x", ["xfoo", "xbar"], -1);

  state = await msg("key", "VK_LEFT");
  checkState(state, "x", ["xfoo", "xbar"], -1);

  state = await msg("key", "VK_LEFT");
  checkState(state, "x", ["xfoo", "xbar"], -1);

  state = await msg("key", "VK_RIGHT");
  checkState(state, "x", ["xfoo", "xbar"], -1);

  state = await msg("key", "VK_RIGHT");
  checkState(state, "x", [], -1);

  state = await msg("key", { key: "VK_DOWN", waitForSuggestions: true });
  checkState(state, "x", ["xfoo", "xbar"], -1);

  state = await msg("key", "VK_DOWN");
  checkState(state, "xfoo", ["xfoo", "xbar"], 0);

  // This should make the xfoo suggestion sticky.  To make sure it sticks,
  // trigger suggestions again and cycle through them by pressing Down until
  // nothing is selected again.
  state = await msg("key", "VK_RIGHT");
  checkState(state, "xfoo", [], -1);

  state = await msg("key", { key: "VK_DOWN", waitForSuggestions: true });
  checkState(state, "xfoo", ["xfoofoo", "xfoobar"], -1);

  state = await msg("key", "VK_DOWN");
  checkState(state, "xfoofoo", ["xfoofoo", "xfoobar"], 0);

  state = await msg("key", "VK_DOWN");
  checkState(state, "xfoobar", ["xfoofoo", "xfoobar"], 1);

  state = await msg("key", "VK_DOWN");
  checkState(state, "xfoo", ["xfoofoo", "xfoobar"], 2);

  state = await msg("key", "VK_DOWN");
  checkState(state, "xfoo", ["xfoofoo", "xfoobar"], 3);

  state = await msg("key", "VK_DOWN");
  checkState(state, "xfoo", ["xfoofoo", "xfoobar"], -1);

  await msg("reset");
});

add_task(async function tabKey() {
  await focusContentSearchBar();
  await msg("key", { key: "x", waitForSuggestions: true });

  let state = await msg("key", "VK_TAB");
  checkState(state, "x", ["xfoo", "xbar"], 2);

  state = await msg("key", "VK_TAB");
  checkState(state, "x", ["xfoo", "xbar"], 3);

  state = await msg("key", { key: "VK_TAB", modifiers: { shiftKey: true } });
  checkState(state, "x", ["xfoo", "xbar"], 2);

  state = await msg("key", { key: "VK_TAB", modifiers: { shiftKey: true } });
  checkState(state, "x", [], -1);

  await focusContentSearchBar();

  await msg("key", { key: "VK_DOWN", waitForSuggestions: true });

  for (let i = 0; i < 3; ++i) {
    state = await msg("key", "VK_TAB");
  }
  checkState(state, "x", [], -1);

  await focusContentSearchBar();

  await msg("key", { key: "VK_DOWN", waitForSuggestions: true });
  state = await msg("key", "VK_DOWN");
  checkState(state, "xfoo", ["xfoo", "xbar"], 0);

  state = await msg("key", "VK_TAB");
  checkState(state, "xfoo", ["xfoo", "xbar"], 0, 0);

  state = await msg("key", "VK_TAB");
  checkState(state, "xfoo", ["xfoo", "xbar"], 0, 1);

  state = await msg("key", "VK_DOWN");
  checkState(state, "xbar", ["xfoo", "xbar"], 1, 1);

  state = await msg("key", "VK_DOWN");
  checkState(state, "x", ["xfoo", "xbar"], 2);

  state = await msg("key", "VK_UP");
  checkState(state, "xbar", ["xfoo", "xbar"], 1);

  state = await msg("key", "VK_TAB");
  checkState(state, "xbar", ["xfoo", "xbar"], 1, 0);

  state = await msg("key", "VK_TAB");
  checkState(state, "xbar", ["xfoo", "xbar"], 1, 1);

  state = await msg("key", "VK_TAB");
  checkState(state, "xbar", [], -1);

  await msg("reset");
});

add_task(async function cycleSuggestions() {
  await focusContentSearchBar();
  await msg("key", { key: "x", waitForSuggestions: true });

  let cycle = async function (aSelectedButtonIndex) {
    let modifiers = {
      shiftKey: true,
      accelKey: true,
    };

    let state = await msg("key", { key: "VK_DOWN", modifiers });
    checkState(state, "xfoo", ["xfoo", "xbar"], 0, aSelectedButtonIndex);

    state = await msg("key", { key: "VK_DOWN", modifiers });
    checkState(state, "xbar", ["xfoo", "xbar"], 1, aSelectedButtonIndex);

    state = await msg("key", { key: "VK_DOWN", modifiers });
    checkState(state, "x", ["xfoo", "xbar"], -1, aSelectedButtonIndex);

    state = await msg("key", { key: "VK_DOWN", modifiers });
    checkState(state, "xfoo", ["xfoo", "xbar"], 0, aSelectedButtonIndex);

    state = await msg("key", { key: "VK_UP", modifiers });
    checkState(state, "x", ["xfoo", "xbar"], -1, aSelectedButtonIndex);

    state = await msg("key", { key: "VK_UP", modifiers });
    checkState(state, "xbar", ["xfoo", "xbar"], 1, aSelectedButtonIndex);

    state = await msg("key", { key: "VK_UP", modifiers });
    checkState(state, "xfoo", ["xfoo", "xbar"], 0, aSelectedButtonIndex);

    state = await msg("key", { key: "VK_UP", modifiers });
    checkState(state, "x", ["xfoo", "xbar"], -1, aSelectedButtonIndex);
  };

  await cycle();

  // Repeat with a one-off selected.
  let state = await msg("key", "VK_TAB");
  checkState(state, "x", ["xfoo", "xbar"], 2);
  await cycle(0);

  // Repeat with the settings button selected.
  state = await msg("key", "VK_TAB");
  checkState(state, "x", ["xfoo", "xbar"], 3);
  await cycle(1);

  await msg("reset");
});

add_task(async function cycleOneOffs() {
  await focusContentSearchBar();
  await msg("key", { key: "x", waitForSuggestions: true });

  await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
    let btn =
      Cu.waiveXrays(content).gController._oneOffButtons[
        Cu.waiveXrays(content).gController._oneOffButtons.length - 1
      ];
    let newBtn = btn.cloneNode(true);
    btn.parentNode.appendChild(newBtn);
    Cu.waiveXrays(content).gController._oneOffButtons.push(newBtn);
  });

  let state = await msg("key", "VK_DOWN");
  state = await msg("key", "VK_DOWN");
  checkState(state, "xbar", ["xfoo", "xbar"], 1);

  let modifiers = {
    altKey: true,
  };

  state = await msg("key", { key: "VK_DOWN", modifiers });
  checkState(state, "xbar", ["xfoo", "xbar"], 1, 0);

  state = await msg("key", { key: "VK_DOWN", modifiers });
  checkState(state, "xbar", ["xfoo", "xbar"], 1, 1);

  state = await msg("key", { key: "VK_DOWN", modifiers });
  checkState(state, "xbar", ["xfoo", "xbar"], 1);

  state = await msg("key", { key: "VK_UP", modifiers });
  checkState(state, "xbar", ["xfoo", "xbar"], 1, 1);

  state = await msg("key", { key: "VK_UP", modifiers });
  checkState(state, "xbar", ["xfoo", "xbar"], 1, 0);

  state = await msg("key", { key: "VK_UP", modifiers });
  checkState(state, "xbar", ["xfoo", "xbar"], 1);

  // If the settings button is selected, pressing alt+up/down should select the
  // last/first one-off respectively (and deselect the settings button).
  await msg("key", "VK_TAB");
  await msg("key", "VK_TAB");
  state = await msg("key", "VK_TAB"); // Settings button selected.
  checkState(state, "xbar", ["xfoo", "xbar"], 1, 2);

  state = await msg("key", { key: "VK_UP", modifiers });
  checkState(state, "xbar", ["xfoo", "xbar"], 1, 1);

  state = await msg("key", "VK_TAB");
  checkState(state, "xbar", ["xfoo", "xbar"], 1, 2);

  state = await msg("key", { key: "VK_DOWN", modifiers });
  checkState(state, "xbar", ["xfoo", "xbar"], 1, 0);

  await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
    Cu.waiveXrays(content).gController._oneOffButtons.pop().remove();
  });
  await msg("reset");
});

add_task(async function mouse() {
  await focusContentSearchBar();

  let state = await msg("key", { key: "x", waitForSuggestions: true });
  checkState(state, "x", ["xfoo", "xbar"], -1);

  state = await msg("mousemove", 0);
  checkState(state, "x", ["xfoo", "xbar"], 0);

  state = await msg("mousemove", 1);
  checkState(state, "x", ["xfoo", "xbar"], 1);

  state = await msg("mousemove", 2);
  checkState(state, "x", ["xfoo", "xbar"], 2, 0);

  state = await msg("mousemove", 3);
  checkState(state, "x", ["xfoo", "xbar"], 3, 1);

  state = await msg("mousemove", -1);
  checkState(state, "x", ["xfoo", "xbar"], -1);

  await msg("reset");
  await focusContentSearchBar();

  state = await msg("key", { key: "x", waitForSuggestions: true });
  checkState(state, "x", ["xfoo", "xbar"], -1);

  state = await msg("mousemove", 0);
  checkState(state, "x", ["xfoo", "xbar"], 0);

  state = await msg("mousemove", 2);
  checkState(state, "x", ["xfoo", "xbar"], 2, 0);

  state = await msg("mousemove", -1);
  checkState(state, "x", ["xfoo", "xbar"], -1);

  await msg("reset");
});

add_task(async function formHistory() {
  await focusContentSearchBar();

  // Type an X and add it to form history.
  let state = await msg("key", { key: "x", waitForSuggestions: true });
  checkState(state, "x", ["xfoo", "xbar"], -1);
  // Wait for Satchel to say it's been added to form history.
  let observePromise = new Promise(resolve => {
    Services.obs.addObserver(function onAdd(subj, topic, data) {
      if (data == "formhistory-add") {
        Services.obs.removeObserver(onAdd, "satchel-storage-changed");
        executeSoon(resolve);
      }
    }, "satchel-storage-changed");
  });

  await FormHistoryTestUtils.clear("searchbar-history");
  let entry = await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
    return Cu.waiveXrays(content).gController.addInputValueToFormHistory();
  });
  await observePromise;
  Assert.greater(
    await FormHistoryTestUtils.count("searchbar-history", {
      source: entry.source,
    }),
    0
  );

  // Reset the input.
  state = await msg("reset");
  checkState(state, "", [], -1);

  // Type an X again.  The form history entry should appear.
  state = await msg("key", { key: "x", waitForSuggestions: true });
  checkState(
    state,
    "x",
    [{ str: "x", type: "formHistory" }, "xfoo", "xbar"],
    -1
  );

  // Select the form history entry and delete it.
  state = await msg("key", "VK_DOWN");
  checkState(
    state,
    "x",
    [{ str: "x", type: "formHistory" }, "xfoo", "xbar"],
    0
  );

  // Wait for Satchel.
  observePromise = new Promise(resolve => {
    Services.obs.addObserver(function onRemove(subj, topic, data) {
      if (data == "formhistory-remove") {
        Services.obs.removeObserver(onRemove, "satchel-storage-changed");
        executeSoon(resolve);
      }
    }, "satchel-storage-changed");
  });
  state = await msg("key", "VK_DELETE");
  checkState(state, "x", ["xfoo", "xbar"], -1);

  await observePromise;

  // Reset the input.
  state = await msg("reset");
  checkState(state, "", [], -1);

  // Type an X again.  The form history entry should still be gone.
  state = await msg("key", { key: "x", waitForSuggestions: true });
  checkState(state, "x", ["xfoo", "xbar"], -1);

  await msg("reset");
});

add_task(async function formHistory_limit() {
  info("Check long strings are not added to form history");
  await focusContentSearchBar();
  const gLongString = new Array(
    SearchSuggestionController.SEARCH_HISTORY_MAX_VALUE_LENGTH + 1
  )
    .fill("x")
    .join("");
  // Type and confirm a very long string.
  let state = await msg("text", {
    value: gLongString,
    waitForSuggestions: true,
  });
  checkState(
    state,
    gLongString,
    [`${gLongString}foo`, `${gLongString}bar`],
    -1
  );

  await FormHistoryTestUtils.clear("searchbar-history");
  let entry = await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
    return Cu.waiveXrays(content).gController.addInputValueToFormHistory();
  });
  // There's nothing we can wait for, since addition should not be happening.
  /* eslint-disable mozilla/no-arbitrary-setTimeout */
  await new Promise(resolve => setTimeout(resolve, 500));
  Assert.equal(
    await FormHistoryTestUtils.count("searchbar-history", {
      source: entry.source,
    }),
    0
  );

  await msg("reset");
});

add_task(async function cycleEngines() {
  await focusContentSearchBar();
  await msg("key", { key: "VK_DOWN", waitForSuggestions: true });

  Services.telemetry.clearEvents();
  Services.fog.testResetFOG();

  let p = SearchTestUtils.promiseSearchNotification(
    "engine-default",
    "browser-search-engine-modified"
  );
  await msg("key", { key: "VK_DOWN", modifiers: { accelKey: true } });
  let newEngine = await p;
  Assert.equal(
    newEngine.name,
    TEST_ENGINE2.name,
    "Should have correctly cycled the engine"
  );
  TelemetryTestUtils.assertEvents(
    [
      {
        object: "change_default",
        value: "user_searchbar",
        extra: {
          prev_id: TEST_ENGINE1.id,
          new_id: TEST_ENGINE2.id,
          new_name: TEST_ENGINE2.name,
          new_load_path: TEST_ENGINE2.loadPath,
          new_sub_url: "",
        },
      },
    ],
    { category: "search", method: "engine" }
  );

  let snapshot = await Glean.searchEngineDefault.changed.testGetValue();
  delete snapshot[0].timestamp;
  Assert.deepEqual(
    snapshot[0],
    {
      category: "search.engine.default",
      name: "changed",
      extra: {
        new_load_path: TEST_ENGINE2.loadPath,
        previous_engine_id: TEST_ENGINE1.id,
        change_source: "user_searchbar",
        new_engine_id: TEST_ENGINE2.id,
        new_display_name: TEST_ENGINE2.name,
        new_submission_url: "",
      },
    },
    "Should have received the correct event details"
  );

  p = SearchTestUtils.promiseSearchNotification(
    "engine-default",
    "browser-search-engine-modified"
  );
  await msg("key", { key: "VK_UP", modifiers: { accelKey: true } });
  newEngine = await p;
  Assert.equal(
    newEngine.name,
    TEST_ENGINE1.name,
    "Should have correctly cycled the engine"
  );

  TelemetryTestUtils.assertEvents(
    [
      {
        object: "change_default",
        value: "user_searchbar",
        extra: {
          prev_id: TEST_ENGINE2.id,
          new_id: TEST_ENGINE1.id,
          new_name: TEST_ENGINE1.name,
          new_load_path: TEST_ENGINE1.loadPath,
          new_sub_url: "",
        },
      },
    ],
    { category: "search", method: "engine" }
  );

  snapshot = await Glean.searchEngineDefault.changed.testGetValue();
  delete snapshot[1].timestamp;
  Assert.deepEqual(
    snapshot[1],
    {
      category: "search.engine.default",
      name: "changed",
      extra: {
        new_load_path: TEST_ENGINE1.loadPath,
        previous_engine_id: TEST_ENGINE2.id,
        change_source: "user_searchbar",
        new_engine_id: TEST_ENGINE1.id,
        new_display_name: TEST_ENGINE1.name,
        new_submission_url: "",
      },
    },
    "Should have received the correct event details"
  );

  await msg("reset");
});

add_task(async function search() {
  await focusContentSearchBar();

  let modifiers = {};
  ["altKey", "ctrlKey", "metaKey", "shiftKey"].forEach(
    k => (modifiers[k] = true)
  );

  // Test typing a query and pressing enter.
  let p = waitForSearch();
  await msg("key", { key: "x", waitForSuggestions: true });
  await msg("key", { key: "VK_RETURN", modifiers });
  let mesg = await p;
  let eventData = {
    engineName: TEST_ENGINE1.name,
    searchString: "x",
    healthReportKey: "test",
    searchPurpose: "test",
    originalEvent: modifiers,
  };
  SimpleTest.isDeeply(eventData, mesg, "Search event data");

  await promiseTab();
  await focusContentSearchBar();

  // Test typing a query, then selecting a suggestion and pressing enter.
  p = waitForSearch();
  await msg("key", { key: "x", waitForSuggestions: true });
  await msg("key", "VK_DOWN");
  await msg("key", "VK_DOWN");
  await msg("key", { key: "VK_RETURN", modifiers });
  mesg = await p;
  eventData.searchString = "xfoo";
  eventData.engineName = TEST_ENGINE1.name;
  eventData.selection = {
    index: 1,
    kind: "key",
  };
  SimpleTest.isDeeply(eventData, mesg, "Search event data");

  await promiseTab();
  await focusContentSearchBar();

  // Test typing a query, then selecting a one-off button and pressing enter.
  p = waitForSearch();
  await msg("key", { key: "x", waitForSuggestions: true });
  await msg("key", "VK_UP");
  await msg("key", "VK_UP");
  await msg("key", { key: "VK_RETURN", modifiers });
  mesg = await p;
  delete eventData.selection;
  eventData.searchString = "x";
  eventData.engineName = TEST_ENGINE2.name;
  SimpleTest.isDeeply(eventData, mesg, "Search event data");

  await promiseTab();
  await focusContentSearchBar();

  // Test typing a query and clicking the search engine header.
  p = waitForSearch();
  modifiers.button = 0;
  await msg("key", { key: "x", waitForSuggestions: true });
  await msg("mousemove", -1);
  await msg("click", { eltIdx: -1, modifiers });
  mesg = await p;
  eventData.originalEvent = modifiers;
  eventData.engineName = TEST_ENGINE1.name;
  SimpleTest.isDeeply(eventData, mesg, "Search event data");

  await promiseTab();
  await focusContentSearchBar();

  // Test typing a query and then clicking a suggestion.
  await msg("key", { key: "x", waitForSuggestions: true });
  p = waitForSearch();
  await msg("mousemove", 1);
  await msg("click", { eltIdx: 1, modifiers });
  mesg = await p;
  eventData.searchString = "xfoo";
  eventData.selection = {
    index: 1,
    kind: "mouse",
  };
  SimpleTest.isDeeply(eventData, mesg, "Search event data");

  await promiseTab();
  await focusContentSearchBar();

  // Test typing a query and then clicking a one-off button.
  await msg("key", { key: "x", waitForSuggestions: true });
  p = waitForSearch();
  await msg("mousemove", 3);
  await msg("click", { eltIdx: 3, modifiers });
  mesg = await p;
  eventData.searchString = "x";
  eventData.engineName = TEST_ENGINE2.name;
  delete eventData.selection;
  SimpleTest.isDeeply(eventData, mesg, "Search event data");

  await promiseTab();
  await focusContentSearchBar();

  // Test selecting a suggestion, then clicking a one-off without deselecting the
  // suggestion, using the keyboard.
  delete modifiers.button;
  await msg("key", { key: "x", waitForSuggestions: true });
  p = waitForSearch();
  await msg("key", "VK_DOWN");
  await msg("key", "VK_DOWN");
  await msg("key", "VK_TAB");
  await msg("key", { key: "VK_RETURN", modifiers });
  mesg = await p;
  eventData.searchString = "xfoo";
  eventData.selection = {
    index: 1,
    kind: "key",
  };
  SimpleTest.isDeeply(eventData, mesg, "Search event data");

  await promiseTab();
  await focusContentSearchBar();

  // Test searching when using IME composition.
  let state = await msg("startComposition", { data: "" });
  checkState(state, "", [], -1);
  state = await msg("changeComposition", {
    data: "x",
    waitForSuggestions: true,
  });
  checkState(
    state,
    "x",
    [
      { str: "x", type: "formHistory" },
      { str: "xfoo", type: "formHistory" },
      "xbar",
    ],
    -1
  );
  await msg("commitComposition");
  delete modifiers.button;
  p = waitForSearch();
  await msg("key", { key: "VK_RETURN", modifiers });
  mesg = await p;
  eventData.searchString = "x";
  eventData.originalEvent = modifiers;
  eventData.engineName = TEST_ENGINE1.name;
  delete eventData.selection;
  SimpleTest.isDeeply(eventData, mesg, "Search event data");

  await promiseTab();
  await focusContentSearchBar();

  state = await msg("startComposition", { data: "" });
  checkState(state, "", [], -1);
  state = await msg("changeComposition", {
    data: "x",
    waitForSuggestions: true,
  });
  checkState(
    state,
    "x",
    [
      { str: "x", type: "formHistory" },
      { str: "xfoo", type: "formHistory" },
      "xbar",
    ],
    -1
  );

  // Mouse over the first suggestion.
  state = await msg("mousemove", 0);
  checkState(
    state,
    "x",
    [
      { str: "x", type: "formHistory" },
      { str: "xfoo", type: "formHistory" },
      "xbar",
    ],
    0
  );

  // Mouse over the second suggestion.
  state = await msg("mousemove", 1);
  checkState(
    state,
    "x",
    [
      { str: "x", type: "formHistory" },
      { str: "xfoo", type: "formHistory" },
      "xbar",
    ],
    1
  );

  modifiers.button = 0;
  p = waitForSearch();
  await msg("click", { eltIdx: 1, modifiers });
  mesg = await p;
  eventData.searchString = "xfoo";
  eventData.originalEvent = modifiers;
  eventData.selection = {
    index: 1,
    kind: "mouse",
  };
  SimpleTest.isDeeply(eventData, mesg, "Search event data");

  await promiseTab();
  await focusContentSearchBar();

  // Remove form history entries.
  // Wait for Satchel.
  let observePromise = new Promise(resolve => {
    let historyCount = 2;
    Services.obs.addObserver(function onRemove(subj, topic, data) {
      if (data == "formhistory-remove") {
        if (--historyCount) {
          return;
        }
        Services.obs.removeObserver(onRemove, "satchel-storage-changed");
        executeSoon(resolve);
      }
    }, "satchel-storage-changed");
  });

  await msg("key", { key: "x", waitForSuggestions: true });
  await msg("key", "VK_DOWN");
  await msg("key", "VK_DOWN");
  await msg("key", "VK_DELETE");
  await msg("key", "VK_DOWN");
  await msg("key", "VK_DELETE");
  await observePromise;

  await msg("reset");
  state = await msg("key", { key: "x", waitForSuggestions: true });
  checkState(state, "x", ["xfoo", "xbar"], -1);

  await promiseTab();
  await focusContentSearchBar();
  await msg("reset");
});

add_task(async function settings() {
  await focusContentSearchBar();
  await msg("key", { key: "VK_DOWN", waitForSuggestions: true });
  await msg("key", "VK_UP");
  let p = waitForSearchSettings();
  await msg("key", "VK_RETURN");
  await p;

  await msg("reset");
});

add_task(async function cleanup() {
  Services.search.restoreDefaultEngines();
});

function checkState(
  actualState,
  expectedInputVal,
  expectedSuggestions,
  expectedSelectedIdx,
  expectedSelectedButtonIdx
) {
  expectedSuggestions = expectedSuggestions.map(sugg => {
    return typeof sugg == "object"
      ? sugg
      : {
          str: sugg,
          type: "remote",
        };
  });

  if (expectedSelectedIdx == -1 && expectedSelectedButtonIdx != undefined) {
    expectedSelectedIdx =
      expectedSuggestions.length + expectedSelectedButtonIdx;
  }

  let expectedState = {
    selectedIndex: expectedSelectedIdx,
    numSuggestions: expectedSuggestions.length,
    suggestionAtIndex: expectedSuggestions.map(s => s.str),
    isFormHistorySuggestionAtIndex: expectedSuggestions.map(
      s => s.type == "formHistory"
    ),

    tableHidden: !expectedSuggestions.length,

    inputValue: expectedInputVal,
    ariaExpanded: !expectedSuggestions.length ? "false" : "true",
  };
  if (expectedSelectedButtonIdx != undefined) {
    expectedState.selectedButtonIndex = expectedSelectedButtonIdx;
  } else if (expectedSelectedIdx < expectedSuggestions.length) {
    expectedState.selectedButtonIndex = -1;
  } else {
    expectedState.selectedButtonIndex =
      expectedSelectedIdx - expectedSuggestions.length;
  }

  SimpleTest.isDeeply(actualState, expectedState, "State");
}

var gMsgMan;

async function promiseTab() {
  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
  registerCleanupFunction(() => BrowserTestUtils.removeTab(tab));

  let loadedPromise = BrowserTestUtils.firstBrowserLoaded(window);
  openTrustedLinkIn("about:test-about-content-search-ui", "current");
  await loadedPromise;
}
