// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements.  See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership.  The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License.  You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied.  See the License for the
// specific language governing permissions and limitations
// under the License.

'use strict'

/**
 * @fileoverview Defines types related to describing the capabilities of a
 * WebDriver session.
 */

const Symbols = require('./symbols')

/**
 * Recognized browser names.
 * @enum {string}
 */
const Browser = {
  CHROME: 'chrome',
  EDGE: 'MicrosoftEdge',
  FIREFOX: 'firefox',
  INTERNET_EXPLORER: 'internet explorer',
  SAFARI: 'safari',
  OPERA: 'opera',
}

/**
 * Strategies for waiting for [document readiness] after a navigation
 * event.
 *
 * [document readiness]: https://html.spec.whatwg.org/#current-document-readiness
 *
 * @enum {string}
 */
const PageLoadStrategy = {
  /**
   * Indicates WebDriver should not wait on the document readiness state after a
   * navigation event.
   */
  NONE: 'none',

  /**
   * Indicates WebDriver should wait for the document readiness state to
   * become "interactive" after navigation.
   */
  EAGER: 'eager',

  /**
   * Indicates WebDriver should wait for the document readiness state to
   * be "complete" after navigation. This is the default page loading strategy.
   */
  NORMAL: 'normal',
}

/**
 * Common platform names. These platforms are not explicitly defined by the
 * WebDriver spec, however, their use is encouraged for interoperability.
 *
 * @enum {string}
 * @see <https://w3c.github.io/webdriver/webdriver-spec.html>
 */
const Platform = {
  LINUX: 'linux',
  MAC: 'mac',
  WINDOWS: 'windows',
}

/**
 * Record object defining the timeouts that apply to certain WebDriver actions.
 *
 * @record
 */
function Timeouts() {}

/**
 * Defines when, in milliseconds, to interrupt a script that is being
 * {@linkplain ./webdriver.IWebDriver#executeScript evaluated}.
 * @type {number}
 */
Timeouts.prototype.script

/**
 * The timeout, in milliseconds, to apply to navigation events along with the
 * {@link PageLoadStrategy}.
 * @type {number}
 */
Timeouts.prototype.pageLoad

/**
 * The maximum amount of time, in milliseconds, to spend attempting to
 * {@linkplain ./webdriver.IWebDriver#findElement locate} an element on the
 * current page.
 * @type {number}
 */
Timeouts.prototype.implicit

/**
 * The possible default actions a WebDriver session can take to respond to
 * unhandled user prompts (`window.alert()`, `window.confirm()`, and
 * `window.prompt()`).
 *
 * @enum {string}
 */
const UserPromptHandler = {
  /** All prompts should be silently accepted. */
  ACCEPT: 'accept',
  /** All prompts should be silently dismissed. */
  DISMISS: 'dismiss',
  /**
   * All prompts should be automatically accepted, but an error should be
   * returned to the next (or currently executing) WebDriver command.
   */
  ACCEPT_AND_NOTIFY: 'accept and notify',
  /**
   * All prompts should be automatically dismissed, but an error should be
   * returned to the next (or currently executing) WebDriver command.
   */
  DISMISS_AND_NOTIFY: 'dismiss and notify',
  /** All prompts should be left unhandled. */
  IGNORE: 'ignore',
}

/**
 * The standard WebDriver capability keys.
 *
 * @enum {string}
 * @see <https://w3c.github.io/webdriver/webdriver-spec.html#capabilities>
 */
const Capability = {
  /**
   * Indicates whether a WebDriver session implicitly trusts otherwise untrusted
   * and self-signed TLS certificates during navigation.
   */
  ACCEPT_INSECURE_TLS_CERTS: 'acceptInsecureCerts',

  /**
   * The browser name. Common browser names are defined in the
   * {@link ./capabilities.Browser Browser} enum.
   */
  BROWSER_NAME: 'browserName',

  /** Identifies the browser version. */
  BROWSER_VERSION: 'browserVersion',

  /**
   * Key for the logging driver logging preferences.
   * The browser name. Common browser names are defined in the
   * {@link ./capabilities.Browser Browser} enum.
   */
  LOGGING_PREFS: 'goog:loggingPrefs',

  /**
   * Defines the session's
   * {@linkplain ./capabilities.PageLoadStrategy page loading strategy}.
   */
  PAGE_LOAD_STRATEGY: 'pageLoadStrategy',

  /**
   * Identifies the operating system of the endpoint node. Common values
   * recognized by the most WebDriver server implementations are predefined in
   * the {@link ./capabilities.Platform Platform} enum.
   */
  PLATFORM_NAME: 'platformName',

  /**
   * Describes the proxy configuration to use for a new WebDriver session.
   */
  PROXY: 'proxy',

  /**
   * Indicates whether the remote end supports all of the window resizing and
   * positioning commands:
   *
   * -  {@linkplain ./webdriver.Window#getRect Window.getRect()}
   * -  {@linkplain ./webdriver.Window#setRect Window.setRect()}
   * -  {@linkplain ./webdriver.Window#maximize Window.maximize()}
   * -  {@linkplain ./webdriver.Window#minimize Window.minimize()}
   * -  {@linkplain ./webdriver.Window#fullscreen Window.fullscreen()}
   */
  SET_WINDOW_RECT: 'setWindowRect',

  /**
   * Describes the {@linkplain ./capabilities.Timeouts timeouts} imposed on
   * certain session operations.
   */
  TIMEOUTS: 'timeouts',

  /**
   * Defines how a WebDriver session should
   * {@linkplain ./capabilities.UserPromptHandler respond} to unhandled user
   * prompts.
   */
  UNHANDLED_PROMPT_BEHAVIOR: 'unhandledPromptBehavior',

  /**
   * Defines the current session’s strict file interactability.
   * Used to upload a file when strict file interactability is on
   */
  STRICT_FILE_INTERACTABILITY: 'strictFileInteractability',
}

