import {services} from 'appium-ios-device';
import {errors} from 'appium/driver';
import {util} from 'appium/support';
import {AuthorizationStatus} from './enum';
import { isIos17OrNewer } from '../utils';
import type {XCUITestDriver} from '../driver';
import type {Location} from '@appium/types';
import type {LocationWithAltitude, WDALocationInfo} from './types';
import type {Simulator} from 'appium-ios-simulator';

/**
 * Returns location of the device under test.
 * The device under test must allow the location services for WDA
 * as 'Always' to get the location data correctly.
 *
 * The 'latitude', 'longitude' and 'altitude' could be zero even
 * if the Location Services are set to 'Always', because the device
 * needs some time to update the location data.
 *
 * For iOS 17, the return value could be the result of
 * "mobile:getSimulatedLocation" if the simulated location has been previously set
 * "mobile:setSimulatedLocation" already.
 *
 * @returns Location with altitude
 * @throws {Error} If the device under test returns an error message.
 *                 i.e.: tvOS returns unsupported error
 */
export async function getGeoLocation(this: XCUITestDriver): Promise<LocationWithAltitude> {
  // Currently we proxy the setGeoLocation to mobile:setSimulatedLocation for iOS 17+.
  // It would be helpful to address to use "mobile:getSimulatedLocation" for iOS 17+.
  if (isIos17OrNewer(this.opts)) {
    const {latitude, longitude} = await this.mobileGetSimulatedLocation();
    if (latitude && longitude) {
      this.log.debug('Returning the geolocation that has been previously set by mobile:setSimulatedLocation. ' +
        'mobile:resetSimulatedLocation can reset the location configuration.');
      return {latitude, longitude, altitude: 0};
    }

    this.log.warn(`No location was set by mobile:setSimulatedLocation. Trying to return the location from the device.`);
  }

  // Please do not change the way to get the location here with '/wda/simulatedLocation'
  // endpoint because they could return different value before setting the simulated location.
  // '/wda/device/location' returns current device location information,
  // but '/wda/simulatedLocation' returns `null` values until the WDA process
  // sets a simulated location. After setting the value, both returns the same values.
  const {authorizationStatus, latitude, longitude, altitude} = await this.proxyCommand('/wda/device/location', 'GET') as WDALocationInfo;

  // '3' is 'Always' in the privacy
  // https://developer.apple.com/documentation/corelocation/clauthorizationstatus
  if (authorizationStatus !== AuthorizationStatus.authorizedAlways) {
    throw this.log.errorWithException(
      `Location service must be set to 'Always' in order to ` +
        `retrieve the current geolocation data. Please set it up manually via ` +
        `'Settings > Privacy > Location Services -> WebDriverAgentRunner-Runner'. ` +
        `Or please use 'mobile:getSimulatedLocation'/'mobile:setSimulatedLocation' commands ` +
        `to simulate locations instead.`,
    );
  }

  return {latitude, longitude, altitude};
}

/**
 * Set location of the device under test.
 *
 * iOS 17+ real device environment will be via "mobile:setSimulatedLocation" as
 * setting simulated location for XCTest session.
 *
 * @param location - Location with latitude and longitude
 */
export async function setGeoLocation(
  this: XCUITestDriver,
  location: Partial<Location>,
): Promise<Location> {
  const {latitude, longitude} = location;

  if (!util.hasValue(latitude) || !util.hasValue(longitude)) {
    throw new errors.InvalidArgumentError(`Both latitude and longitude should be set`);
  }

  if (this.isSimulator()) {
    await (this.device as Simulator).setGeolocation(`${latitude}`, `${longitude}`);
    return {latitude, longitude, altitude: 0};
  }

  if (isIos17OrNewer(this.opts)) {
    this.log.info(`Proxying to mobile:setSimulatedLocation method for iOS 17+`);
    await this.mobileSetSimulatedLocation(latitude, longitude);
  } else {
    const service = await services.startSimulateLocationService(this.opts.udid);
    try {
      service.setLocation(latitude, longitude);
    } catch (e: any) {
      throw this.log.errorWithException(
        `Can't set the location on device '${this.opts.udid}'. Original error: ${e.message}`,
      );
    } finally {
      service.close();
    }
  }

  return {latitude, longitude, altitude: 0};
}

/**
 * Reset the location service on real device.
 *
 * @throws {Error} If the device is simulator and iOS version is below 17,
 * or 'resetLocation' raises an error.
 */
export async function mobileResetLocationService(this: XCUITestDriver): Promise<void> {
  if (isIos17OrNewer(this.opts)) {
    this.log.info(`Proxying to mobile:resetSimulatedLocation method for iOS 17+`);
    await this.mobileResetSimulatedLocation();
    return;
  }

  if (this.isSimulator()) {
    throw new errors.NotImplementedError();
  }

  const service = await services.startSimulateLocationService(this.opts.udid);
  try {
    service.resetLocation();
  } catch (err: any) {
    throw this.log.errorWithException(
      `Failed to reset the location on the device on device '${this.opts.udid}'. ` +
        `Original error: ${err.message}`,
    );
  } finally {
    service.close();
  }
}

