// 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 = {
  ANDROID: 'android',
  CHROME: 'chrome',
  EDGE: 'MicrosoftEdge',
  FIREFOX: 'firefox',
  IE: 'internet explorer',
  INTERNET_EXPLORER: 'internet explorer',
  IPAD: 'iPad',
  IPHONE: 'iPhone',
  OPERA: 'opera',
  PHANTOM_JS: 'phantomjs',
  SAFARI: 'safari',
  HTMLUNIT: 'htmlunit'
};


/**
 * Common Capability keys.
 * @enum {string}
 */
const Capability = {

  /**
   * Indicates whether a driver should accept all SSL certs by default. This
   * capability only applies when requesting a new session. To query whether
   * a driver can handle insecure SSL certs, see {@link #SECURE_SSL}.
   */
  ACCEPT_SSL_CERTS: 'acceptSslCerts',


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

  /**
   * Defines how elements should be scrolled into the viewport for interaction.
   * This capability will be set to zero (0) if elements are aligned with the
   * top of the viewport, or one (1) if aligned with the bottom. The default
   * behavior is to align with the top of the viewport.
   */
  ELEMENT_SCROLL_BEHAVIOR: 'elementScrollBehavior',

  /**
   * Whether the driver is capable of handling modal alerts (e.g. alert,
   * confirm, prompt). To define how a driver <i>should</i> handle alerts,
   * use {@link #UNEXPECTED_ALERT_BEHAVIOR}.
   */
  HANDLES_ALERTS: 'handlesAlerts',

  /**
   * Key for the logging driver logging preferences.
   */
  LOGGING_PREFS: 'loggingPrefs',

  /**
   * Whether this session generates native events when simulating user input.
   */
  NATIVE_EVENTS: 'nativeEvents',

  /**
   * Describes the platform the browser is running on. Will be one of
   * ANDROID, IOS, LINUX, MAC, UNIX, or WINDOWS. When <i>requesting</i> a
   * session, ANY may be used to indicate no platform preference (this is
   * semantically equivalent to omitting the platform capability).
   */
  PLATFORM: 'platform',

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

  /** Whether the driver supports changing the browser's orientation. */
  ROTATABLE: 'rotatable',

  /**
   * Whether a driver is only capable of handling secure SSL certs. To request
   * that a driver accept insecure SSL certs by default, use
   * {@link #ACCEPT_SSL_CERTS}.
   */
  SECURE_SSL: 'secureSsl',

  /** Whether the driver supports manipulating the app cache. */
  SUPPORTS_APPLICATION_CACHE: 'applicationCacheEnabled',

  /** Whether the driver supports locating elements with CSS selectors. */
  SUPPORTS_CSS_SELECTORS: 'cssSelectorsEnabled',

  /** Whether the browser supports JavaScript. */
  SUPPORTS_JAVASCRIPT: 'javascriptEnabled',

  /** Whether the driver supports controlling the browser's location info. */
  SUPPORTS_LOCATION_CONTEXT: 'locationContextEnabled',

  /** Whether the driver supports taking screenshots. */
  TAKES_SCREENSHOT: 'takesScreenshot',

  /**
   * Defines how the driver should handle unexpected alerts. The value should
   * be one of "accept", "dismiss", or "ignore".
   */
  UNEXPECTED_ALERT_BEHAVIOR: 'unexpectedAlertBehaviour',

  /** Defines the browser version. */
  VERSION: 'version'
};


/**
 * Describes how a proxy should be configured for a WebDriver session.
 * @record
 */
function ProxyConfig() {}

/**
 * The proxy type. Must be one of {"manual", "pac", "system"}.
 * @type {string}
 */
ProxyConfig.prototype.proxyType;

/**
 * URL for the PAC file to use. Only used if {@link #proxyType} is "pac".
 * @type {(string|undefined)}
 */
ProxyConfig.prototype.proxyAutoconfigUrl;

/**
 * The proxy host for FTP requests. Only used if {@link #proxyType} is "manual".
 * @type {(string|undefined)}
 */
ProxyConfig.prototype.ftpProxy;

