/* 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/. */

const lazy = {};

ChromeUtils.defineESModuleGetters(lazy, {
  assert: "chrome://remote/content/shared/webdriver/Assert.sys.mjs",
  pprint: "chrome://remote/content/shared/Format.sys.mjs",
});

/**
 * @typedef {string} PromptHandlers
 */

/**
 * Enum of known prompt handlers.
 *
 * @readonly
 * @enum {PromptHandlers}
 *
 * @see https://w3c.github.io/webdriver/#dfn-known-prompt-handlers
 */
export const PromptHandlers = {
  /** All simple dialogs encountered should be accepted. */
  Accept: "accept",
  /**
   * All simple dialogs encountered should be accepted, and an error
   * returned that the dialog was handled.
   */
  AcceptAndNotify: "accept and notify",
  /** All simple dialogs encountered should be dismissed. */
  Dismiss: "dismiss",
  /**
   * All simple dialogs encountered should be dismissed, and an error
   * returned that the dialog was handled.
   */
  DismissAndNotify: "dismiss and notify",
  /** All simple dialogs encountered should be left to the user to handle. */
  Ignore: "ignore",
};

/**
 * @typedef {string} PromptTypes
 */

/**
 * Enum of valid prompt types.
 *
 * @readonly
 * @enum {PromptTypes}
 *
 * @see https://w3c.github.io/webdriver/#dfn-valid-prompt-types
 */
export const PromptTypes = {
  // A simple alert dialog
  Alert: "alert",
  // A simple beforeunload dialog. If no handler is set it falls back to the
  // `accept` handler to keep backward compatibility with WebDriver classic,
  // which doesn't customize this prompt type.
  BeforeUnload: "beforeUnload",
  // A simple confirm dialog
  Confirm: "confirm",
  // Default value when no specific handler is configured. Can only be set when
  // specifying the unhandlePromptBehavior capability with a map containing a
  // "default" entry. See FALLBACK_DEFAULT_PROMPT_TYPE.
  Default: "default",
  // A file picker dialog
  File: "file",
  // A simple prompt dialog
  Prompt: "prompt",
};

/**
 * Internal prompt type used when the unhandledPromptBehavior capability is
 * set as a string. The "fallbackDefault" type will apply to "alert", "confirm"
 * and "prompt" prompts, but will not apply to "beforeUnload" prompts.
 */
const FALLBACK_DEFAULT_PROMPT_TYPE = "fallbackDefault";

export class PromptHandlerConfiguration {
  #handler;
  #notify;

  /**
   * Create an instance of a prompt handler configuration.
   *
   * @param {string} handler
   *     Handler to use for the user prompt. One of "accept", "dismiss" or "ignore".
   * @param {boolean} notify
   *     Flag to indicate if the user needs to be notified when the dialog was
   *     handled.
   *
   * @see https://w3c.github.io/webdriver/#dfn-prompt-handler-configuration
   */
  constructor(handler, notify) {
    this.#handler = handler;
    this.#notify = notify;
  }

  get handler() {
    return this.#handler;
  }

  get notify() {
    return this.#notify;
  }

  toString() {
    return "[object PromptHandlerConfiguration]";
  }

  /**
   * JSON serialization of the prompt handler configuration object.
   *
   * @returns {Record<string, ?>} json
   *
   * @see https://w3c.github.io/webdriver/#dfn-serialize-a-prompt-handler-configuration
   */
  toJSON() {
    let serialized = this.#handler;
    if (["accept", "dismiss"].includes(serialized) && this.#notify) {
      serialized += " and notify";
    }

    return serialized;
  }
}

export class UserPromptHandler {
  #promptTypeToHandlerMap;

  constructor() {
    // Note: this map is null until update-the-user-prompt-handler initializes
    // it.
    this.#promptTypeToHandlerMap = null;
  }

  get activePromptHandlers() {
    return this.#promptTypeToHandlerMap;
  }

  get PromptHandlers() {
    return PromptHandlers;
  }

  get PromptTypes() {
    return PromptTypes;
  }