/**
 * Converts a generic hash object to a map.
 * @param {!Object<string, ?>} hash The hash object.
 * @return {!Map<string, ?>} The converted map.
 */
function toMap(hash) {
  let m = new Map()
  for (let key in hash) {
    if (Object.prototype.hasOwnProperty.call(hash, key)) {
      m.set(key, hash[key])
    }
  }
  return m
}

/**
 * Describes a set of capabilities for a WebDriver session.
 */
class Capabilities {
  /**
   * @param {(Capabilities|Map<string, ?>|Object)=} other Another set of
   *     capabilities to initialize this instance from.
   */
  constructor(other = undefined) {
    if (other instanceof Capabilities) {
      other = other.map_
    } else if (other && !(other instanceof Map)) {
      other = toMap(other)
    }
    /** @private @const {!Map<string, ?>} */
    this.map_ = new Map(other)
  }

  /**
   * @return {!Capabilities} A basic set of capabilities for Chrome.
   */
  static chrome() {
    return new Capabilities().setBrowserName(Browser.CHROME)
  }

  /**
   * @return {!Capabilities} A basic set of capabilities for Microsoft Edge.
   */
  static edge() {
    return new Capabilities().setBrowserName(Browser.EDGE)
  }

  /**
   * @return {!Capabilities} A basic set of capabilities for Firefox.
   */
  static firefox() {
    return new Capabilities()
      .setBrowserName(Browser.FIREFOX)
      .set('moz:debuggerAddress', true)
  }

  /**
   * @return {!Capabilities} A basic set of capabilities for Internet Explorer.
   */
  static ie() {
    return new Capabilities().setBrowserName(Browser.INTERNET_EXPLORER)
  }

  /**
   * @return {!Capabilities} A basic set of capabilities for Safari.
   */
  static safari() {
    return new Capabilities().setBrowserName(Browser.SAFARI)
  }

  /**
   * @return {!Capabilities} A basic set of capabilities for Opera
   */
  static opera() {
    return new Capabilities().setBrowserName(Browser.OPERA)
  }

  /**
   * @return {!Object<string, ?>} The JSON representation of this instance.
   *     Note, the returned object may contain nested promised values.
   * @suppress {checkTypes} Suppress [] access on a struct (state inherited from
   *     Map).
   */
  [Symbols.serialize]() {
    return serialize(this)
  }

  /**
   * @param {string} key the parameter key to get.
   * @return {T} the stored parameter value.
   * @template T
   */
  get(key) {
    return this.map_.get(key)
  }

  /**
   * @param {string} key the key to test.
   * @return {boolean} whether this capability set has the specified key.
   */
  has(key) {
    return this.map_.has(key)
  }

  /**
   * @return {!Iterator<string>} an iterator of the keys set.
   */
  keys() {
    return this.map_.keys()
  }

  /** @return {number} The number of capabilities set. */
  get size() {
    return this.map_.size
  }

  /**
   * Merges another set of capabilities into this instance.
   * @param {!(Capabilities|Map<String, ?>|Object<string, ?>)} other The other
   *     set of capabilities to merge.
   * @return {!Capabilities} A self reference.
   */
  merge(other) {
    if (other) {
      let otherMap
      if (other instanceof Capabilities) {
        otherMap = other.map_
      } else if (other instanceof Map) {
        otherMap = other
      } else {
        otherMap = toMap(other)
      }
      otherMap.forEach((value, key) => {
        this.set(key, value)
      })
      return this
    } else {
      throw new TypeError('no capabilities provided for merge')
    }
  }

  /**
   * Deletes an entry from this set of capabilities.
   *
   * @param {string} key the capability key to delete.
   */
  delete(key) {
    this.map_.delete(key)
  }

  /**
   * @param {string} key The capability key.
   * @param {*} value The capability value.
   * @return {!Capabilities} A self reference.
   * @throws {TypeError} If the `key` is not a string.
   */
  set(key, value) {
    if (typeof key !== 'string') {
      throw new TypeError('Capability keys must be strings: ' + typeof key)
    }
    this.map_.set(key, value)
    return this
  }

