import { ExpoConfig } from '@expo/config-types';
import fs from 'fs';
import { vol } from 'memfs';
import path from 'path';

import rnFixture from '../../plugins/__tests__/fixtures/react-native-project';
import { format } from '../../utils/XML';
import * as XML from '../../utils/XML';
import { AndroidManifest, getMainApplication } from '../Manifest';
import { readResourcesXMLAsync } from '../Resources';
import * as Updates from '../Updates';

async function getFixtureManifestAsync() {
  return (await XML.parseXMLAsync(
    rnFixture['android/app/src/main/AndroidManifest.xml']
  )) as AndroidManifest;
}

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

jest.mock('fs');
jest.mock('resolve-from');

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

const fsReal = jest.requireActual('fs') as typeof import('fs');

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

  it('set correct values in AndroidManifest.xml', async () => {
    vol.fromJSON({
      '/app/hello': fsReal.readFileSync(sampleCodeSigningCertificatePath, 'utf-8'),
    });

    let androidManifestJson = await getFixtureManifestAsync();
    const config: ExpoConfig = {
      name: 'foo',
      sdkVersion: '37.0.0',
      slug: 'my-app',
      owner: 'owner',
      updates: {
        enabled: false,
        fallbackToCacheTimeout: 2000,
        checkAutomatically: 'ON_ERROR_RECOVERY',
        codeSigningCertificate: 'hello',
        codeSigningMetadata: {
          alg: 'rsa-v1_5-sha256',
          keyid: 'test',
        },
        requestHeaders: {
          'expo-channel-name': 'test',
          testheader: 'test',
        },
      },
    };
    androidManifestJson = await Updates.setUpdatesConfigAsync(
      '/app',
      config,
      androidManifestJson,
      '0.11.0'
    );
    const mainApplication = getMainApplication(androidManifestJson)!;

    if (!mainApplication['meta-data']) {
      throw new Error('No meta-data found in AndroidManifest.xml');
    }

    const updateUrl = mainApplication['meta-data'].filter(
      (e) => e.$['android:name'] === 'expo.modules.updates.EXPO_UPDATE_URL'
    );
    expect(updateUrl).toHaveLength(0);

    const sdkVersion = mainApplication['meta-data'].filter(
      (e) => e.$['android:name'] === 'expo.modules.updates.EXPO_SDK_VERSION'
    );
    expect(sdkVersion).toHaveLength(1);
    expect(sdkVersion[0].$['android:value']).toMatch('37.0.0');

    const enabled = mainApplication['meta-data'].filter(
      (e) => e.$['android:name'] === 'expo.modules.updates.ENABLED'
    );
    expect(enabled).toHaveLength(1);
    expect(enabled[0].$['android:value']).toMatch('false');

    const checkOnLaunch = mainApplication['meta-data'].filter(
      (e) => e.$['android:name'] === 'expo.modules.updates.EXPO_UPDATES_CHECK_ON_LAUNCH'
    );
    expect(checkOnLaunch).toHaveLength(1);
    expect(checkOnLaunch[0].$['android:value']).toMatch('ERROR_RECOVERY_ONLY');

    const timeout = mainApplication['meta-data'].filter(
      (e) => e.$['android:name'] === 'expo.modules.updates.EXPO_UPDATES_LAUNCH_WAIT_MS'
    );
    expect(timeout).toHaveLength(1);
    expect(timeout[0].$['android:value']).toMatch('2000');

    const codeSigningCertificate = mainApplication['meta-data'].filter(
      (e) => e.$['android:name'] === 'expo.modules.updates.CODE_SIGNING_CERTIFICATE'
    );
    expect(codeSigningCertificate).toHaveLength(1);
    expect(codeSigningCertificate[0].$['android:value']).toMatch(
      fsReal.readFileSync(sampleCodeSigningCertificatePath, 'utf-8')
    );

    const codeSigningMetadata = mainApplication['meta-data'].filter(
      (e) => e.$['android:name'] === 'expo.modules.updates.CODE_SIGNING_METADATA'
    );
    expect(codeSigningMetadata).toHaveLength(1);
    expect(codeSigningMetadata[0].$['android:value']).toMatch(
      '{"alg":"rsa-v1_5-sha256","keyid":"test"}'
    );

    const requestHeaders = mainApplication['meta-data'].filter(
      (e) =>
        e.$['android:name'] === 'expo.modules.updates.UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY'
    );
    expect(requestHeaders).toHaveLength(1);
    expect(requestHeaders[0].$['android:value']).toMatch(
      '{"expo-channel-name":"test","testheader":"test"}'
    );

    // For this config, runtime version should not be defined, so check that it does not appear in the manifest
    const runtimeVersion = mainApplication['meta-data']?.filter(
      (e) => e.$['android:name'] === 'expo.modules.updates.EXPO_RUNTIME_VERSION'
    );
    expect(runtimeVersion).toHaveLength(0);
  });

  describe(Updates.ensureBuildGradleContainsConfigurationScript, () => {
    it('adds create-manifest-android.gradle line to build.gradle', async () => {
      vol.fromJSON(
        {
          'android/app/build.gradle': fsReal.readFileSync(
            path.join(__dirname, 'fixtures/build-without-create-manifest-android.gradle'),
            'utf-8'
          ),
          'node_modules/expo-updates/scripts/create-manifest-android.gradle': 'whatever',
        },
        '/app'
      );

      const contents = rnFixture['android/app/build.gradle'];
      const newContents = Updates.ensureBuildGradleContainsConfigurationScript('/app', contents);
      expect(newContents).toMatchSnapshot();
    });

    it('fixes the path to create-manifest-android.gradle in case of a monorepo', async () => {
      // Pseudo node module resolution since actually mocking it could prove challenging.
      // In a yarn workspace, resolve-from would be able to locate a module in any node_module folder if properly linked.
      const resolveFrom = require('resolve-from');
      resolveFrom.silent = (p, a) => {
        return silent(path.join(p, '..'), a);
      };

      vol.fromJSON(
        {
          'workspace/android/app/build.gradle': fsReal.readFileSync(
            path.join(
              __dirname,
              'fixtures/build-with-incorrect-create-manifest-android-path.gradle'
            ),
            'utf-8'
          ),
          'node_modules/expo-updates/scripts/create-manifest-android.gradle': 'whatever',
        },
        '/app'
      );

      const contents = await fs.promises.readFile(
        '/app/workspace/android/app/build.gradle',
        'utf-8'
      );
      const newContents = Updates.ensureBuildGradleContainsConfigurationScript(
        '/app/workspace',
        contents
      );
      expect(newContents).toMatchSnapshot();
    });
  });

  describe('Runtime version tests', () => {
    const sampleStringsXML = `
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<resources>
</resources>`;

    beforeAll(async () => {
      const directoryJSON = {
        './android/app/src/main/res/values/strings.xml': sampleStringsXML,
      };
      vol.fromJSON(directoryJSON, '/app');
    });

    it('Correct metadata written to Android manifest with appVersion policy', async () => {
      vol.fromJSON({
        '/app/hello': fsReal.readFileSync(sampleCodeSigningCertificatePath, 'utf-8'),
      });

      let androidManifestJson = await getFixtureManifestAsync();
      const config: ExpoConfig = {
        name: 'foo',
        version: '37.0.0',
        slug: 'my-app',
        owner: 'owner',
        runtimeVersion: {
          policy: 'appVersion',
        },
      };
      androidManifestJson = await Updates.setUpdatesConfigAsync(
        '/app',
        config,
        androidManifestJson,
        '0.11.0'
      );
      const mainApplication = getMainApplication(androidManifestJson);

      const runtimeVersion = mainApplication!['meta-data']?.filter(
        (e) => e.$['android:name'] === 'expo.modules.updates.EXPO_RUNTIME_VERSION'
      );
      expect(runtimeVersion).toHaveLength(1);
      expect(runtimeVersion && runtimeVersion[0].$['android:value']).toMatch(
        '@string/expo_runtime_version'
      );
    });

    it('Write and clear runtime version in strings resource', async () => {
      const stringsPath = '/app/android/app/src/main/res/values/strings.xml';
      const stringsJSON = await readResourcesXMLAsync({ path: stringsPath });
      const config = {
        runtimeVersion: '1.10',
        modRequest: {
          projectRoot: '/',
        },
      };
      await Updates.applyRuntimeVersionFromConfigAsync(config, stringsJSON);
      expect(format(stringsJSON)).toEqual(
        '<resources>\n  <string name="expo_runtime_version">1.10</string>\n</resources>'
      );

      const config2 = {
        sdkVersion: '1.10',
        modRequest: {
          projectRoot: '/',
        },
      };
      await Updates.applyRuntimeVersionFromConfigAsync(config2, stringsJSON);
      expect(format(stringsJSON)).toEqual('<resources/>');
    });

    afterAll(async () => {
      vol.reset();
    });
  });
});
