// 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';

/**
 * The base WebDriver error type. This error type is only used directly when a
 * more appropriate category is not defined for the offending error.
 */
class WebDriverError extends Error {
  /** @param {string=} opt_error the error message, if any. */
  constructor(opt_error) {
    super(opt_error);

    /** @override */
    this.name = this.constructor.name;

    /**
     * A stacktrace reported by the remote webdriver endpoint that initially
     * reported this error. This property will be an empty string if the remote
     * end did not provide a stacktrace.
     * @type {string}
     */
    this.remoteStacktrace = '';
  }
}


/**
 * Indicates a {@linkplain ./webdriver.WebElement#click click command} could not
 * completed because the click target is obscured by other elements on the
 * page.
 */
class ElementClickInterceptedError extends WebDriverError {
  /** @param {string=} opt_error the error message, if any. */
  constructor(opt_error) {
    super(opt_error);
  }
}


/**
 * An attempt was made to select an element that cannot be selected.
 */
class ElementNotSelectableError extends WebDriverError {
  /** @param {string=} opt_error the error message, if any. */
  constructor(opt_error) {
    super(opt_error);
  }
}


/**
 * Indicates a command could not be completed because the target element is
 * not pointer or keyboard interactable. This will often occur if an element
 * is present in the DOM, but not rendered (i.e. its CSS style has
 * "display: none").
 */
class ElementNotInteractableError extends WebDriverError {
  /** @param {string=} opt_error the error message, if any. */
  constructor(opt_error) {
    super(opt_error);
  }
}


/**
 * Indicates a navigation event caused the browser to generate a certificate
 * warning. This is usually caused by an expired or invalid TLS certificate.
 */
class InsecureCertificateError extends WebDriverError {
  /** @param {string=} opt_error the error message, if any. */
  constructor(opt_error) {
    super(opt_error);
  }
}


/**
 * The arguments passed to a command are either invalid or malformed.
 */
class InvalidArgumentError extends WebDriverError {
  /** @param {string=} opt_error the error message, if any. */
  constructor(opt_error) {
    super(opt_error);
  }
}


/**
 * An illegal attempt was made to set a cookie under a different domain than
 * the current page.
 */
class InvalidCookieDomainError extends WebDriverError {
  /** @param {string=} opt_error the error message, if any. */
  constructor(opt_error) {
    super(opt_error);
  }
}


/**
 * The coordinates provided to an interactions operation are invalid.
 */
class InvalidCoordinatesError extends WebDriverError {
  /** @param {string=} opt_error the error message, if any. */
  constructor(opt_error) {
    super(opt_error);
  }
}


/**
 * An element command could not be completed because the element is in an
 * invalid state, e.g. attempting to click an element that is no longer attached
 * to the document.
 */
class InvalidElementStateError extends WebDriverError {
  /** @param {string=} opt_error the error message, if any. */
  constructor(opt_error) {
    super(opt_error);
  }
}


/**
 * Argument was an invalid selector.
 */
class InvalidSelectorError extends WebDriverError {
  /** @param {string=} opt_error the error message, if any. */
  constructor(opt_error) {
    super(opt_error);
  }
}


/**
 * Occurs when a command is directed to a session that does not exist.
 */
class NoSuchSessionError extends WebDriverError {
  /** @param {string=} opt_error the error message, if any. */
  constructor(opt_error) {
    super(opt_error);
  }
}


/**
 * An error occurred while executing JavaScript supplied by the user.
 */
class JavascriptError extends WebDriverError {
  /** @param {string=} opt_error the error message, if any. */
  constructor(opt_error) {
    super(opt_error);
  }
}


/**
 * The target for mouse interaction is not in the browser’s viewport and cannot
 * be brought into that viewport.
 */
class MoveTargetOutOfBoundsError extends WebDriverError {
  /** @param {string=} opt_error the error message, if any. */
  constructor(opt_error) {
    super(opt_error);
  }
}


/**
 * An attempt was made to operate on a modal dialog when one was not open.
 */
class NoSuchAlertError extends WebDriverError {
  /** @param {string=} opt_error the error message, if any. */
  constructor(opt_error) {
    super(opt_error);
  }
}


/**
 * Indicates a named cookie could not be found in the cookie jar for the
 * currently selected document.
 */
class NoSuchCookieError extends WebDriverError {
  /** @param {string=} opt_error the error message, if any. */
  constructor(opt_error) {
    super(opt_error);
  }
}


/**
 * An element could not be located on the page using the given search
 * parameters.
 */
class NoSuchElementError extends WebDriverError {
  /** @param {string=} opt_error the error message, if any. */
  constructor(opt_error) {
    super(opt_error);
  }
}


/**
 * A request to switch to a frame could not be satisfied because the frame
 * could not be found.
 */
