// Copyright 2020 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

/**
 * @fileoverview Script mutator for differential fuzzing.
 */

'use strict';

const assert = require('assert');
const fs = require('fs');
const path = require('path');

const common = require('./mutators/common.js');
const random = require('./random.js');
const sourceHelpers = require('./source_helpers.js');

const { filterDifferentialFuzzFlags } = require('./exceptions.js');
const { DifferentialFuzzMutator, DifferentialFuzzSuppressions } = require(
    './mutators/differential_fuzz_mutator.js');
const { ScriptMutator } = require('./script_mutator.js');


const USE_ORIGINAL_FLAGS_PROB = 0.2;

/**
 * Randomly chooses a configuration from experiments. The configuration
 * parameters are expected to be passed from a bundled V8 build. Constraints
 * mentioned below are enforced by PRESUBMIT checks on the V8 side.
 *
 * @param {Object[]} experiments List of tuples (probability, first config name,
 *     second config name, second d8 name). The probabilities are integers in
 *     [0,100]. We assume the sum of all probabilities is 100.
 * @param {Object[]} additionalFlags List of tuples (probability, flag strings).
 *     Probability is in [0,1).
 * @return {string[]} List of flags for v8_foozzie.py.
 */
function chooseRandomFlags(experiments, additionalFlags) {
  // Add additional flags to second config based on experiment percentages.
  const extra_flags = [];
  for (const [p, flags] of additionalFlags) {
    if (random.choose(p)) {
      for (const flag of flags.split(' ')) {
        extra_flags.push('--second-config-extra-flags=' + flag);
      }
    }
  }

  // Calculate flags determining the experiment.
  let acc = 0;
  const threshold = random.random() * 100;
  for (let [prob, first_config, second_config, second_d8] of experiments) {
    acc += prob;
    if (acc > threshold) {
      return [
        '--first-config=' + first_config,
        '--second-config=' + second_config,
        '--second-d8=' + second_d8,
      ].concat(extra_flags);
    }
  }
  // Unreachable.
  assert(false);
}

function loadJSONFromBuild(name) {
  assert(process.env.APP_DIR);
  const fullPath = path.join(path.resolve(process.env.APP_DIR), name);
  return JSON.parse(fs.readFileSync(fullPath, 'utf-8'));
}

function hasMjsunit(dependencies) {
  return dependencies.some(dep => dep.relPath.endsWith('mjsunit.js'));
}

function hasJSTests(dependencies) {
  return dependencies.some(dep => dep.relPath.endsWith('jstest_stubs.js'));
}

class DifferentialScriptMutator extends ScriptMutator {
  constructor(settings, db_path) {
    super(settings, db_path);

    // Mutators for differential fuzzing.
    this.differential = [
      new DifferentialFuzzSuppressions(settings),
      new DifferentialFuzzMutator(settings),
    ];

    // Flag configurations from the V8 build directory.
    this.experiments = loadJSONFromBuild('v8_fuzz_experiments.json');
    this.additionalFlags = loadJSONFromBuild('v8_fuzz_flags.json');
  }

  /**
   * Performes the high-level mutation and afterwards adds flags for the
   * v8_foozzie.py harness.
   */
  mutateMultiple(inputs) {
    const result = super.mutateMultiple(inputs);
    const originalFlags = [];

    // Keep original JS flags in some cases. Let the harness pass them to
    // baseline _and_ comparison run.
    if (random.choose(USE_ORIGINAL_FLAGS_PROB)) {
      for (const flag of filterDifferentialFuzzFlags(result.flags)) {
        originalFlags.push('--first-config-extra-flags=' + flag);
        originalFlags.push('--second-config-extra-flags=' + flag);
      }
    }

    // Add flags for the differnetial-fuzzing settings.
    const fuzzFlags = chooseRandomFlags(this.experiments, this.additionalFlags);
    result.flags = fuzzFlags.concat(originalFlags);
    return result;
  }

  /**
   * Mutatates a set of inputs.
   *
   * Additionally we prepare inputs by tagging each with the original source
   * path for later printing. The mutated sources are post-processed by the
   * differential-fuzz mutators, adding extra printing and other substitutions.
   */
  mutateInputs(inputs) {
    inputs.forEach(input => common.setOriginalPath(input, input.relPath));

    const result = super.mutateInputs(inputs);
    this.differential.forEach(mutator => mutator.mutate(result));
    return result;
  }

  /**
   * Adds extra dependencies for differential fuzzing.
   */
  resolveDependencies(inputs) {
    const dependencies = super.resolveDependencies(inputs);
    // The suppression file neuters functions not working with differential
    // fuzzing. It can also be used to temporarily silence some functionality
    // leading to dupes of an active bug.
    dependencies.push(
        sourceHelpers.loadResource('differential_fuzz_suppressions.js'));
    // Extra printing and tracking functionality.
    dependencies.push(
        sourceHelpers.loadResource('differential_fuzz_library.js'));
    // Make Chakra tests print more.
    dependencies.push(
        sourceHelpers.loadResource('differential_fuzz_chakra.js'));

    if (hasMjsunit(dependencies)) {
      // Make V8 tests print more. We guard this as the functionality
      // relies on mjsunit.js.
      dependencies.push(sourceHelpers.loadResource('differential_fuzz_v8.js'));
    }

    if (hasJSTests(dependencies)) {
      dependencies.push(
          sourceHelpers.loadResource('differential_fuzz_jstest.js'));
    }

    return dependencies;
  }
}

module.exports = {
  DifferentialScriptMutator: DifferentialScriptMutator,
};