/**
 * This module exposes the utility functions for JET-based WebDriver tests.
 */
import {
  By,
  Condition,
  error,
  Locator,
  WebDriver,
  WebElement,
  WebElementPromise
} from 'selenium-webdriver';

import {
  MarshalableRequestInfo,
  MarshalableRequestInit,
  MarshalableResponseInfo,
  MarshaledResponse,
  OjFetch
} from './lib/OjFetch';

import { checkVersions } from './lib/version-check';
import { Expectation } from './lib/Expectation';
import * as sm from './lib/ScreenshotManager';
import { getBusyStates, pageReady } from './lib/busy-context-utils';
import { assertViewModelValue } from './lib/viewmodel-utils';
import { isNoRequireJet, setNoRequireJet } from './lib/no-require-jet';

// Export register function before importing elements so that they can
// call it to register themselves
export { fetchKeyByFilter } from './lib/dataprovider-utils';
export { DriverManager } from './lib/driver-manager';
export { register } from './lib/driver-override';
export type { SlotProxy } from './lib/slot-proxy';
export { slotProxy } from './lib/slot-proxy';
export { sm as ScreenshotManager };
export { Expectation };

/**
 * This interface describes the functions that are needed from the driver object
 * passed to the factory functions in this utility.  This allows tests to pass
 * either the main WebDriver instance or individual WebElements from which to
 * locate the component elemenet.
 * ###### Using WebDriver instance to locate a button in the document
 * ```javascript
 * const driver = new Builder().build();
 * let button = await ojButton(driver, By.id("mybutton"))
 * ```
 * ###### Using WebElement instance to locate a button underneath a &lt;div>
 * ```javascript
 * let div = await driver.findElement(By.id("my-div"));
 * let button = await ojButton(div, By.css("oj-button"));
 * ```
 */
export interface DriverLike {
  /**
   * Find an element using a given locator strategy
   * @param by The locator strategy to find the element
   */
  findElement(by: Locator): WebElementPromise;
  /**
   * Find a set of elements using a given locator strategy
   * @param by The locator strategy to find the elements
   */
  findElements(by: Locator): Promise<WebElement[]>;
}

/**
 * The test utility object for JET component WebElements.  This object contains
 * all of the factory functions needed to create the component WebElements, plus,
 * additional utilities useful for testing JET-based UIs.  This object should be
 * imported into tests with
 * ```javascript
 * import ojwd from "@oracle/oraclejet-webdriver";
 * ```
 */