class NoSuchFrameError extends WebDriverError {
  /** @param {string=} opt_error the error message, if any. */
  constructor(opt_error) {
    super(opt_error);
  }
}


/**
 * A request to switch to a window could not be satisfied because the window
 * could not be found.
 */
class NoSuchWindowError extends WebDriverError {
  /** @param {string=} opt_error the error message, if any. */
  constructor(opt_error) {
    super(opt_error);
  }
}


/**
 * A script did not complete before its timeout expired.
 */
class ScriptTimeoutError extends WebDriverError {
  /** @param {string=} opt_error the error message, if any. */
  constructor(opt_error) {
    super(opt_error);
  }
}


/**
 * A new session could not be created.
 */
class SessionNotCreatedError extends WebDriverError {
  /** @param {string=} opt_error the error message, if any. */
  constructor(opt_error) {
    super(opt_error);
  }
}



/**
 * An element command failed because the referenced element is no longer
 * attached to the DOM.
 */
class StaleElementReferenceError extends WebDriverError {
  /** @param {string=} opt_error the error message, if any. */
  constructor(opt_error) {
    super(opt_error);
  }
}


/**
 * An operation did not complete before its timeout expired.
 */
class TimeoutError extends WebDriverError {
  /** @param {string=} opt_error the error message, if any. */
  constructor(opt_error) {
    super(opt_error);
  }
}


/**
 * A request to set a cookie’s value could not be satisfied.
 */
class UnableToSetCookieError extends WebDriverError {
  /** @param {string=} opt_error the error message, if any. */
  constructor(opt_error) {
    super(opt_error);
  }
}


/**
 * A screen capture operation was not possible.
 */
class UnableToCaptureScreenError extends WebDriverError {
  /** @param {string=} opt_error the error message, if any. */
  constructor(opt_error) {
    super(opt_error);
  }
}


/**
 * A modal dialog was open, blocking this operation.
 */
class UnexpectedAlertOpenError extends WebDriverError {
  /**
   * @param {string=} opt_error the error message, if any.
   * @param {string=} opt_text the text of the open dialog, if available.
   */
  constructor(opt_error, opt_text) {
    super(opt_error);

    /** @private {(string|undefined)} */
    this.text_ = opt_text;
  }

  /**
   * @return {(string|undefined)} The text displayed with the unhandled alert,
   *     if available.
   */
  getAlertText() {
    return this.text_;
  }
}


/**
 * A command could not be executed because the remote end is not aware of it.
 */
class UnknownCommandError extends WebDriverError {
  /** @param {string=} opt_error the error message, if any. */
  constructor(opt_error) {
    super(opt_error);
  }
}


/**
 * The requested command matched a known URL but did not match an method for
 * that URL.
 */
class UnknownMethodError extends WebDriverError {
  /** @param {string=} opt_error the error message, if any. */
  constructor(opt_error) {
    super(opt_error);
  }
}


/**
 * Reports an unsupported operation.
 */
class UnsupportedOperationError extends WebDriverError {
  /** @param {string=} opt_error the error message, if any. */
  constructor(opt_error) {
    super(opt_error);
  }
}

// TODO(jleyba): Define UnknownError as an alias of WebDriverError?


/**
 * Enum of legacy error codes.
 * TODO: remove this when all code paths have been switched to the new error
 * types.
 * @deprecated
 * @enum {number}
 */
const ErrorCode = {
  SUCCESS: 0,
  NO_SUCH_SESSION: 6,
  NO_SUCH_ELEMENT: 7,
  NO_SUCH_FRAME: 8,
  UNKNOWN_COMMAND: 9,
  UNSUPPORTED_OPERATION: 9,
  STALE_ELEMENT_REFERENCE: 10,
  ELEMENT_NOT_VISIBLE: 11,
  INVALID_ELEMENT_STATE: 12,
  UNKNOWN_ERROR: 13,
  ELEMENT_NOT_SELECTABLE: 15,
  JAVASCRIPT_ERROR: 17,
  XPATH_LOOKUP_ERROR: 19,
  TIMEOUT: 21,
  NO_SUCH_WINDOW: 23,
  INVALID_COOKIE_DOMAIN: 24,
  UNABLE_TO_SET_COOKIE: 25,
  UNEXPECTED_ALERT_OPEN: 26,
  NO_SUCH_ALERT: 27,
  SCRIPT_TIMEOUT: 28,
  INVALID_ELEMENT_COORDINATES: 29,
  IME_NOT_AVAILABLE: 30,
  IME_ENGINE_ACTIVATION_FAILED: 31,
  INVALID_SELECTOR_ERROR: 32,
  SESSION_NOT_CREATED: 33,
  MOVE_TARGET_OUT_OF_BOUNDS: 34,
  SQL_DATABASE_ERROR: 35,
  INVALID_XPATH_SELECTOR: 51,
  INVALID_XPATH_SELECTOR_RETURN_TYPE: 52,
  ELEMENT_NOT_INTERACTABLE: 60,
  INVALID_ARGUMENT: 61,
  NO_SUCH_COOKIE: 62,
  UNABLE_TO_CAPTURE_SCREEN: 63,
  ELEMENT_CLICK_INTERCEPTED: 64,
  METHOD_NOT_ALLOWED: 405
};


