/**
 * Adds Expo-related mocks to the Jest environment. Jest runs this setup module after it runs the
 * React Native setup module.
 */
'use strict';

const mockNativeModules = require('react-native/Libraries/BatchedBridge/NativeModules');
const createMockConstants = require('./createMockConstants');

// window isn't defined as of react-native 0.45+ it seems
if (typeof window !== 'object') {
  global.window = global;
  global.window.navigator = {};
}

const mockImageLoader = {
  configurable: true,
  enumerable: true,
  get: () => ({
    prefetchImage: jest.fn(),
    getSize: jest.fn((uri, success) => process.nextTick(() => success(320, 240))),
  }),
};
Object.defineProperty(mockNativeModules, 'ImageLoader', mockImageLoader);
Object.defineProperty(mockNativeModules, 'ImageViewManager', mockImageLoader);

Object.defineProperty(mockNativeModules, 'LinkingManager', {
  configurable: true,
  enumerable: true,
  get: () => mockNativeModules.Linking,
});

const mockPlatformConstants = {
  configurable: true,
  enumerable: true,
  get: () => ({
    forceTouchAvailable: true,
  }),
};
Object.defineProperty(mockNativeModules, 'PlatformConstants', mockPlatformConstants);

const publicExpoModules = require('./expoModules');
const internalExpoModules = require('./internalExpoModules');
const expoModules = {
  ...publicExpoModules,
  ...internalExpoModules,
};

function mock(property, customMock) {
  const propertyType = property.type;
  let mockValue;
  if (customMock !== undefined) {
    mockValue = customMock;
  } else if (propertyType === 'function') {
    if (property.functionType === 'promise') {
      mockValue = jest.fn(() => Promise.resolve());
    } else {
      mockValue = jest.fn();
    }
  } else if (propertyType === 'number') {
    mockValue = 1;
  } else if (propertyType === 'string') {
    mockValue = 'mock';
  } else if (propertyType === 'array') {
    mockValue = [];
  } else if (propertyType === 'mock') {
    mockValue = mockByMockDefinition(property.mockDefinition);
  } else {
    mockValue = {};
  }
  return mockValue;
}

function mockProperties(moduleProperties, customMocks) {
  const mockedProperties = {};
  for (let propertyName of Object.keys(moduleProperties)) {
    const property = moduleProperties[propertyName];
    const customMock =
      customMocks && customMocks.hasOwnProperty(propertyName)
        ? customMocks[propertyName]
        : property.mock;
    mockedProperties[propertyName] = mock(property, customMock);
  }
  return mockedProperties;
}

function mockByMockDefinition(definition) {
  const mock = {};
  for (const key of Object.keys(definition)) {
    mock[key] = mockProperties(definition[key]);
  }
  return mock;
}

for (let moduleName of Object.keys(expoModules)) {
  const moduleProperties = expoModules[moduleName];
  const mockedProperties = mockProperties(moduleProperties);

  Object.defineProperty(mockNativeModules, moduleName, {
    configurable: true,
    enumerable: true,
    get: () => mockedProperties,
  });
}

mockNativeModules.NativeUnimoduleProxy.viewManagersNames.forEach(viewManagerName => {
  Object.defineProperty(mockNativeModules.UIManager, `ViewManagerAdapter_${viewManagerName}`, {
    get: () => ({
      NativeProps: {},
      directEventTypes: [],
    }),
  });
});

Object.defineProperty(mockNativeModules.UIManager, 'takeSnapshot', {
  configurable: true,
  enumerable: true,
  writable: true,
  value: jest.fn(),
});

const modulesConstants = mockNativeModules.NativeUnimoduleProxy.modulesConstants;
mockNativeModules.NativeUnimoduleProxy.modulesConstants = {
  ...modulesConstants,
  ExponentConstants: {
    ...modulesConstants.ExponentConstants,
    ...createMockConstants(),
  },
};

try {
  jest.mock('expo-file-system', () => ({
    downloadAsync: jest.fn(() => Promise.resolve({ md5: 'md5', uri: 'uri' })),
    getInfoAsync: jest.fn(() => Promise.resolve({ exists: true, md5: 'md5', uri: 'uri' })),
    readAsStringAsync: jest.fn(() => Promise.resolve()),
    writeAsStringAsync: jest.fn(() => Promise.resolve()),
    deleteAsync: jest.fn(() => Promise.resolve()),
    moveAsync: jest.fn(() => Promise.resolve()),
    copyAsync: jest.fn(() => Promise.resolve()),
    makeDirectoryAsync: jest.fn(() => Promise.resolve()),
    readDirectoryAsync: jest.fn(() => Promise.resolve()),
    createDownloadResumable: jest.fn(() => Promise.resolve()),
  }));
} catch (error) {
  // Allow this module to be optional for bare-workflow
  if (error.code !== 'MODULE_NOT_FOUND') {
    throw error;
  }
}

jest.mock('react-native/Libraries/Image/AssetRegistry', () => ({
  registerAsset: jest.fn(() => 1),
  getAssetByID: jest.fn(() => ({
    fileSystemLocation: '/full/path/to/directory',
    httpServerLocation: '/assets/full/path/to/directory',
    scales: [1],
    fileHashes: ['md5'],
    name: 'name',
    exists: true,
    type: 'type',
    hash: 'md5',
    uri: 'uri',
    width: 1,
    height: 1,
  })),
}));

jest.doMock('react-native/Libraries/BatchedBridge/NativeModules', () => mockNativeModules);

try {
  jest.mock('@unimodules/react-native-adapter', () => {
    const ReactNativeAdapter = require.requireActual('@unimodules/react-native-adapter');
    const { NativeModulesProxy } = ReactNativeAdapter;

    // After the NativeModules mock is set up, we can mock NativeModuleProxy's functions that call
    // into the native proxy module. We're not really interested in checking whether the underlying
    // method is called, just that the proxy method is called, since we have unit tests for the
    // adapter and believe it works correctly.
    //
    // NOTE: The adapter validates the number of arguments, which we don't do in the mocked functions.
    // This means the mock functions will not throw validation errors the way they would in an app.
    for (const moduleName of Object.keys(NativeModulesProxy)) {
      const nativeModule = NativeModulesProxy[moduleName];
      for (const propertyName of Object.keys(nativeModule)) {
        if (typeof nativeModule[propertyName] === 'function') {
          nativeModule[propertyName] = jest.fn(async () => {});
        }
      }
    }

    return ReactNativeAdapter;
  });
} catch (error) {
  // Allow this module to be optional for bare-workflow
  if (error.code !== 'MODULE_NOT_FOUND') {
    throw error;
  }
}

// The UIManager module is not idempotent and causes issues if we load it again after resetting
// the modules with Jest.
let UIManager;
jest.doMock('react-native/Libraries/ReactNative/UIManager', () => {
  if (!UIManager) {
    UIManager = require.requireActual('react-native/Libraries/ReactNative/UIManager');
  }
  return UIManager;
});