import { NativeModulesProxy } from '@unimodules/core';
import { mockProperty, unmockAllProperties, mockPlatformIOS, mockPlatformAndroid } from 'jest-expo';
import * as Location from '../Location';

const fakeReturnValue = {
  coords: {
    latitude: 1,
    longitude: 2,
    altitude: 3,
    accuracy: 4,
    heading: 5,
    speed: 6,
  },
  timestamp: 7,
};

function applyMocks() {
  mockProperty(
    NativeModulesProxy.ExpoLocation,
    'getCurrentPositionAsync',
    jest.fn(async () => fakeReturnValue)
  );
  mockProperty(NativeModulesProxy.ExpoLocation, 'requestPermissionsAsync', jest.fn(async () => {}));
}

describe('Location', () => {
  beforeAll(() => {
    Location.installWebGeolocationPolyfill();
  });

  beforeEach(() => {
    applyMocks();
  });

  afterEach(() => {
    unmockAllProperties();
  });

  describe('getCurrentPositionAsync', () => {
    it('works on Android', async () => {
      mockPlatformAndroid();
      const result = await Location.getCurrentPositionAsync({});
      expect(result).toEqual(fakeReturnValue);
    });

    it('works on iOS', async () => {
      mockPlatformIOS();
      const result = await Location.getCurrentPositionAsync({});
      expect(result).toEqual(fakeReturnValue);
    });
  });

  describe('watchPositionAsync', () => {
    it('receives repeated events', async () => {
      let resolveBarrier;
      const callback = jest.fn();
      const watchBarrier = new Promise(resolve => {
        resolveBarrier = resolve;
      });
      mockProperty(
        NativeModulesProxy.ExpoLocation,
        'watchPositionImplAsync',
        jest.fn(resolveBarrier)
      );
      await Location.watchPositionAsync({}, callback);
      await watchBarrier;

      emitNativeLocationUpdate(fakeReturnValue);
      emitNativeLocationUpdate(fakeReturnValue);
      expect(callback).toHaveBeenCalledTimes(2);
    });
  });

  describe('geocodeAsync', () => {
    // TODO(@tsapeta): This doesn't work due to missing Google Maps API key.
    xit('falls back to Google Maps API on Android without Google Play services', () => {
      mockPlatformAndroid();
      mockProperty(NativeModulesProxy.ExpoLocation, 'geocodeAsync', async () => {
        const error = new Error();
        (error as any).code = 'E_NO_GEOCODER';
        throw error;
      });
      return expect(Location.geocodeAsync('Googleplex')).rejects.toMatchObject({
        code: 'E_NO_GEOCODER',
      });
    });
  });

  describe('reverseGeocodeAsync', () => {
    it('rejects non-numeric latitude/longitude', () => {
      // We need to cast these latitude/longitude strings to any type, so TypeScript diagnostics will pass here.
      return expect(
        Location.reverseGeocodeAsync({ latitude: '37.7' as any, longitude: '-122.5' as any })
      ).rejects.toEqual(expect.any(TypeError));
    });
  });

  describe('navigator.geolocation polyfill', () => {
    beforeEach(() => {
      applyMocks();
    });

    afterEach(() => {
      unmockAllProperties();
    });

    describe('getCurrentPosition', () => {
      it('delegates to getCurrentPositionAsync', async () => {
        let pass;
        const barrier = new Promise(resolve => {
          pass = resolve;
        });
        const options = {};
        navigator.geolocation.getCurrentPosition(pass, pass, options);
        await barrier;
        expect(NativeModulesProxy.ExpoLocation.getCurrentPositionAsync).toHaveBeenCalledWith(
          options
        );
      });
    });

    describe('watchPosition', () => {
      it('watches for updates and stops when clearWatch is called', async () => {
        let resolveBarrier;
        const watchBarrier = new Promise(resolve => {
          resolveBarrier = resolve;
        });
        mockProperty(
          NativeModulesProxy.ExpoLocation,
          'watchPositionImplAsync',
          jest.fn(async () => {
            resolveBarrier();
          })
        );
        const callback = jest.fn();

        const watchId = navigator.geolocation.watchPosition(callback);
        await watchBarrier;

        emitNativeLocationUpdate(fakeReturnValue);
        emitNativeLocationUpdate(fakeReturnValue);
        expect(callback).toHaveBeenCalledTimes(2);

        navigator.geolocation.clearWatch(watchId);
        emitNativeLocationUpdate(fakeReturnValue);
        expect(callback).toHaveBeenCalledTimes(2);
      });
    });
  });
});

function emitNativeLocationUpdate(location) {
  Location.EventEmitter.emit('Expo.locationChanged', {
    watchId: Location._getCurrentWatchId(),
    location,
  });
}