const LEGACY_ERROR_CODE_TO_TYPE = new Map([
    [ErrorCode.NO_SUCH_SESSION, NoSuchSessionError],
    [ErrorCode.NO_SUCH_ELEMENT, NoSuchElementError],
    [ErrorCode.NO_SUCH_FRAME, NoSuchFrameError],
    [ErrorCode.UNSUPPORTED_OPERATION, UnsupportedOperationError],
    [ErrorCode.STALE_ELEMENT_REFERENCE, StaleElementReferenceError],
    [ErrorCode.INVALID_ELEMENT_STATE, InvalidElementStateError],
    [ErrorCode.UNKNOWN_ERROR, WebDriverError],
    [ErrorCode.ELEMENT_NOT_SELECTABLE, ElementNotSelectableError],
    [ErrorCode.JAVASCRIPT_ERROR, JavascriptError],
    [ErrorCode.XPATH_LOOKUP_ERROR, InvalidSelectorError],
    [ErrorCode.TIMEOUT, TimeoutError],
    [ErrorCode.NO_SUCH_WINDOW, NoSuchWindowError],
    [ErrorCode.INVALID_COOKIE_DOMAIN, InvalidCookieDomainError],
    [ErrorCode.UNABLE_TO_SET_COOKIE, UnableToSetCookieError],
    [ErrorCode.UNEXPECTED_ALERT_OPEN, UnexpectedAlertOpenError],
    [ErrorCode.NO_SUCH_ALERT, NoSuchAlertError],
    [ErrorCode.SCRIPT_TIMEOUT, ScriptTimeoutError],
    [ErrorCode.INVALID_ELEMENT_COORDINATES, InvalidCoordinatesError],
    [ErrorCode.INVALID_SELECTOR_ERROR, InvalidSelectorError],
    [ErrorCode.SESSION_NOT_CREATED, SessionNotCreatedError],
    [ErrorCode.MOVE_TARGET_OUT_OF_BOUNDS, MoveTargetOutOfBoundsError],
    [ErrorCode.INVALID_XPATH_SELECTOR, InvalidSelectorError],
    [ErrorCode.INVALID_XPATH_SELECTOR_RETURN_TYPE, InvalidSelectorError],
    [ErrorCode.ELEMENT_NOT_INTERACTABLE, ElementNotInteractableError],
    [ErrorCode.INVALID_ARGUMENT, InvalidArgumentError],
    [ErrorCode.NO_SUCH_COOKIE, NoSuchCookieError],
    [ErrorCode.UNABLE_TO_CAPTURE_SCREEN, UnableToCaptureScreenError],
    [ErrorCode.ELEMENT_CLICK_INTERCEPTED, ElementClickInterceptedError],
    [ErrorCode.METHOD_NOT_ALLOWED, UnsupportedOperationError]]);


const ERROR_CODE_TO_TYPE = new Map([
    ['unknown error', WebDriverError],
    ['element click intercepted', ElementClickInterceptedError],
    ['element not interactable', ElementNotInteractableError],
    ['element not selectable', ElementNotSelectableError],
    ['insecure certificate', InsecureCertificateError],
    ['invalid argument', InvalidArgumentError],
    ['invalid cookie domain', InvalidCookieDomainError],
    ['invalid coordinates', InvalidCoordinatesError],
    ['invalid element state', InvalidElementStateError],
    ['invalid selector', InvalidSelectorError],
    ['invalid session id', NoSuchSessionError],
    ['javascript error', JavascriptError],
    ['move target out of bounds', MoveTargetOutOfBoundsError],
    ['no such alert', NoSuchAlertError],
    ['no such cookie', NoSuchCookieError],
    ['no such element', NoSuchElementError],
    ['no such frame', NoSuchFrameError],
    ['no such window', NoSuchWindowError],
    ['script timeout', ScriptTimeoutError],
    ['session not created', SessionNotCreatedError],
    ['stale element reference', StaleElementReferenceError],
    ['timeout', TimeoutError],
    ['unable to set cookie', UnableToSetCookieError],
    ['unable to capture screen', UnableToCaptureScreenError],
    ['unexpected alert open', UnexpectedAlertOpenError],
    ['unknown command', UnknownCommandError],
    ['unknown method', UnknownMethodError],
    ['unsupported operation', UnsupportedOperationError]]);