const ojwd = {
  /**
   * Flag to indicate if testing environment is running a JET application without
   * RequireJS, such as WebPack. This property can only be set once.
   * Defaults to false.
   */
  noRequireJet: false,
  /**
   * Opens a JET page and wait for the UI to be ready for testing. This method
   * should be used in place of WebDriver.get() to open a page for test. It will
   * wait on the page's BusyContext to clear before continuing on.
   * Note that this method should only be used to open a JET page, one which uses
   * RequireJS. Attempting to open a non-JET page will result in the script
   * timing out waiting for RequireJS to become available.
   *
   * @param driver The WebDriver instance
   * @param url The URL to open
   */
  async get(driver: WebDriver, url: string) {
    await driver.get(url);
    const timeouts = await driver.manage().getTimeouts();
    try {
      await driver.wait(pageReady(), timeouts.pageLoad);
      await checkVersions(driver);
    } catch (ex) {
      if (ex instanceof error.TimeoutError || ex instanceof error.ScriptTimeoutError) {
        const openStates = await getBusyStates(driver);
        ex.message += ` - open busy states: ${openStates}`;
      }
      throw ex;
    }
  },
  /**
   * @deprecated
   * Wait until the JET page indicates that it's loaded and ready.  This function
   * returns a Condition that can be used with WebDriver.wait().
   * ```javascript
   * await driver.wait(ojwd.pageReady())
   * ```
   * This function is deprecated. All WebElements interacting with JET pages
   * should automatically block until the BusyContext is cleared before performing
   * their actions. Therefore, explicitly waiting for the page to be ready should
   * not be needed.
   */
  pageReady(): Condition<Promise<boolean>> {
    console.warn(
      'pageReady() is deprecated and will be removed in the next release. ' +
        'Use ojwd.get() to open and wait for the page to be ready for testing'
    );
    return pageReady();
  },

  /**
   * Invoke fetch in the browser to permit interaction with REST services.
   * Browsers must support fetch api.
   *
   * The fetch parameters mirror the MDN fetch api
   * https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch#Parameters
   *
   * Note the return value is not the typical Response object, to accommodate webdriver marshalling.
   *
   * @param {WebDriver} driver A WebDriver-like object
   * @param {MarshalableRequestInfo} input String containing the direct URL of the resource you want to fetch.
   * @param {MarshalableResponseInfo} responseInfo Indicates the desired structure of the response body, text or json
   * @param {MarshalableRequestInit} init Optional
   *  An options object containing any custom settings that you want to apply to the request.
   * @param {string} init.method: The request method, e.g., GET, POST. Note that the Origin header is not set on Fetch requests with a method of HEAD or GET (this behavior was corrected in Firefox 65 — seebug 1508661).
   * @param {MarshalableHeaders} init.headers: Any headers you want to add to your request, contained within a MarshalableHeaders object
   * @param {MarshalableBody} init.body: Any body that you want to add to your request, json or text, in a MarshalableBody object
   * @param {string} init.mode: The mode you want to use for the request, e.g., cors, no-cors, or same-origin.
   * @param {string} init.credentials: The request credentials you want to use for the request: omit, same-origin, or include. To automatically send cookies for the current domain, this option must be provided. Starting with Chrome 50, this property also takes a FederatedCredential instance or a PasswordCredential instance.
   * @return {Promise<MarshaledResponse>} A Promise resolving to a MarshaledResponse object
   *
   * ###### Example of READ and UPDATE operations
   * ```javascript
   *     let dept:any;
   *     try {
   *       let res:MarshaledResponse = await ojwd.fetch(driver,restServer + 'Departments/?DepartmentId=20',
   *          { "responseBodyAs": "json" } );
   *       assert(res.status == 200,"Status code should be 200, was " + res.status);
   *       assert(res.body.kind == "json","Body kind should be 'json', was " + res.body.kind);
   *       assert(res.body.body[0].DepartmentName == "Marketing","Dept 20 should be named 'Marketing' not " + res.body.body[0].DepartmentName);
   *       dept = res.body.body[0];
   *
   *       dept.DepartmentName = "Product Marketing";
   *       res = await ojwd.fetch(driver,restServer + 'Departments/30', { "responseBodyAs": "json" }, {
   *         method: 'put',
   *         headers: { "Content-Type": "application/json" },
   *         body: {"kind":"json", "body": JSON.stringify(dept)}
   *       });
   *       assert(res.status == 200,"Status code should be 200, was " + res.status + " body = " + JSON.stringify(res.body,null,2));
   *     } catch(err) {
   *       assert(false, "fetch failed : " + err);
   *     }
   * ```
   *
   * ###### Example of CREATE and DELETE operations
   * ```javascript
   *     try {
   *       let res:MarshaledResponse = await ojwd.fetch(driver,restServer + 'Departments/',
   *          { "responseBodyAs": "json" },
   *          { method: 'post',
   *          headers: { "Content-Type": "application/json"},
   *          body: { "kind": "json", "body": JSON.stringify(newdept)} });
   *        assert(res.status == 201,"Status code should be 201, was " + res.status);
   *
   *        res = await ojwd.fetch(driver,restServer + 'Departments/190',
   *          { "responseBodyAs": "text" },
   *          { method: 'delete' });
   *        assert(res.status == 200,"Status code should be 200, was " + res.status);
   *       } catch(err) {
   *        assert(false, "fetch failed : " + err);
   *       }
   * ```
   *
   * ###### Example of Checking for row deleted operation
   * ```javascript
   *       try {
   *         let res:MarshaledResponse = await ojwd.fetch(driver,restServer + 'Departments/400',
   *           { "responseBodyAs": "json" });
   *         assert(res.status == 404,"Status code should be 404, was " + res.status);
   *       } catch(err) {
   *         assert(false, "fetch failed : " + err);
   *       }
   * ```
   *
   * ###### Example of MarshaledResponse object for READ fetch
   * ```javascript
   * {
   *  "body": {
   *    "body": [
   *     {
   *        "DepartmentId": 20,
   *        "DepartmentName": "Marketing",
   *        "LocationId": null,
   *         "ManagerId": null
   *      }
   *    ],
   *     "kind": "json"
   *   },
   *   "headers": {
   *    "cache-control": "no-cache",
   *     "content-type": "application/json; charset=utf-8",
   *     "expires": "-1",
   *     "pragma": "no-cache"
   *   },
   *   "ok": true,
   *   "redirected": false,
   *   "status": 200,
   *   "statusText": "OK",
   *   "type": "cors",
   *   "url": "http://localhost:3000/Departments/?DepartmentId=20"
   *   }
   * ```
   *
   */
  async fetch(
    driver: WebDriver,
    requestInfo: MarshalableRequestInfo,
    responseInfo: MarshalableResponseInfo,
    init?: MarshalableRequestInit
  ): Promise<MarshaledResponse> {
    let ojFetch: OjFetch = new OjFetch();
    return ojFetch.fetch(driver, requestInfo, responseInfo, init);
  },

  /**
   * This is a convenience function to allow either WebDriver or WebElement to
   * be passed as the driver and the search to be performed using that object.
   * @param driver A WebDriver-like object, either WebDriver itself or a
   * WebElement. The target element will be searched using this driver.
   * @param by The locator by which to find the element
   * @deprecated Since 11.0.0 This method is equivalient to calling
   * driver.findElement(by)
   */
  async waitAndFindElement(driver: DriverLike, by: By): Promise<WebElement> {
    // findElement will wait on busy context before executing
    return driver.findElement(by);
  },

  assertViewModelValue
};

Object.defineProperties(ojwd, {
  /*
   * Add a guard to only set the noRequireJet flag once
   */
  noRequireJet: {
    set: function (v) {
      if (isNoRequireJet() === undefined) {
        setNoRequireJet(v);
      } else {
        throw Error('noRequireJet cannot be set more than once');
      }
    },
    get: isNoRequireJet,
    enumerable: false,
    configurable: false
  }
});

export default ojwd;
