// 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 Regression tests.
 */

'use strict';

const assert = require('assert');
const { execSync } = require("child_process");
const fs = require('fs');
const sinon = require('sinon');
const tempfile = require('tempfile');
const tempy = require('tempy');

const exceptions = require('../exceptions.js');
const helpers = require('./helpers.js');
const scriptMutator = require('../script_mutator.js');

const sandbox = sinon.createSandbox();

const SYNTAX_ERROR_RE = /.*SyntaxError.*/

function createFuzzTest(fake_db, settings, inputFiles) {
  const sources = inputFiles.map(input => helpers.loadTestData(input));

  const mutator = new scriptMutator.ScriptMutator(settings, fake_db);
  const result = mutator.mutateMultiple(sources);

  const output_file = tempfile('.js');
  fs.writeFileSync(output_file, result.code);
  return output_file;
}

function execFile(jsFile) {
  execSync("node " + jsFile, {stdio: ['pipe']});
}

function buildDb(inputDir, corpusName, outputDir) {
  execSync(
      `node build_db.js -i ${inputDir} -o ${outputDir} ${corpusName}`,
      {stdio: ['pipe']});
}

function assertFuzzWithDbThrows(dbInputDir, corpusName, settings, regexp) {
  const outPath = tempy.directory();
  buildDb(dbInputDir, corpusName, outPath);

  settings['MUTATE_CROSSOVER_INSERT'] = 1.0;
  assert.throws(
    () => {
      createFuzzTest(
          outPath, settings,
          ['regress/build_db/cross_over_mutator_input.js']);
    },
    err => {
      assert(regexp.test(err));
      return true;
    },
    'unexpected error',
  );
}

describe('Regression tests', () => {
  beforeEach(() => {
    helpers.deterministicRandom(sandbox);

    this.settings = {
      ADD_VAR_OR_OBJ_MUTATIONS: 0.0,
      MUTATE_CROSSOVER_INSERT: 0.0,
      MUTATE_EXPRESSIONS: 0.0,
      MUTATE_FUNCTION_CALLS: 0.0,
      MUTATE_NUMBERS: 0.0,
      MUTATE_VARIABLES: 0.0,
      engine: 'V8',
      testing: true,
    }
  });

  afterEach(() => {
    sandbox.restore();
  });

  it('combine strict and with', () => {
    // Test that when a file with "use strict" is used in the inputs,
    // the result is only strict if no other file contains anything
    // prohibited in strict mode (here a with statement).
    // It is assumed that such input files are marked as sloppy in the
    // auto generated exceptions.
    sandbox.stub(exceptions, 'getGeneratedSloppy').callsFake(
        () => { return new Set(['regress/strict/input_with.js']); });
    const file = createFuzzTest(
        'test_data/regress/strict/db',
        this.settings,
        ['regress/strict/input_strict.js', 'regress/strict/input_with.js']);
    execFile(file);
  });

  it('combine strict and delete', () => {
    // As above with unqualified delete.
    sandbox.stub(exceptions, 'getGeneratedSloppy').callsFake(
        () => { return new Set(['regress/strict/input_delete.js']); });
    const file = createFuzzTest(
        'test_data/regress/strict/db',
        this.settings,
        ['regress/strict/input_strict.js', 'regress/strict/input_delete.js']);
    execFile(file);
  });

  it('mutates negative value', () => {
    // This tests that the combination of number, function call and expression
    // mutator does't produce an update expression.
    // Previously the 1 in -1 was replaced with another negative number leading
    // to e.g. -/*comment/*-2. Then cloning the expression removed the
    // comment and produced --2 in the end.
    this.settings['MUTATE_NUMBERS'] = 1.0;
    this.settings['MUTATE_FUNCTION_CALLS'] = 1.0;
    this.settings['MUTATE_EXPRESSIONS'] = 1.0;
    const file = createFuzzTest(
        'test_data/regress/numbers/db',
        this.settings,
        ['regress/numbers/input_negative.js']);
    execFile(file);
  });

  it('mutates indices', () => {
    // Test that indices are not replaced with a negative number causing a
    // syntax error (e.g. {-1: ""}).
    this.settings['MUTATE_NUMBERS'] = 1.0;
    const file = createFuzzTest(
        'test_data/regress/numbers/db',
        this.settings,
        ['regress/numbers/input_indices.js']);
    execFile(file);
  });

  it('create call expression', () => {
    // TODO(machenbach): Build_db extracts a function expression without
    // parentheses, re-parsing this later fails in cross-over mutator.
    assertFuzzWithDbThrows(
        'test_data/regress/build_db',
        'destructuring',
        this.settings,
        SYNTAX_ERROR_RE);
  });

  it('create assignment expression', () => {
    // TODO(machenbach): Build_db extracts some assignment expressions with a
    // spurious dependency. This leads to an "unknown substitution" error
    // when applying the template.
    assertFuzzWithDbThrows(
        'test_data/regress/build_db',
        'this',
        this.settings,
        /.*Unknown substitution.*/);
  });
});
