import fs from 'fs';
import { vol } from 'memfs';
import path from 'path';

import {
  getNativeVersion,
  getRuntimeVersionAsync,
  getSDKVersion,
  getUpdatesCheckOnLaunch,
  getUpdatesCodeSigningCertificate,
  getUpdatesCodeSigningMetadata,
  getUpdatesCodeSigningMetadataStringified,
  getUpdatesRequestHeaders,
  getUpdatesRequestHeadersStringified,
  getUpdatesEnabled,
  getUpdatesTimeout,
  getUpdateUrl,
} from '../Updates';

const fsReal = jest.requireActual('fs') as typeof fs;
jest.mock('fs');
jest.mock('resolve-from');

const { silent } = require('resolve-from');

const fixturesPath = path.resolve(__dirname, 'fixtures');
const sampleCodeSigningCertificatePath = path.resolve(fixturesPath, 'codeSigningCertificate.pem');

console.warn = jest.fn();

describe('shared config getters', () => {
  beforeEach(() => {
    const resolveFrom = require('resolve-from');
    resolveFrom.silent = silent;
    vol.reset();
  });

  it(`returns correct default values from all getters if no value provided`, () => {
    expect(getSDKVersion({})).toBe(null);
    expect(getUpdatesCheckOnLaunch({})).toBe('ALWAYS');
    expect(getUpdatesTimeout({})).toBe(0);
    expect(getUpdatesCodeSigningCertificate('/app', {})).toBe(undefined);
    expect(getUpdatesCodeSigningMetadata({})).toBe(undefined);
    expect(getUpdatesRequestHeaders({})).toBe(undefined);

    expect(getUpdatesEnabled({})).toBe(false);
    expect(getUpdatesEnabled({ updates: {} })).toBe(false);
  });

  it(`returns correct value from all getters if value provided`, () => {
    vol.fromJSON({
      '/app/hello': fsReal.readFileSync(sampleCodeSigningCertificatePath, 'utf-8'),
    });

    expect(getSDKVersion({ sdkVersion: '37.0.0' })).toBe('37.0.0');
    expect(getUpdatesCheckOnLaunch({ updates: { checkAutomatically: 'ON_ERROR_RECOVERY' } })).toBe(
      'NEVER'
    );
    expect(
      getUpdatesCheckOnLaunch({ updates: { checkAutomatically: 'ON_ERROR_RECOVERY' } }, '0.11.0')
    ).toBe('ERROR_RECOVERY_ONLY');
    expect(
      getUpdatesCheckOnLaunch({ updates: { checkAutomatically: 'ON_ERROR_RECOVERY' } }, '0.10.15')
    ).toBe('NEVER');
    expect(getUpdatesCheckOnLaunch({ updates: { checkAutomatically: 'ON_LOAD' } })).toBe('ALWAYS');
    expect(getUpdatesCheckOnLaunch({ updates: { checkAutomatically: 'WIFI_ONLY' } })).toBe(
      'WIFI_ONLY'
    );
    expect(getUpdatesCheckOnLaunch({ updates: { checkAutomatically: 'NEVER' } })).toBe('NEVER');
    expect(getUpdatesCheckOnLaunch({ updates: {} })).toBe('ALWAYS');
    expect(getUpdatesEnabled({ updates: { enabled: false } })).toBe(false);
    expect(getUpdatesTimeout({ updates: { fallbackToCacheTimeout: 2000 } })).toBe(2000);
    expect(
      getUpdatesCodeSigningCertificate('/app', {
        updates: {
          codeSigningCertificate: 'hello',
        },
      })
    ).toBe(fsReal.readFileSync(sampleCodeSigningCertificatePath, 'utf-8'));
    expect(
      getUpdatesCodeSigningMetadataStringified({
        updates: {
          codeSigningMetadata: {
            alg: 'rsa-v1_5-sha256',
            keyid: 'test',
          },
        },
      })
    ).toBe(
      JSON.stringify({
        alg: 'rsa-v1_5-sha256',
        keyid: 'test',
      })
    );
    expect(
      getUpdatesCodeSigningMetadata({
        updates: {
          codeSigningMetadata: {
            alg: 'rsa-v1_5-sha256',
            keyid: 'test',
          },
        },
      })
    ).toMatchObject({
      alg: 'rsa-v1_5-sha256',
      keyid: 'test',
    });
    expect(
      getUpdatesRequestHeadersStringified({
        updates: {
          requestHeaders: {
            'expo-channel-name': 'test',
            testheader: 'test',
          },
        },
      })
    ).toBe(
      JSON.stringify({
        'expo-channel-name': 'test',
        testheader: 'test',
      })
    );
    expect(
      getUpdatesRequestHeaders({
        updates: {
          requestHeaders: {
            'expo-channel-name': 'test',
            testheader: 'test',
          },
        },
      })
    ).toMatchObject({
      'expo-channel-name': 'test',
      testheader: 'test',
    });
  });
});

