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

"use strict";

// Test selection on result view by mouse.

ChromeUtils.defineESModuleGetters(this, {
  ActionsProviderQuickActions:
    "moz-src:///browser/components/urlbar/ActionsProviderQuickActions.sys.mjs",
});

add_setup(async function () {
  await SpecialPowers.pushPrefEnv({
    set: [
      ["browser.urlbar.secondaryActions.featureGate", true],
      ["browser.urlbar.quickactions.enabled", true],
      ["browser.urlbar.suggest.quickactions", true],
    ],
  });

  UrlbarTestUtils.disableResultMenuAutohide(window);

  await SearchTestUtils.installSearchExtension({}, { setAsDefault: true });

  ActionsProviderQuickActions.addAction("test-addons", {
    commands: ["test-addons"],
    label: "quickactions-addons",
    onPick: () =>
      BrowserTestUtils.startLoadingURIString(
        gBrowser.selectedBrowser,
        "about:about"
      ),
  });
  ActionsProviderQuickActions.addAction("test-downloads", {
    commands: ["test-downloads"],
    label: "quickactions-downloads2",
    onPick: () =>
      BrowserTestUtils.startLoadingURIString(
        gBrowser.selectedBrowser,
        "about:downloads"
      ),
  });

  registerCleanupFunction(function () {
    ActionsProviderQuickActions.removeAction("test-addons");
    ActionsProviderQuickActions.removeAction("test-downloads");
  });
});

add_task(async function basic() {
  const testData = [
    {
      description: "Quick action button to normal result",
      mousedown: ".urlbarView-action-btn[data-action=test-addons]",
      mouseup: ".urlbarView-row:nth-child(1)",
      expected: "https://example.com/?q=test",
    },
    {
      description: "Quick action button to out of result",
      mousedown: ".urlbarView-action-btn[data-action=test-addons]",
      mouseup: "body",
      expected: false,
    },
  ];

  for (const { description, mousedown, mouseup, expected } of testData) {
    info(description);

    await BrowserTestUtils.withNewTab("about:blank", async () => {
      await UrlbarTestUtils.promiseAutocompleteResultPopup({
        value: "test",
        window,
      });
      let [downElement, upElement] = await waitForElements([
        mousedown,
        mouseup,
      ]);

      EventUtils.synthesizeMouseAtCenter(downElement, {
        type: "mousedown",
      });
      Assert.ok(
        downElement.hasAttribute("selected"),
        "Mousedown element should be selected after mousedown"
      );

      if (upElement.tagName === "html:body") {
        // We intentionally turn off this a11y check, because the following
        // click is sent to test the selection behavior using an alternative way
        // of the urlbar dismissal, where other ways are accessible, therefore
        // this test can be ignored.
        AccessibilityUtils.setEnv({
          mustHaveAccessibleRule: false,
        });
      }
      EventUtils.synthesizeMouseAtCenter(upElement, { type: "mouseup" });
      AccessibilityUtils.resetEnv();
      Assert.ok(
        !downElement.hasAttribute("selected"),
        "Mousedown element should not be selected after mouseup"
      );
      Assert.ok(
        !upElement.hasAttribute("selected"),
        "Mouseup element should not be selected after mouseup"
      );

      if (expected) {
        await BrowserTestUtils.browserLoaded(
          gBrowser.selectedBrowser,
          false,
          expected
        );
        Assert.ok(true, "Expected page is opened");
      }
    });
  }
});