  /**
   * Unmarshal a JSON object representation of the unhandledPromptBehavior capability.
   *
   * @param {Record<string, ?>} json
   *     JSON Object to unmarshal.
   *
   * @throws {InvalidArgumentError}
   *     When the value of the unhandledPromptBehavior capability is invalid.
   *
   * @returns {UserPromptHandler}
   *
   * @see https://w3c.github.io/webdriver/#dfn-deserialize-as-an-unhandled-prompt-behavior
   */
  static fromJSON(json) {
    let isStringValue = false;
    let value = json;
    if (typeof value === "string") {
      // A single specified prompt behavior or for WebDriver classic.
      value = { [FALLBACK_DEFAULT_PROMPT_TYPE]: value };
      isStringValue = true;
    }

    lazy.assert.object(
      value,
      lazy.pprint`Expected "unhandledPromptBehavior" to be an object, got ${value}`
    );

    const userPromptHandlerCapability = new Map();
    for (let [promptType, handler] of Object.entries(value)) {
      if (!isStringValue) {
        const validPromptTypes = Object.values(PromptTypes);
        lazy.assert.in(
          promptType,
          validPromptTypes,
          lazy.pprint`Expected "promptType" to be one of ${validPromptTypes}, got ${promptType}`
        );
      }

      const knownPromptHandlers = Object.values(PromptHandlers);
      lazy.assert.in(
        handler,
        knownPromptHandlers,
        lazy.pprint`Expected "handler" to be one of ${knownPromptHandlers}, got ${handler}`
      );

      let notify = false;
      switch (handler) {
        case PromptHandlers.AcceptAndNotify:
          handler = PromptHandlers.Accept;
          notify = true;
          break;
        case PromptHandlers.DismissAndNotify:
          handler = PromptHandlers.Dismiss;
          notify = true;
          break;
        case PromptHandlers.Ignore:
          notify = true;
          break;
      }

      const configuration = new PromptHandlerConfiguration(handler, notify);
      userPromptHandlerCapability.set(promptType, configuration);
    }
    const userPromptHandler = new UserPromptHandler();
    userPromptHandler.update(userPromptHandlerCapability);
    return userPromptHandler;
  }

  /**
   * Get the handler for the given prompt type.
   *
   * @param {string} promptType
   *     The prompt type to retrieve the handler for.
   *
   * @returns {PromptHandlerConfiguration}
   *
   * @see https://w3c.github.io/webdriver/#dfn-get-the-prompt-handler
   */
  getPromptHandler(promptType) {
    let handlers;

    if (this.#promptTypeToHandlerMap === null) {
      handlers = new Map();
    } else {
      handlers = this.#promptTypeToHandlerMap;
    }

    if (handlers.has(promptType)) {
      return handlers.get(promptType);
    }

    if (handlers.has(PromptTypes.Default)) {
      return handlers.get(PromptTypes.Default);
    }

    if (promptType === PromptTypes.BeforeUnload) {
      return new PromptHandlerConfiguration(PromptHandlers.Accept, false);
    }

    if (handlers.has(FALLBACK_DEFAULT_PROMPT_TYPE)) {
      return handlers.get(FALLBACK_DEFAULT_PROMPT_TYPE);
    }

    return new PromptHandlerConfiguration(PromptHandlers.Dismiss, true);
  }

  /**
   * Updates the prompt handler configuration for a given requested prompt
   * handler map.
   *
   * @param {Map} requestedPromptHandler
   *     The request prompt handler configuration map.
   *
   * @see https://w3c.github.io/webdriver/#dfn-update-the-user-prompt-handler
   */
  update(requestedPromptHandler) {
    if (this.#promptTypeToHandlerMap === null) {
      this.#promptTypeToHandlerMap = new Map();
    }

    for (const [promptType, handler] of requestedPromptHandler) {
      this.#promptTypeToHandlerMap.set(promptType, handler);
    }
  }

  /**
   * JSON serialization of the user prompt handler object.
   *
   * @returns {Record<string, ?>} json
   *
   * @see https://w3c.github.io/webdriver/#dfn-serialize-the-user-prompt-handler
   */
  toJSON() {
    if (this.#promptTypeToHandlerMap === null) {
      // Fallback to "dismiss and notify" if no handler is set
      return PromptHandlers.DismissAndNotify;
    }

    if (
      this.#promptTypeToHandlerMap.size === 1 &&
      this.#promptTypeToHandlerMap.has(FALLBACK_DEFAULT_PROMPT_TYPE)
    ) {
      return this.#promptTypeToHandlerMap
        .get(FALLBACK_DEFAULT_PROMPT_TYPE)
        .toJSON();
    }

    const serialized = {};
    for (const [key, value] of this.#promptTypeToHandlerMap.entries()) {
      serialized[key] = value.toJSON();
    }

    return serialized;
  }

  toString() {
    return "[object UserPromptHandler]";
  }
}
