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

const PAGECONTENT_SMALL = `
  <!doctype html>
  <html>
  <body><select id='one'>
    <option value='One'>One</option>
    <option value='Two'>Two</option>
  </select><select id='two'>
    <option value='Three'>Three</option>
    <option value='Four'>Four</option>
  </select><select id='three'>
    <option value='Five'>Five</option>
    <option value='Six'>Six</option>
  </select></body></html>
`;

async function performLargePopupTests(win) {
  let browser = win.gBrowser.selectedBrowser;

  await SpecialPowers.spawn(browser, [], async function () {
    let doc = content.document;
    let select = doc.getElementById("one");
    for (var i = 0; i < 180; i++) {
      select.add(new content.Option("Test" + i));
    }

    select.options[60].selected = true;
    select.focus();
  });

  // Check if a drag-select works and scrolls the list.
  const selectPopup = await openSelectPopup("mousedown", "select", win);
  const browserRect = browser.getBoundingClientRect();

  let getScrollPos = () => selectPopup.scrollBox.scrollbox.scrollTop;
  let scrollPos = getScrollPos();
  let popupRect = selectPopup.getBoundingClientRect();

  // First, check that scrolling does not occur when the mouse is moved over the
  // anchor button but not the popup yet.
  EventUtils.synthesizeMouseAtPoint(
    popupRect.left + 5,
    popupRect.top - 10,
    {
      type: "mousemove",
      buttons: 1,
    },
    win
  );
  is(
    getScrollPos(),
    scrollPos,
    "scroll position after mousemove over button should not change"
  );

  EventUtils.synthesizeMouseAtPoint(
    popupRect.left + 20,
    popupRect.top + 10,
    {
      type: "mousemove",
      buttons: 1,
    },
    win
  );

  // Dragging above the popup scrolls it up.
  let scrolledPromise = BrowserTestUtils.waitForEvent(
    selectPopup,
    "scroll",
    false,
    () => getScrollPos() < scrollPos - 5
  );
  EventUtils.synthesizeMouseAtPoint(
    popupRect.left + 20,
    popupRect.top - 20,
    {
      type: "mousemove",
      buttons: 1,
    },
    win
  );
  await scrolledPromise;
  ok(true, "scroll position at drag up");

  // Dragging below the popup scrolls it down.
  scrollPos = getScrollPos();
  scrolledPromise = BrowserTestUtils.waitForEvent(
    selectPopup,
    "scroll",
    false,
    () => getScrollPos() > scrollPos + 5
  );
  EventUtils.synthesizeMouseAtPoint(
    popupRect.left + 20,
    popupRect.bottom + 20,
    {
      type: "mousemove",
      buttons: 1,
    },
    win
  );
  await scrolledPromise;
  ok(true, "scroll position at drag down");

  // Releasing the mouse button and moving the mouse does not change the scroll position.
  scrollPos = getScrollPos();
  EventUtils.synthesizeMouseAtPoint(
    popupRect.left + 20,
    popupRect.bottom + 25,
    { type: "mouseup" },
    win
  );
  is(getScrollPos(), scrollPos, "scroll position at mouseup should not change");

  EventUtils.synthesizeMouseAtPoint(
    popupRect.left + 20,
    popupRect.bottom + 20,
    { type: "mousemove" },
    win
  );
  is(
    getScrollPos(),
    scrollPos,
    "scroll position at mousemove after mouseup should not change"
  );

  // Now check dragging with a mousedown on an item
  let menuRect = selectPopup.children[51].getBoundingClientRect();
  EventUtils.synthesizeMouseAtPoint(
    menuRect.left + 5,
    menuRect.top + 5,
    { type: "mousedown" },
    win
  );

  // Dragging below the popup scrolls it down.
  scrolledPromise = BrowserTestUtils.waitForEvent(
    selectPopup,
    "scroll",
    false,
    () => getScrollPos() > scrollPos + 5
  );
  EventUtils.synthesizeMouseAtPoint(
    popupRect.left + 20,
    popupRect.bottom + 20,
    {
      type: "mousemove",
      buttons: 1,
    },
    win
  );
  await scrolledPromise;
  ok(true, "scroll position at drag down from option");

  // Dragging above the popup scrolls it up.
  scrolledPromise = BrowserTestUtils.waitForEvent(
    selectPopup,
    "scroll",
    false,
    () => getScrollPos() < scrollPos - 5
  );
  EventUtils.synthesizeMouseAtPoint(
    popupRect.left + 20,
    popupRect.top - 20,
    {
      type: "mousemove",
      buttons: 1,
    },
    win
  );
  await scrolledPromise;
  ok(true, "scroll position at drag up from option");

  scrollPos = getScrollPos();
  // We intentionally turn off this a11y check, because the following click
  // is sent on an arbitrary web content that is not expected to be tested
  // by itself with the browser mochitests, therefore this rule check shall
  // be ignored by a11y-checks suite.
  AccessibilityUtils.setEnv({ labelRule: false });
  EventUtils.synthesizeMouseAtPoint(
    popupRect.left + 20,
    popupRect.bottom + 25,
    { type: "mouseup" },
    win
  );
  AccessibilityUtils.resetEnv();
  is(
    getScrollPos(),
    scrollPos,
    "scroll position at mouseup from option should not change"
  );

  EventUtils.synthesizeMouseAtPoint(
    popupRect.left + 20,
    popupRect.bottom + 20,
    { type: "mousemove" },
    win
  );
  is(
    getScrollPos(),
    scrollPos,
    "scroll position at mousemove after mouseup should not change"
  );

  await hideSelectPopup("escape", win);

  let positions = [
    "margin-top: 300px;",
    "position: fixed; bottom: 200px;",
    "width: 100%; height: 9999px;",
  ];

  let position;
  while (positions.length) {
    await openSelectPopup("key", "select", win);

    let rect = selectPopup.getBoundingClientRect();
    let marginBottom = parseFloat(getComputedStyle(selectPopup).marginBottom);
    let marginTop = parseFloat(getComputedStyle(selectPopup).marginTop);
    Assert.greaterOrEqual(
      rect.top - marginTop,
      browserRect.top,
      "Popup top position in within browser area"
    );
    Assert.lessOrEqual(
      rect.bottom + marginBottom,
      browserRect.bottom,
      "Popup bottom position in within browser area"
    );

    let cs = win.getComputedStyle(selectPopup);
    let csArrow = win.getComputedStyle(selectPopup.scrollBox);
    let bpBottom =
      parseFloat(cs.paddingBottom) +
      parseFloat(cs.borderBottomWidth) +
      parseFloat(csArrow.paddingBottom) +
      parseFloat(csArrow.borderBottomWidth);
    let selectedOption = 60;

    if (Services.prefs.getBoolPref("dom.forms.selectSearch")) {
      // Use option 61 instead of 60, as the 60th option element is actually the
      // 61st child, since the first child is now the search input field.
      selectedOption = 61;
    }
    // Some of the styles applied to the menuitems are percentages, meaning
    // that the final layout calculations returned by getBoundingClientRect()
    // might return floating point values. We don't care about sub-pixel
    // accuracy, and only care about the final pixel value, so we add a
    // fuzz-factor of 1.
    const fuzzFactor = 1;
    SimpleTest.isfuzzy(
      selectPopup.children[selectedOption].getBoundingClientRect().bottom,
      selectPopup.getBoundingClientRect().bottom - bpBottom + marginBottom,
      fuzzFactor,
      "Popup scroll at correct position " + bpBottom
    );

    await hideSelectPopup("enter", win);

    position = positions.shift();

    let contentPainted = BrowserTestUtils.waitForContentEvent(
      browser,
      "MozAfterPaint"
    );
    await SpecialPowers.spawn(
      browser,
      [position],
      async function (contentPosition) {
        let select = content.document.getElementById("one");
        select.setAttribute("style", contentPosition || "");
        select.getBoundingClientRect();
      }
    );
    await contentPainted;
  }
}