add_task(async function outOfBrowser() {
  const testData = [
    {
      description: "Normal result to out of browser",
      mousedown: ".urlbarView-row:nth-child(1) > .urlbarView-row-inner",
    },
    {
      description: "Normal result to out of result",
      mousedown: ".urlbarView-row:nth-child(1) > .urlbarView-row-inner",
      expected: false,
    },
    {
      description: "Quick action button to out of browser",
      mousedown: ".urlbarView-action-btn[data-action=test-addons]",
    },
  ];

  for (const { description, mousedown } of testData) {
    info(description);

    await BrowserTestUtils.withNewTab("about:blank", async () => {
      await UrlbarTestUtils.promiseAutocompleteResultPopup({
        value: "test",
        window,
      });
      let [downElement] = await waitForElements([mousedown]);

      EventUtils.synthesizeMouseAtCenter(downElement, {
        type: "mousedown",
      });
      Assert.ok(
        downElement.hasAttribute("selected"),
        "Mousedown element should be selected after mousedown"
      );

      // Mouseup at out of browser.
      EventUtils.synthesizeMouse(document.documentElement, -1, -1, {
        type: "mouseup",
      });

      Assert.ok(
        !downElement.hasAttribute("selected"),
        "Mousedown element should not be selected after mouseup"
      );
    });
  }
});

add_task(async function withSelectionByKeyboard() {
  const testData = [
    {
      description: "Select normal result, then click on out of result",
      mousedown: "body",
      mouseup: "body",
      expected: {
        selectedElementByKey:
          "#urlbar-results .urlbarView-row > .urlbarView-row-inner[selected]",
        selectedElementAfterMouseDown:
          "#urlbar-results .urlbarView-row > .urlbarView-row-inner[selected]",
        actionedPage: false,
      },
    },
    {
      description: "Select quick action button, then click on out of result",
      arrowDown: 1,
      mousedown: "body",
      mouseup: "body",
      expected: {
        selectedElementByKey:
          "#urlbar-results .urlbarView-action-btn[selected]",
        selectedElementAfterMouseDown:
          "#urlbar-results .urlbarView-action-btn[selected]",
        actionedPage: false,
      },
    },
    {
      description: "Select normal result, then click on about:addons",
      mousedown: ".urlbarView-action-btn[data-action=test-addons]",
      mouseup: ".urlbarView-action-btn[data-action=test-addons]",
      expected: {
        selectedElementByKey:
          "#urlbar-results .urlbarView-row > .urlbarView-row-inner[selected]",
        selectedElementAfterMouseDown:
          ".urlbarView-action-btn[data-action=test-addons]",
        actionedPage: "about:about",
      },
    },
  ];

  for (const {
    description,
    arrowDown,
    mousedown,
    mouseup,
    expected,
  } of testData) {
    info(description);

    await BrowserTestUtils.withNewTab("about:blank", async () => {
      await UrlbarTestUtils.promiseAutocompleteResultPopup({
        value: "test",
        window,
      });
      let [downElement, upElement] = await waitForElements([
        mousedown,
        mouseup,
      ]);

      if (arrowDown) {
        EventUtils.synthesizeKey("KEY_Tab", { repeat: arrowDown }, window);
      }

      let [selectedElementByKey] = await waitForElements([
        expected.selectedElementByKey,
      ]);
      Assert.ok(
        selectedElementByKey.hasAttribute("selected"),
        "selectedElementByKey should be selected after arrow down"
      );

      EventUtils.synthesizeMouseAtCenter(downElement, {
        type: "mousedown",
      });

      if (
        expected.selectedElementByKey !== expected.selectedElementAfterMouseDown
      ) {
        let [selectedElementAfterMouseDown] = await waitForElements([
          expected.selectedElementAfterMouseDown,
        ]);
        Assert.ok(
          selectedElementAfterMouseDown.hasAttribute("selected"),
          "selectedElementAfterMouseDown should be selected after mousedown"
        );
        Assert.ok(
          !selectedElementByKey.hasAttribute("selected"),
          "selectedElementByKey should not be selected after mousedown"
        );
      }

      if (upElement.tagName === "html:body") {
        // We intentionally turn off this a11y check, because the following
        // click is sent to test the selection behavior using an alternative way
        // of the urlbar dismissal, where other ways are accessible, therefore
        // this test can be ignored.
        AccessibilityUtils.setEnv({
          mustHaveAccessibleRule: false,
        });
      }
      EventUtils.synthesizeMouseAtCenter(upElement, {
        type: "mouseup",
      });
      AccessibilityUtils.resetEnv();

      if (expected.actionedPage) {
        Assert.ok(
          !selectedElementByKey.hasAttribute("selected"),
          "selectedElementByKey should not be selected after page starts load"
        );
        await BrowserTestUtils.browserLoaded(
          gBrowser.selectedBrowser,
          false,
          expected.actionedPage
        );
        Assert.ok(true, "Expected page is opened");
      } else {
        Assert.ok(
          selectedElementByKey.hasAttribute("selected"),
          "selectedElementByKey should remain selected"
        );
      }
    });
  }
});