describe(getUpdateUrl, () => {
  it(`returns correct default values from all getters if no value provided.`, () => {
    const url = 'https://u.expo.dev/00000000-0000-0000-0000-000000000000';
    expect(getUpdateUrl({ updates: { url } })).toBe(url);
  });

  it(`returns correct legacy urls if 'updates.url' is not provided, but 'slug' and ('username'|'owner') are provided and useClassicUpdates is false.`, () => {
    expect(getUpdateUrl({})).toBe(null);
  });
});

describe(getNativeVersion, () => {
  const version = '2.0.0';
  const versionCode = 42;
  const buildNumber = '13';
  it('works for android', () => {
    expect(getNativeVersion({ version, android: { versionCode } }, 'android')).toBe(
      `${version}(${versionCode})`
    );
  });
  it('works for ios', () => {
    expect(getNativeVersion({ version, ios: { buildNumber } }, 'ios')).toBe(
      `${version}(${buildNumber})`
    );
  });
  it('throws an error if platform is not recognized', () => {
    const fakePlatform = 'doesnotexist';
    expect(() => {
      getNativeVersion({ version }, fakePlatform as any);
    }).toThrow(`"${fakePlatform}" is not a supported platform. Choose either "ios" or "android".`);
  });
  it('uses the default version if the version is missing', () => {
    expect(getNativeVersion({}, 'ios')).toBe('1.0.0(1)');
  });
  it('uses the default buildNumber if the platform is ios and the buildNumber is missing', () => {
    expect(getNativeVersion({ version }, 'ios')).toBe(`${version}(1)`);
  });
  it('uses the default versionCode if the platform is android and the versionCode is missing', () => {
    expect(getNativeVersion({ version }, 'android')).toBe(`${version}(1)`);
  });
});

describe(getRuntimeVersionAsync, () => {
  it('works if the top level runtimeVersion is a string', async () => {
    const runtimeVersion = '42';
    expect(await getRuntimeVersionAsync('', { runtimeVersion }, 'ios')).toBe(runtimeVersion);
  });
  it('works if the platform specific runtimeVersion is a string', async () => {
    const runtimeVersion = '42';
    expect(await getRuntimeVersionAsync('', { ios: { runtimeVersion } }, 'ios')).toBe(
      runtimeVersion
    );
  });
  it('works if the runtimeVersion is a nativeVersion policy', async () => {
    const version = '1';
    const buildNumber = '2';
    expect(
      await getRuntimeVersionAsync(
        '',
        { version, runtimeVersion: { policy: 'nativeVersion' }, ios: { buildNumber } },
        'ios'
      )
    ).toBe(`${version}(${buildNumber})`);
  });
  it('works if the runtimeVersion is an appVersion policy', async () => {
    const version = '1';
    const buildNumber = '2';
    expect(
      await getRuntimeVersionAsync(
        '',
        { version, runtimeVersion: { policy: 'appVersion' }, ios: { buildNumber } },
        'ios'
      )
    ).toBe(version);
  });
  it('returns null if no runtime version is supplied', async () => {
    expect(await getRuntimeVersionAsync('', {}, 'ios')).toEqual(null);
  });
  it('throws if runtime version is not parseable', async () => {
    await expect(getRuntimeVersionAsync('', { runtimeVersion: 1 } as any, 'ios')).rejects.toThrow(
      `"1" is not a valid runtime version. getRuntimeVersionAsync only supports a string, "sdkVersion", "appVersion", "nativeVersion" or "fingerprintExperimental" policy.`
    );
    await expect(
      getRuntimeVersionAsync('', { runtimeVersion: { policy: 'unsupportedPlugin' } } as any, 'ios')
    ).rejects.toThrow(
      `"{"policy":"unsupportedPlugin"}" is not a valid runtime version. getRuntimeVersionAsync only supports a string, "sdkVersion", "appVersion", "nativeVersion" or "fingerprintExperimental" policy.`
    );
  });
});
