/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

import { TestUtils } from "resource://testing-common/TestUtils.sys.mjs";

/**
 * A helper function for determining whether or not a notification with
 * a particular value is being displayed.
 *
 * @param aWindow        the window to check
 * @param aBoxId         the id of the notification box
 * @param aValue         the value of the notification to look for
 * @param aNotification  an optional out parameter: object that will pass the
 *                       notification element out of this function in its
 *                       'notification' property
 *
 * @returns True/false depending on the state of the notification.
 */
export function check_notification_displayed(
  aWindow,
  aBoxId,
  aValue,
  aNotification
) {
  const nb = aWindow.document.getElementById(aBoxId);
  if (!nb) {
    throw new Error("Couldn't find a notification box for id=" + aBoxId);
  }

  if (nb.querySelector(".notificationbox-stack")) {
    const box = nb.querySelector(".notificationbox-stack")._notificationBox;
    const notification = box.getNotificationWithValue(aValue);
    if (aNotification) {
      aNotification.notification = notification;
    }
    return notification != null;
  }

  return false;
}

/**
 * A helper function ensuring whether or not a notification with
 * a particular value is being displayed. Throws if the state is
 * not the expected one.
 *
 * @param aWindow     the window to check
 * @param aBoxId      the id of the notification box
 * @param aValue      the value of the notification to look for
 * @param aDisplayed  true if the notification should be displayed, false
 *                    otherwise
 * @returns the notification if we're asserting that the notification is
 *          displayed, and it actually shows up. Throws otherwise.
 */
export function assert_notification_displayed(
  aWindow,
  aBoxId,
  aValue,
  aDisplayed
) {
  const notification = {};
  const hasNotification = check_notification_displayed(
    aWindow,
    aBoxId,
    aValue,
    notification
  );
  if (hasNotification != aDisplayed) {
    throw new Error(
      "Expected the notification with value " +
        aValue +
        " to " +
        (aDisplayed ? "be shown" : "not be shown")
    );
  }

  return notification.notification;
}

/**
 * A helper function for closing a notification if one is currently displayed
 * in the window.
 *
 * @param aWindow  the window with the notification
 * @param aBoxId   the id of the notification box
 * @param aValue   the value of the notification to close
 */
export function close_notification(aWindow, aBoxId, aValue) {
  const nb = aWindow.document.getElementById(aBoxId);
  if (!nb) {
    throw new Error("Couldn't find a notification box for id=" + aBoxId);
  }

  const box = nb.querySelector(".notificationbox-stack")._notificationBox;
  const notification = box.getNotificationWithValue(aValue);
  if (notification) {
    notification.close();
  }
}

/**
 * A helper function that waits for a notification with value aValue
 * to stop displaying in the window.
 *
 * @param aWindow  the window with the notification
 * @param aBoxId   the id of the notification box
 * @param aValue   the value of the notification to wait to stop
 */
export async function wait_for_notification_to_stop(aWindow, aBoxId, aValue) {
  const nb = aWindow.document.getElementById(aBoxId);
  if (!nb) {
    throw new Error("Couldn't find a notification box for id=" + aBoxId);
  }

  const box = nb.querySelector(".notificationbox-stack")?._notificationBox;
  await TestUtils.waitForCondition(
    () => !box?.getNotificationWithValue(aValue),
    "Timed out waiting for notification with value " + aValue + " to stop."
  );
}

/**
 * A helper function that waits for a notification with value aValue
 * to show in the window.
 *
 * @param aWindow  the window that we want the notification to appear in
 * @param aBoxId   the id of the notification box
 * @param aValue   the value of the notification to wait for
 */
export async function wait_for_notification_to_show(aWindow, aBoxId, aValue) {
  const nb = aWindow.document.getElementById(aBoxId);
  if (!nb) {
    throw new Error("Couldn't find a notification box for id=" + aBoxId);
  }

  await TestUtils.waitForCondition(function () {
    if (nb.querySelector(".notificationbox-stack")) {
      const box = nb.querySelector(".notificationbox-stack")._notificationBox;
      return box.getNotificationWithValue(aValue) != null && !box._animating;
    }
    return false;
  }, "Timed out waiting for notification with value " + aValue + " to show.");
}

/**
 * Return the notification element based on the container ID and the Value type.
 *
 * @param {Window} win - The window that we want the notification to appear in.
 * @param {string} id - The id of the notification box.
 * @param {string} val - The value of the notification to fetch.
 * @returns {?Element} - The notification element if found.
 */
export function get_notification(win, id, val) {
  const nb = win.document.getElementById(id);
  if (!nb) {
    throw new Error("Couldn't find a notification box for id=" + id);
  }

  if (nb.querySelector(".notificationbox-stack")) {
    const box = nb.querySelector(".notificationbox-stack")._notificationBox;
    return box.getNotificationWithValue(val);
  }

  return null;
}

/**
 * Gets a button in a notification, as those do not have IDs.
 *
 * @param aWindow  The window that has the notification.
 * @param aBoxId   The id of the notification box.
 * @param aValue   The value of the notification to find.
 * @param aMatch   Attributes of the button to find. An object with key:value
 *                   pairs, similar to click_menus_in_sequence().
 */
export function get_notification_button(aWindow, aBoxId, aValue, aMatch) {
  const notification = get_notification(aWindow, aBoxId, aValue);
  const buttons = notification.buttonContainer.querySelectorAll(
    "button, toolbarbutton"
  );
  for (const button of buttons) {
    let matchedAll = true;
    for (const name in aMatch) {
      const value = aMatch[name];
      let matched = false;
      if (name == "popup") {
        if (button.getAttribute("type") == "menu") {
          // The button contains a menupopup as the first child.
          matched = button.querySelector("menupopup#" + value);
        } else {
          // The "popup" attribute is not on the button itself but in its
          // buttonInfo member.
          matched = "buttonInfo" in button && button.buttonInfo.popup == value;
        }
      } else if (
        button.hasAttribute(name) &&
        button.getAttribute(name) == value
      ) {
        matched = true;
      }
      if (!matched) {
        matchedAll = false;
        break;
      }
    }
    if (matchedAll) {
      return button;
    }
  }

  throw new Error("Couldn't find the requested button on a notification");
}