  /**
   * Sets whether a WebDriver session should implicitly accept self-signed, or
   * other untrusted TLS certificates on navigation.
   *
   * @param {boolean} accept whether to accept insecure certs.
   * @return {!Capabilities} a self reference.
   */
  setAcceptInsecureCerts(accept) {
    return this.set(Capability.ACCEPT_INSECURE_TLS_CERTS, accept)
  }

  /**
   * @return {boolean} whether the session is configured to accept insecure
   *     TLS certificates.
   */
  getAcceptInsecureCerts() {
    return this.get(Capability.ACCEPT_INSECURE_TLS_CERTS)
  }

  /**
   * Sets the name of the target browser.
   *
   * @param {(Browser|string)} name the browser name.
   * @return {!Capabilities} a self reference.
   */
  setBrowserName(name) {
    return this.set(Capability.BROWSER_NAME, name)
  }

  /**
   * @return {(string|undefined)} the configured browser name, or undefined if
   *     not set.
   */
  getBrowserName() {
    return this.get(Capability.BROWSER_NAME)
  }

  /**
   * Sets the desired version of the target browser.
   *
   * @param {string} version the desired version.
   * @return {!Capabilities} a self reference.
   */
  setBrowserVersion(version) {
    return this.set(Capability.BROWSER_VERSION, version)
  }

  /**
   * @return {(string|undefined)} the configured browser version, or undefined
   *     if not set.
   */
  getBrowserVersion() {
    return this.get(Capability.BROWSER_VERSION)
  }

  /**
   * Sets the desired page loading strategy for a new WebDriver session.
   *
   * @param {PageLoadStrategy} strategy the desired strategy.
   * @return {!Capabilities} a self reference.
   */
  setPageLoadStrategy(strategy) {
    return this.set(Capability.PAGE_LOAD_STRATEGY, strategy)
  }

  /**
   * Returns the configured page load strategy.
   *
   * @return {(string|undefined)} the page load strategy.
   */
  getPageLoadStrategy() {
    return this.get(Capability.PAGE_LOAD_STRATEGY)
  }

  /**
   * Sets the target platform.
   *
   * @param {(Platform|string)} platform the target platform.
   * @return {!Capabilities} a self reference.
   */
  setPlatform(platform) {
    return this.set(Capability.PLATFORM_NAME, platform)
  }

  /**
   * @return {(string|undefined)} the configured platform or undefined if not
   *     set.
   */
  getPlatform() {
    return this.get(Capability.PLATFORM_NAME)
  }

  /**
   * Sets the logging preferences. Preferences may be specified as a
   * {@link ./logging.Preferences} instance, or as a map of log-type to
   * log-level.
   * @param {!(./logging.Preferences|Object<string>)} prefs The logging
   *     preferences.
   * @return {!Capabilities} A self reference.
   */
  setLoggingPrefs(prefs) {
    return this.set(Capability.LOGGING_PREFS, prefs)
  }

  /**
   * Sets the proxy configuration for this instance.
   * @param {proxy.Config} proxy The desired proxy configuration.
   * @return {!Capabilities} A self reference.
   */
  setProxy(proxy) {
    return this.set(Capability.PROXY, proxy)
  }

  /**
   * @return {(proxy.Config|undefined)} the configured proxy settings, or
   *     undefined if not set.
   */
  getProxy() {
    return this.get(Capability.PROXY)
  }

  /**
   * Sets the default action to take with an unexpected alert before returning
   * an error. If unspecified, WebDriver will default to
   * {@link UserPromptHandler.DISMISS_AND_NOTIFY}.
   *
   * @param {?UserPromptHandler} behavior The way WebDriver should respond to
   *     unhandled user prompts.
   * @return {!Capabilities} A self reference.
   */
  setAlertBehavior(behavior) {
    return this.set(Capability.UNHANDLED_PROMPT_BEHAVIOR, behavior)
  }

  /**
   * @return {(UserPromptHandler|undefined)} the behavior pattern for responding
   *     to unhandled user prompts, or undefined if not set.
   */
  getAlertBehavior() {
    return this.get(Capability.UNHANDLED_PROMPT_BEHAVIOR)
  }

  /**
   * Sets the boolean flag configuration for this instance.
   */
  setStrictFileInteractability(strictFileInteractability) {
    return this.set(
      Capability.STRICT_FILE_INTERACTABILITY,
      strictFileInteractability
    )
  }
}

/**
 * Serializes a capabilities object. This is defined as a standalone function
 * so it may be type checked (where Capabilities[Symbols.serialize] has type
 * checking disabled since it is defined with [] access on a struct).
 *
 * @param {!Capabilities} caps The capabilities to serialize.
 * @return {!Object<string, ?>} The JSON representation of this instance.
 *     Note, the returned object may contain nested promised values.
 */
function serialize(caps) {
  let ret = {}
  for (let key of caps.keys()) {
    let cap = caps.get(key)
    if (cap !== undefined && cap !== null) {
      ret[key] = cap
    }
  }
  return ret
}

// PUBLIC API

module.exports = {
  Browser,
  Capabilities,
  Capability,
  PageLoadStrategy,
  Platform,
  Timeouts,
  UserPromptHandler,
}
