/**
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @format
 */

/**
 * This script is meant to be used only in CircleCI to compute which tests we should execute
 * based on the files modified by the commit.
 */

const {execSync} = require('child_process');
const fs = require('fs');
const yargs = require('yargs');

/**
 * Check whether the filename is a JS/TS file and not in the script folder
 */
function isJSChange(name) {
  const isJS =
    name.endsWith('.js') ||
    name.endsWith('.jsx') ||
    name.endsWith('.ts') ||
    name.endsWith('.tsx');
  const notAScript = name.indexOf('scripts/') < 0;
  return isJS && notAScript;
}

function isIOS(name) {
  return (
    name.indexOf('React/') > -1 ||
    name.endsWith('.rb') ||
    name.endsWith('.podspec')
  );
}

/**
 * This maps some high level pipelines to some conditions.
 * If those conditions are true, we are going to run the associated pipeline in CI.
 * So, for example, is the commit the CI is analyzing only contains changes in the React/ folder or changes to ruby files
 * the RUN_IOS flag will be turned on and we will run only the CI suite for iOS tests.
 *
 * This mapping is not final and we can update it with more pipelines or we can update the filter condition in the future.
 */
const mapping = [
  {
    name: 'RUN_IOS',
    filterFN: name => isIOS(name),
  },
  {
    name: 'RUN_ANDROID',
    filterFN: name => name.indexOf('ReactAndroid/') > -1,
  },
  {
    name: 'RUN_JS',
    filterFN: name => isJSChange(name),
  },
  {
    name: 'SKIP',
    filterFN: name => name.endsWith('.md'),
  },
];

// ===== //
// Yargs //
// ===== //

const createConfigsOption = {
  input_path: {
    alias: 'i',
    describe: 'The path to the folder where the Config parts are located',
    default: '.circleci/configurations',
  },
  output_path: {
    alias: 'o',
    describe: 'The path where the `generated_config.yml` will be created',
    default: '.circleci',
  },
  config_file: {
    alias: 'c',
    describe: 'The configuration file with the parameter to set',
    default: '/tmp/circleci/pipeline_config.json',
  },
};

const filterJobsOptions = {
  output_path: {
    alias: 'o',
    ddescribe: 'The output path for the configurations',
    default: '/tmp/circleci',
  },
};

yargs
  .command(
    'create-configs',
    'Creates the circleci config from its files',
    createConfigsOption,
    argv => createConfigs(argv.input_path, argv.output_path, argv.config_file),
  )
  .command(
    'filter-jobs',
    'Filters the jobs based on the list of chaged files in the PR',
    filterJobsOptions,
    argv => filterJobs(argv.output_path),
  )
  .demandCommand()
  .strict()
  .help().argv;

// ============== //
//  GITHUB UTILS  //
// ============== //

function _getFilesFromGit() {
  try {
    execSync('git fetch');
    const commonCommit = String(
      execSync('git merge-base HEAD origin/HEAD '),
    ).trim();
    return String(execSync(`git diff --name-only HEAD ${commonCommit}`))
      .trim()
      .split('\n');
  } catch (error) {
    console.error(error);
    return [];
  }
}

function _shouldRunE2E() {
  try {
    const gitCommand = 'git log -1 --pretty=format:"%B" | head -n 1';
    const commitMessage = String(execSync(gitCommand)).trim();
    console.log(`commitMessage: ${commitMessage}`);
    return commitMessage.includes('#run-e2e-tests');
  } catch (error) {
    console.error(error);
    return false;
  }
}

function _computeAndSavePipelineParameters(
  pipelineType,
  outputPath,
  shouldRunE2E,
) {
  fs.mkdirSync(outputPath, {recursive: true});
  const filePath = `${outputPath}/pipeline_config.json`;

  if (pipelineType === 'SKIP') {
    fs.writeFileSync(filePath, JSON.stringify({}, null, 2));
    return;
  }

  console.log(`Should run e2e? ${shouldRunE2E}`);
  if (pipelineType === 'ALL') {
    fs.writeFileSync(
      filePath,
      JSON.stringify({run_all: true, run_e2e: shouldRunE2E}, null, 2),
    );
    return;
  }

  const params = {
    run_all: false,
    run_ios: pipelineType === 'RUN_IOS',
    run_android: pipelineType === 'RUN_ANDROID',
    run_js: pipelineType === 'RUN_JS',
    run_e2e: shouldRunE2E,
  };

  const stringifiedParams = JSON.stringify(params, null, 2);
  fs.writeFileSync(filePath, stringifiedParams);
  console.info(`Generated params:\n${stringifiedParams}`);
}

// ============== //
//    COMMANDS    //
// ============== //

function createConfigs(inputPath, outputPath, configFile) {
  // Order is important!
  const baseConfigFiles = [
    'top_level.yml',
    'executors.yml',
    'commands.yml',
    'jobs.yml',
    'workflows.yml',
  ];

  const baseFolder = 'test_workflows';
  const testConfigs = {
    run_ios: ['testIOS.yml'],
    run_android: ['testAndroid.yml'],
    run_e2e: ['testE2E.yml'],
    run_all: ['testJS.yml', 'testAll.yml'],
    run_js: ['testJS.yml'],
  };

  if (!fs.existsSync(configFile)) {
    throw new Error(`Config file: ${configFile} does not exists`);
  }

  const jsonParams = JSON.parse(fs.readFileSync(configFile));
  let configurationPartsFiles = baseConfigFiles;

  for (const [key, shouldTest] of Object.entries(jsonParams)) {
    if (shouldTest) {
      const mappedFiles = testConfigs[key].map(f => `${baseFolder}/${f}`);
      configurationPartsFiles = configurationPartsFiles.concat(mappedFiles);
    }
  }
  console.log(configurationPartsFiles);
  let configParts = [];
  configurationPartsFiles.forEach(yamlPart => {
    const fileContent = fs.readFileSync(`${inputPath}/${yamlPart}`);
    configParts.push(fileContent);
  });
  console.info(`writing to: ${outputPath}/generated_config.yml`);
  fs.writeFileSync(
    `${outputPath}/generated_config.yml`,
    configParts.join('\n'),
  );
}

function filterJobs(outputPath) {
  const fileList = _getFilesFromGit();

  if (fileList.length === 0) {
    _computeAndSavePipelineParameters('SKIP', outputPath);
    return;
  }

  const shouldRunE2E = _shouldRunE2E();

  for (const filter of mapping) {
    const found = fileList.every(filter.filterFN);
    if (found) {
      _computeAndSavePipelineParameters(filter.name, outputPath, shouldRunE2E);
      return;
    }
  }
  _computeAndSavePipelineParameters('ALL', outputPath, shouldRunE2E);
}
