import _ from 'lodash';
import {errors} from 'appium/driver';
import moment from 'moment-timezone';
import {LockdownClient} from '../device/lockdown-client';
import type {XCUITestDriver} from '../driver';
import type {Viewport, ScreenInfo, ButtonName} from './types';
import type {Size, Rect} from '@appium/types';
import type {Simulator} from 'appium-ios-simulator';

const MOMENT_FORMAT_ISO8601 = 'YYYY-MM-DDTHH:mm:ssZ';

/**
 * Gets the currently active element.
 *
 * In web context, returns the active element from the DOM.
 * In native context, returns the active element from the current view.
 *
 * @returns The active element
 */
export async function active(this: XCUITestDriver): Promise<any> {
  if (this.isWebContext()) {
    return this.cacheWebElements(await this.executeAtom('active_element', []));
  }
  return await this.proxyCommand(`/element/active`, 'GET');
}

/**
 * Trigger a touch/fingerprint match or match failure.
 *
 * @param match - Whether the match should be a success or failure
 */
export async function touchId(this: XCUITestDriver, match = true): Promise<void> {
  await this.mobileSendBiometricMatch('touchId', match);
}

/**
 * Toggle whether the device is enrolled in the touch ID program.
 *
 * @param isEnabled - Whether to enable or disable the touch ID program
 */
export async function toggleEnrollTouchId(this: XCUITestDriver, isEnabled = true): Promise<void> {
  await this.mobileEnrollBiometric(isEnabled);
}

/**
 * Get the window size.
 *
 * @returns The window size (width and height)
 */
export async function getWindowSize(this: XCUITestDriver): Promise<Size> {
  const {width, height} = await this.getWindowRect();
  return {width, height};
}

/**
 * Retrieves the actual device time.
 *
 * @param format - The format specifier string. Read the [MomentJS documentation](https://momentjs.com/docs/) to get the full list of supported datetime format specifiers. The default format is `YYYY-MM-DDTHH:mm:ssZ`, which complies to ISO-8601.
 * @returns Formatted datetime string or the raw command output (if formatting fails)
 */
export async function getDeviceTime(
  this: XCUITestDriver,
  format = MOMENT_FORMAT_ISO8601,
): Promise<string> {
  this.log.debug('Attempting to capture iOS device date and time');
  if (!this.isRealDevice()) {
    this.log.debug('On simulator. Assuming device time is the same as host time');
    const hostNow = moment();
    const utc = moment.unix(hostNow.unix()).utc();
    return utc.utcOffset(hostNow.utcOffset()).format(format);
  }

  const udid = this.opts.udid;
  if (!udid) {
    throw new errors.UnknownError('Device UDID is required to read time on a real device');
  }

  const lockdown = await LockdownClient.createForDevice(udid, this.opts, this.log);
  let timestamp: number;
  let utcOffset: number;
  try {
    ({timestamp, utcOffset} = await lockdown.getDeviceTimeFields());
  } finally {
    await lockdown.close();
  }
  this.log.debug(`timestamp: ${timestamp}, utcOffset: ${utcOffset}`);
  const utc = moment.unix(timestamp).utc();
  return utc.utcOffset(utcOffset).format(format);
}

/**
 * Retrieves the current device time.
 *
 * This is a wrapper around {@linkcode getDeviceTime}.
 *
 * @param format - See {@linkcode getDeviceTime.format}
 * @returns Formatted datetime string or the raw command output if formatting fails
 */
export async function mobileGetDeviceTime(
  this: XCUITestDriver,
  format = MOMENT_FORMAT_ISO8601,
): Promise<string> {
  return await this.getDeviceTime(format);
}

/**
 * Gets the window rectangle (position and size).
 *
 * For W3C compatibility. In web context, returns the browser window dimensions.
 * In native context, returns the device window dimensions.
 *
 * @returns The window rectangle
 */
export async function getWindowRect(this: XCUITestDriver): Promise<Rect> {
  if (this.isWebContext()) {
    const script =
      'return {' +
      'x: window.screenX || 0,' +
      'y: window.screenY || 0,' +
      'width: window.innerWidth,' +
      'height: window.innerHeight' +
      '}';
    return await this.executeAtom('execute_script', [script]);
  }

  return (await this.proxyCommand('/window/rect', 'GET')) as Rect;
}

/**
 * Removes/uninstalls the given application from the device under test.
 *
 * This is a wrapper around {@linkcode mobileRemoveApp mobile: removeApp}.
 *
 * @param bundleId - The bundle identifier of the application to be removed
 * @returns `true` if the application has been removed successfully; `false` otherwise
 */
export async function removeApp(this: XCUITestDriver, bundleId: string): Promise<boolean> {
  return await this.mobileRemoveApp(bundleId);
}

/**
 * Launches the app.
 *
 * @deprecated This API has been deprecated and is not supported anymore.
 * Consider using corresponding 'mobile:' extensions to manage the state of the app under test.
 * @throws {Error} Always throws an error indicating the API is deprecated
 */