add_setup(async function () {
  await SpecialPowers.pushPrefEnv({
    set: [["test.wait300msAfterTabSwitch", true]],
  });
});

// This test checks select elements with a large number of options to ensure that
// the popup appears within the browser area.
add_task(async function test_large_popup() {
  const pageUrl = "data:text/html," + escape(PAGECONTENT_SMALL);
  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, pageUrl);

  await performLargePopupTests(window);

  BrowserTestUtils.removeTab(tab);
});

// This test checks the same as the previous test but in a new, vertically smaller window.
add_task(async function test_large_popup_in_small_window() {
  let newWin = await BrowserTestUtils.openNewBrowserWindow();

  let resizePromise = BrowserTestUtils.waitForEvent(
    newWin,
    "resize",
    false,
    () => {
      info(`Got resize event (innerHeight: ${newWin.innerHeight})`);
      return newWin.innerHeight <= 450;
    }
  );
  newWin.resizeTo(600, 450);
  await resizePromise;

  const pageUrl = "data:text/html," + escape(PAGECONTENT_SMALL);
  let browserLoadedPromise = BrowserTestUtils.browserLoaded(
    newWin.gBrowser.selectedBrowser
  );
  BrowserTestUtils.startLoadingURIString(
    newWin.gBrowser.selectedBrowser,
    pageUrl
  );
  await browserLoadedPromise;

  newWin.gBrowser.selectedBrowser.focus();

  await performLargePopupTests(newWin);

  await BrowserTestUtils.closeWindow(newWin);
});