add_task(async function withDnsFirstForSingleWordsPref() {
  await SpecialPowers.pushPrefEnv({
    set: [["browser.fixup.dns_first_for_single_words", true]],
  });

  await PlacesUtils.bookmarks.insert({
    parentGuid: PlacesUtils.bookmarks.unfiledGuid,
    url: "https://example.org/",
    title: "example",
  });
  // An unvisited bookmark may have a lower ranking than a page visited many
  // times, so let's clear history to ensure our bookmark is autofilled.
  await PlacesUtils.history.clear();
  await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();

  await UrlbarTestUtils.promiseAutocompleteResultPopup({
    value: "ex",
    window,
  });

  const details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
  const target = details.element.action;

  const onLoaded = BrowserTestUtils.browserLoaded(
    gBrowser.selectedBrowser,
    false,
    "https://example.org/"
  );
  EventUtils.synthesizeMouseAtCenter(target, {});
  await onLoaded;
  Assert.ok(true, "Expected page is opened");

  await PlacesUtils.bookmarks.eraseEverything();
  await SpecialPowers.popPrefEnv();
});

add_task(async function buttons() {
  let initialTabUrl = "https://example.com/initial";
  let mainResultUrl = "https://example.com/main";
  let mainResultHelpUrl = "https://example.com/help";
  let otherResultUrl = "https://example.com/other";

  let searchString = "test";

  // Add a provider with two results: The first has buttons and the second has a
  // URL that should or shouldn't become the input's value when the block button
  // in the first result is clicked, depending on the test.
  let provider = new UrlbarTestUtils.TestProvider({
    priority: Infinity,
    results: [
      new UrlbarResult({
        type: UrlbarUtils.RESULT_TYPE.URL,
        source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
        payload: {
          url: mainResultUrl,
          helpUrl: mainResultHelpUrl,
          helpL10n: {
            id: "urlbar-result-menu-learn-more-about-firefox-suggest",
          },
          isBlockable: true,
        },
      }),
      new UrlbarResult({
        type: UrlbarUtils.RESULT_TYPE.URL,
        source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
        payload: {
          url: otherResultUrl,
        },
      }),
    ],
  });

  UrlbarProvidersManager.registerProvider(provider);

  let assertResultMenuOpen = () => {
    Assert.equal(
      gURLBar.view.resultMenu.state,
      "showing",
      "Result menu is showing"
    );
    EventUtils.synthesizeKey("KEY_Escape");
  };

  let testData = [
    {
      description: "Menu button to menu button",
      mousedown: ".urlbarView-row:nth-child(1) .urlbarView-button-result-menu",
      afterMouseupCallback: assertResultMenuOpen,
      expected: {
        mousedownSelected: false,
        topSites: {
          pageProxyState: "valid",
          value: initialTabUrl,
        },
        searchString: {
          pageProxyState: "invalid",
          value: searchString,
        },
      },
    },
    {
      description: "Row-inner to menu button",
      mousedown: ".urlbarView-row:nth-child(1) > .urlbarView-row-inner",
      mouseup: ".urlbarView-row:nth-child(1) .urlbarView-button-result-menu",
      afterMouseupCallback: assertResultMenuOpen,
      expected: {
        mousedownSelected: true,
        topSites: {
          pageProxyState: "valid",
          value: initialTabUrl,
        },
        searchString: {
          pageProxyState: "invalid",
          value: searchString,
        },
      },
    },
    {
      description: "Menu button to row-inner",
      mousedown: ".urlbarView-row:nth-child(1) .urlbarView-button-result-menu",
      mouseup: ".urlbarView-row:nth-child(1) > .urlbarView-row-inner",
      expected: {
        mousedownSelected: false,
        url: mainResultUrl,
        newTab: false,
      },
    },
  ];

  for (let showTopSites of [true, false]) {
    for (let {
      description,
      mousedown,
      mouseup,
      expected,
      afterMouseupCallback = null,
    } of testData) {
      info(`Running test with showTopSites = ${showTopSites}: ${description}`);
      mouseup ||= mousedown;

      await BrowserTestUtils.withNewTab(initialTabUrl, async () => {
        Assert.equal(
          gURLBar.getAttribute("pageproxystate"),
          "valid",
          "Sanity check: pageproxystate should be valid initially"
        );
        Assert.equal(
          gURLBar.value,
          UrlbarTestUtils.trimURL(initialTabUrl),
          "Sanity check: input.value should be the initial URL initially"
        );

        if (showTopSites) {
          // Open the view and show top sites by performing the accel+L command.
          await SimpleTest.promiseFocus(window);
          let searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
          document.getElementById("Browser:OpenLocation").doCommand();
          await searchPromise;
        } else {
          // Do a search.
          await UrlbarTestUtils.promiseAutocompleteResultPopup({
            window,
            value: searchString,
          });
        }

        let [downElement, upElement] = await waitForElements([
          mousedown,
          mouseup,
        ]);

        // Mousedown and check the selection.
        EventUtils.synthesizeMouseAtCenter(downElement, {
          type: "mousedown",
        });
        if (expected.mousedownSelected) {
          Assert.ok(
            downElement.hasAttribute("selected"),
            "Mousedown element should be selected after mousedown"
          );
        } else {
          Assert.ok(
            !downElement.hasAttribute("selected"),
            "Mousedown element should not be selected after mousedown"
          );
        }

        let loadPromise;
        if (expected.url) {
          loadPromise = expected.newTab
            ? BrowserTestUtils.waitForNewTab(gBrowser, expected.url)
            : BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser, {
                wantLoad: expected.url,
              });
        }

        // Mouseup and check the selection.
        EventUtils.synthesizeMouseAtCenter(upElement, { type: "mouseup" });
        Assert.ok(
          !downElement.hasAttribute("selected"),
          "Mousedown element should not be selected after mouseup"
        );
        Assert.ok(
          !upElement.hasAttribute("selected"),
          "Mouseup element should not be selected after mouseup"
        );

        // If we expect a URL to load, we're done since the view will close and
        // the input value will be set to the URL.
        if (loadPromise) {
          info("Waiting for URL to load: " + expected.url);
          let tab = await loadPromise;
          Assert.ok(true, "Expected URL loaded");
          if (expected.newTab) {
            BrowserTestUtils.removeTab(tab);
          }
          return;
        }

        if (afterMouseupCallback) {
          await afterMouseupCallback();
        }

        let state = showTopSites ? expected.topSites : expected.searchString;
        Assert.equal(
          gURLBar.getAttribute("pageproxystate"),
          state.pageProxyState,
          "pageproxystate should be as expected"
        );
        Assert.equal(
          gURLBar.value,
          UrlbarTestUtils.trimURL(state.value),
          "input.value should be as expected"
        );
      });
    }
  }

  UrlbarProvidersManager.unregisterProvider(provider);
});

async function waitForElements(selectors) {
  let elements;
  await BrowserTestUtils.waitForCondition(
    () => {
      elements = selectors.map(s => document.querySelector(s));
      return elements.every(e => e && BrowserTestUtils.isVisible(e));
    },
    "Waiting for elements to become visible: " + JSON.stringify(selectors)
  );
  return elements;
}