export async function launchApp(this: XCUITestDriver): Promise<void> {
  throw new Error(
    `The launchApp API has been deprecated and is not supported anymore. ` +
      `Consider using corresponding 'mobile:' extensions to manage the state of the app under test.`,
  );
}

/**
 * Closes the app.
 *
 * @deprecated This API has been deprecated and is not supported anymore.
 * Consider using corresponding 'mobile:' extensions to manage the state of the app under test.
 * @throws {Error} Always throws an error indicating the API is deprecated
 */
export async function closeApp(this: XCUITestDriver): Promise<void> {
  throw new Error(
    `The closeApp API has been deprecated and is not supported anymore. ` +
      `Consider using corresponding 'mobile:' extensions to manage the state of the app under test.`,
  );
}

/**
 * Sets the URL for the current session.
 *
 * In web context, navigates to the URL using the remote debugger.
 * In native context on real devices, uses the proxy command.
 * In native context on simulators, uses simctl to open the URL.
 *
 * @param url - The URL to navigate to
 */
export async function setUrl(this: XCUITestDriver, url: string): Promise<void> {
  this.log.debug(`Attempting to set url '${url}'`);

  if (this.isWebContext()) {
    this.setCurrentUrl(url);
    // make sure to clear out any leftover web frames
    this.curWebFrames = [];
    await this.remote.navToUrl(url);
    return;
  }

  if (this.isRealDevice()) {
    await this.proxyCommand('/url', 'POST', {url});
  } else {
    await (this.device as Simulator).simctl.openUrl(url);
  }
}

/**
 * Retrieves the viewport dimensions.
 *
 * The viewport is the device's screen size with status bar size subtracted if the latter is present/visible.
 *
 * @returns The viewport rectangle
 */
export async function getViewportRect(this: XCUITestDriver): Promise<Viewport> {
  const scale = await this.getDevicePixelRatio();
  // status bar height comes in unscaled, so scale it
  const statusBarHeight = Math.trunc((await this.getStatusBarHeight()) * scale);
  const windowSize = await this.getWindowRect();

  // ios returns coordinates/dimensions in logical pixels, not device pixels,
  // so scale up to device pixels. status bar height is already scaled.
  return {
    left: 0,
    top: statusBarHeight,
    width: Math.trunc(windowSize.width * scale),
    height: Math.trunc(windowSize.height * scale) - statusBarHeight,
  };
}

/**
 * Get information about the screen.
 *
 * @privateRemarks memoized in constructor
 * @returns Screen information including dimensions, scale, and status bar size
 */
export async function getScreenInfo(this: XCUITestDriver): Promise<ScreenInfo> {
  return (await this.proxyCommand('/wda/screen', 'GET')) as ScreenInfo;
}

/**
 * Gets the status bar height.
 *
 * @returns The height of the status bar in logical pixels
 */
export async function getStatusBarHeight(this: XCUITestDriver): Promise<number> {
  const {statusBarSize} = await this.getScreenInfo();
  return statusBarSize.height;
}

/**
 * Gets the device pixel ratio.
 *
 * @privateRemarks memoized in constructor
 * @returns The device pixel ratio (scale factor)
 */
export async function getDevicePixelRatio(this: XCUITestDriver): Promise<number> {
  const {scale} = await this.getScreenInfo();
  return scale;
}

/**
 * Emulates press action on the given physical device button.
 *
 * This executes different methods based on the platform:
 *
 * - iOS: [`pressButton:`](https://developer.apple.com/documentation/xctest/xcuidevice/1619052-pressbutton)
 * - tvOS: [`pressButton:`](https://developer.apple.com/documentation/xctest/xcuiremote/1627475-pressbutton) or [`pressButton:forDuration:`](https://developer.apple.com/documentation/xctest/xcuiremote/1627476-pressbutton)
 *
 * Use {@linkcode mobilePerformIoHidEvent} to call a more universal API to perform a button press with duration on any supported device.
 *
 * @param name - The name of the button to be pressed
 * @param durationSeconds - The duration of the button press in seconds (float)
 */
export async function mobilePressButton(
  this: XCUITestDriver,
  name: ButtonName,
  durationSeconds?: number,
): Promise<void> {
  if (!name) {
    throw new errors.InvalidArgumentError('Button name is mandatory');
  }
  if (!_.isNil(durationSeconds) && !_.isNumber(durationSeconds)) {
    throw new errors.InvalidArgumentError('durationSeconds should be a number');
  }
  return await this.proxyCommand('/wda/pressButton', 'POST', {name, duration: durationSeconds});
}

/**
 * Process a string as speech and send it to Siri.
 *
 * Presents the Siri UI, if it is not currently active, and accepts a string which is then processed as if it were recognized speech. See [the documentation of `activateWithVoiceRecognitionText`](https://developer.apple.com/documentation/xctest/xcuisiriservice/2852140-activatewithvoicerecognitiontext?language=objc) for more details.
 *
 * @param text - Text to be sent to Siri
 */
export async function mobileSiriCommand(this: XCUITestDriver, text: string): Promise<void> {
  if (!text) {
    throw new errors.InvalidArgumentError('"text" argument is mandatory');
  }
  await this.proxyCommand('/wda/siri/activate', 'POST', {text});
}