const TYPE_TO_ERROR_CODE = new Map;
ERROR_CODE_TO_TYPE.forEach((value, key) => {
  TYPE_TO_ERROR_CODE.set(value, key);
});



/**
 * @param {*} err The error to encode.
 * @return {{error: string, message: string}} the encoded error.
 */
function encodeError(err) {
  let type = WebDriverError;
  if (err instanceof WebDriverError
      && TYPE_TO_ERROR_CODE.has(err.constructor)) {
    type = err.constructor;
  }

  let message = err instanceof Error
      ? err.message
      : err + '';

  let code = /** @type {string} */(TYPE_TO_ERROR_CODE.get(type));
  return {'error': code, 'message': message};
}


/**
 * Checks a response object from a server that adheres to the W3C WebDriver
 * protocol.
 * @param {*} data The response data to check.
 * @return {*} The response data if it was not an encoded error.
 * @throws {WebDriverError} the decoded error, if present in the data object.
 * @deprecated Use {@link #throwDecodedError(data)} instead.
 * @see https://w3c.github.io/webdriver/webdriver-spec.html#protocol
 */
function checkResponse(data) {
  if (data && typeof data.error === 'string') {
    let ctor = ERROR_CODE_TO_TYPE.get(data.error) || WebDriverError;
    throw new ctor(data.message);
  }
  return data;
}

/**
 * Tests if the given value is a valid error response object according to the
 * W3C WebDriver spec.
 *
 * @param {?} data The value to test.
 * @return {boolean} Whether the given value data object is a valid error
 *     response.
 * @see https://w3c.github.io/webdriver/webdriver-spec.html#protocol
 */
function isErrorResponse(data) {
  return data && typeof data === 'object' && typeof data.error === 'string';
}

/**
 * Throws an error coded from the W3C protocol. A generic error will be thrown
 * if the provided `data` is not a valid encoded error.
 *
 * @param {{error: string, message: string}} data The error data to decode.
 * @throws {WebDriverError} the decoded error.
 * @see https://w3c.github.io/webdriver/webdriver-spec.html#protocol
 */
function throwDecodedError(data) {
  if (isErrorResponse(data)) {
    let ctor = ERROR_CODE_TO_TYPE.get(data.error) || WebDriverError;
    let err = new ctor(data.message);
    // TODO(jleyba): remove whichever case is excluded from the final W3C spec.
    if (typeof data.stacktrace === 'string') {
      err.remoteStacktrace = data.stacktrace;
    } else if (typeof data.stackTrace === 'string') {
      err.remoteStacktrace = data.stackTrace;
    }
    throw err;
  }
  throw new WebDriverError('Unknown error: ' + JSON.stringify(data));
}


/**
 * Checks a legacy response from the Selenium 2.0 wire protocol for an error.
 * @param {*} responseObj the response object to check.
 * @return {*} responseObj the original response if it does not define an error.
 * @throws {WebDriverError} if the response object defines an error.
 */
function checkLegacyResponse(responseObj) {
  // Handle the legacy Selenium error response format.
  if (responseObj
      && typeof responseObj === 'object'
      && typeof responseObj['status'] === 'number'
      && responseObj['status'] !== 0) {
    let status = responseObj['status'];
    let ctor = LEGACY_ERROR_CODE_TO_TYPE.get(status) || WebDriverError;

    let value = responseObj['value'];

    if (!value || typeof value !== 'object') {
      throw new ctor(value + '');
    } else {
      let message = value['message'] + '';
      if (ctor !== UnexpectedAlertOpenError) {
        throw new ctor(message);
      }

      let text = '';
      if (value['alert'] && typeof value['alert']['text'] === 'string') {
        text = value['alert']['text'];
      }
      throw new UnexpectedAlertOpenError(message, text);
    }
  }
  return responseObj;
}


// PUBLIC API


module.exports = {
  ErrorCode,

  WebDriverError,
  ElementClickInterceptedError,
  ElementNotInteractableError,
  ElementNotSelectableError,
  InsecureCertificateError,
  InvalidArgumentError,
  InvalidCookieDomainError,
  InvalidCoordinatesError,
  InvalidElementStateError,
  InvalidSelectorError,
  JavascriptError,
  MoveTargetOutOfBoundsError,
  NoSuchAlertError,
  NoSuchCookieError,
  NoSuchElementError,
  NoSuchFrameError,
  NoSuchSessionError,
  NoSuchWindowError,
  ScriptTimeoutError,
  SessionNotCreatedError,
  StaleElementReferenceError,
  TimeoutError,
  UnableToSetCookieError,
  UnableToCaptureScreenError,
  UnexpectedAlertOpenError,
  UnknownCommandError,
  UnknownMethodError,
  UnsupportedOperationError,

  checkResponse,
  checkLegacyResponse,
  encodeError,
  isErrorResponse,
  throwDecodedError,
};