/**
 * The proxy host for HTTP requests. Only used if {@link #proxyType} is
 * "manual".
 * @type {(string|undefined)}
 */
ProxyConfig.prototype.httpProxy;

/**
 * The proxy host for HTTPS requests. Only used if {@link #proxyType} is
 * "manual".
 * @type {(string|undefined)}
 */
ProxyConfig.prototype.sslProxy;

/**
 * A comma delimited list of hosts which should bypass all proxies. Only used if
 * {@link #proxyType} is "manual".
 * @type {(string|undefined)}
 */
ProxyConfig.prototype.noProxy;


/**
 * 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 (hash.hasOwnProperty(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 Android.
   */
  static android() {
    return new Capabilities()
        .set(Capability.BROWSER_NAME, Browser.ANDROID);
  }

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

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

  /**
   * @return {!Capabilities} A basic set of capabilities for Firefox.
   */
  static firefox() {
    return new Capabilities().set(Capability.BROWSER_NAME, Browser.FIREFOX);
  }

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

  /**
   * @return {!Capabilities} A basic set of capabilities for iPad.
   */
  static ipad() {
    return new Capabilities().
        set(Capability.BROWSER_NAME, Browser.IPAD);
  }

  /**
   * @return {!Capabilities} A basic set of capabilities for iPhone.
   */
  static iphone() {
    return new Capabilities().
        set(Capability.BROWSER_NAME, Browser.IPHONE);
  }

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

  /**
   * @return {!Capabilities} A basic set of capabilities for PhantomJS.
   */
  static phantomjs() {
    return new Capabilities().
        set(Capability.BROWSER_NAME, Browser.PHANTOM_JS);
  }

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

  /**
   * @return {!Capabilities} A basic set of capabilities for HTMLUnit.
   */
  static htmlunit() {
    return new Capabilities().
        set(Capability.BROWSER_NAME, Browser.HTMLUNIT);
  }

  /**
   * @return {!Capabilities} A basic set of capabilities for HTMLUnit
   *     with enabled Javascript.
   */
  static htmlunitwithjs() {
    return new Capabilities().
        set(Capability.BROWSER_NAME, Browser.HTMLUNIT).
        set(Capability.SUPPORTS_JAVASCRIPT, true);
  }

  /**
   * @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) {
      throw new TypeError('no capabilities provided for merge');
    }

    let map;
    if (other instanceof Capabilities) {
      map = other.map_;
    } else if (other instanceof Map) {
      map = other;
    } else {
      other = toMap(other);
    }

    for (let key of other.keys()) {
      this.set(key, other.get(key));
    }

    return this;
  }

  /**
   * 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 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 {ProxyConfig} proxy The desired proxy configuration.
   * @return {!Capabilities} A self reference.
   */
  setProxy(proxy) {
    return this.set(Capability.PROXY, proxy);
  }

  /**
   * Sets whether native events should be used.
   * @param {boolean} enabled Whether to enable native events.
   * @return {!Capabilities} A self reference.
   */
  setEnableNativeEvents(enabled) {
    return this.set(Capability.NATIVE_EVENTS, enabled);
  }

  /**
   * Sets how elements should be scrolled into view for interaction.
   * @param {number} behavior The desired scroll behavior: either 0 to align
   *     with the top of the viewport or 1 to align with the bottom.
   * @return {!Capabilities} A self reference.
   */
  setScrollBehavior(behavior) {
    return this.set(Capability.ELEMENT_SCROLL_BEHAVIOR, behavior);
  }

  /**
   * Sets the default action to take with an unexpected alert before returning
   * an error.
   * @param {string} behavior The desired behavior should be "accept",
   *     "dismiss", or "ignore". Defaults to "dismiss".
   * @return {!Capabilities} A self reference.
   */
  setAlertBehavior(behavior) {
    return this.set(Capability.UNEXPECTED_ALERT_BEHAVIOR, behavior);
  }
}


/**
 * 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: Browser,
  Capabilities: Capabilities,
  Capability: Capability,
  ProxyConfig: ProxyConfig
};
