/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

// NOTE: If you're adding new test harness functionality -- first, should you
//       at all?  Most stuff is better in specific tests, or in nested shell.js
//       or browser.js.  Second, supposing you should, please add it to this
//       IIFE for better modularity/resilience against tests that must do
//       particularly bizarre things that might break the harness.

(function(global) {
  /**********************************************************************
   * CACHED PRIMORDIAL FUNCTIONALITY (before a test might overwrite it) *
   **********************************************************************/

  var undefined; // sigh

  var Error = global.Error;
  var Number = global.Number;
  var String = global.String;
  var TypeError = global.TypeError;

  var ArrayIsArray = global.Array.isArray;
  var ObjectCreate = global.Object.create;
  var ObjectDefineProperty = global.Object.defineProperty;
  var ReflectApply = global.Reflect.apply;
  var StringPrototypeEndsWith = global.String.prototype.endsWith;

  var runningInBrowser = typeof global.window !== "undefined";
  if (runningInBrowser) {
    // Certain cached functionality only exists (and is only needed) when
    // running in the browser.  Segregate that caching here.

    var SpecialPowersSetGCZeal =
      global.SpecialPowers ? global.SpecialPowers.setGCZeal : undefined;
  }

  var runningInShell = typeof window === "undefined";

  /****************************
   * GENERAL HELPER FUNCTIONS *
   ****************************/

  // We could use Array.prototype.pop, but we don't so it's clear exactly what
  // dependencies this function has on test-modifiable behavior (i.e. none).
  function ArrayPop(arr) {
    assertEq(ArrayIsArray(arr), true,
             "ArrayPop must only be used on actual arrays");

    var len = arr.length;
    if (len === 0)
      return undefined;

    var v = arr[len - 1];
    arr.length--;
    return v;
  }

  // We *cannot* use Array.prototype.push for this, because that function sets
  // the new trailing element, which could invoke a setter (left by a test) on
  // Array.prototype or Object.prototype.
  function ArrayPush(arr, val) {
    assertEq(ArrayIsArray(arr), true,
             "ArrayPush must only be used on actual arrays");

    var desc = ObjectCreate(null);
    desc.value = val;
    desc.enumerable = true;
    desc.configurable = true;
    desc.writable = true;
    ObjectDefineProperty(arr, arr.length, desc);
  }

  function StringEndsWith(str, needle) {
    return ReflectApply(StringPrototypeEndsWith, str, [needle]);
  }

  /****************************
   * TESTING FUNCTION EXPORTS *
   ****************************/

  function SameValue(v1, v2) {
    // We could |return Object.is(v1, v2);|, but that's less portable.
    if (v1 === 0 && v2 === 0)
      return 1 / v1 === 1 / v2;
    if (v1 !== v1 && v2 !== v2)
      return true;
    return v1 === v2;
  }

  var assertEq = global.assertEq;
  if (typeof assertEq !== "function") {
    assertEq = function assertEq(actual, expected, message) {
      if (!SameValue(actual, expected)) {
        throw new TypeError('Assertion failed: got "' + actual + '", ' +
                            'expected "' + expected +
                            (message ? ": " + message : ""));
      }
    };
    global.assertEq = assertEq;
  }

  function assertEqArray(actual, expected) {
    var len = actual.length;
    assertEq(len, expected.length, "mismatching array lengths");

    var i = 0;
    try {
      for (; i < len; i++)
        assertEq(actual[i], expected[i], "mismatch at element " + i);
    } catch (e) {
      throw new Error("Exception thrown at index " + i + ": " + e);
    }
  }
  global.assertEqArray = assertEqArray;

  function assertThrows(f) {
    var ok = false;
    try {
      f();
    } catch (exc) {
      ok = true;
    }
    if (!ok)
      throw new Error("Assertion failed: " + f + " did not throw as expected");
  }
  global.assertThrows = assertThrows;

  function assertThrowsInstanceOf(f, ctor, msg) {
    var fullmsg;
    try {
      f();
    } catch (exc) {
      if (exc instanceof ctor)
        return;
      fullmsg =
        "Assertion failed: expected exception " + ctor.name + ", got " + exc;
    }

    if (fullmsg === undefined) {
      fullmsg =
        "Assertion failed: expected exception " + ctor.name + ", " +
        "no exception thrown";
    }
    if (msg !== undefined)
      fullmsg += " - " + msg;

    throw new Error(fullmsg);
  }
  global.assertThrowsInstanceOf = assertThrowsInstanceOf;

  /****************************
   * UTILITY FUNCTION EXPORTS *
   ****************************/

  var dump = global.dump;
  if (typeof global.dump === "function") {
    // A presumptively-functional |dump| exists, so no need to do anything.
  } else {
    // We don't have |dump|.  Try to simulate the desired effect another way.
    if (runningInBrowser) {
      // We can't actually print to the console: |global.print| invokes browser
      // printing functionality here (it's overwritten just below), and
      // |global.dump| isn't a function that'll dump to the console (presumably
      // because the preference to enable |dump| wasn't set).  Just make it a
      // no-op.
      dump = function() {};
    } else {
      // |print| prints to stdout: make |dump| do likewise.
      dump = global.print;
    }
    global.dump = dump;
  }

  var print;
  if (runningInBrowser) {
    // We're executing in a browser.  Using |global.print| would invoke browser
    // printing functionality: not what tests want!  Instead, use a print
    // function that syncs up with the test harness and console.
    print = function print() {
      var s = "TEST-INFO | ";
      for (var i = 0; i < arguments.length; i++)
        s += String(arguments[i]) + " ";

      // Dump the string to the console for developers and the harness.
      dump(s + "\n");

      // AddPrintOutput doesn't require HTML special characters be escaped.
      global.AddPrintOutput(s);
    };

    global.print = print;
  } else {
    // We're executing in a shell, and |global.print| is the desired function.
    print = global.print;
  }

  var quit = global.quit;
  if (typeof quit !== "function") {
    // XXX There's something very strange about quit() in browser runs being a
    //     function that doesn't quit at all (!).  We should get rid of quit()
    //     as an integral part of tests in favor of something else.
    quit = function quit() {};
    global.quit = quit;
  }

  var gczeal = global.gczeal;
  if (typeof gczeal !== "function") {
    if (typeof SpecialPowersSetGCZeal === "function") {
      gczeal = function gczeal(z) {
        SpecialPowersSetGCZeal(z);
      };
    } else {
      gczeal = function() {}; // no-op if not available
    }

    global.gczeal = gczeal;
  }

  /******************************************************
   * TEST METADATA EXPORTS (these are of dubious value) *
   ******************************************************/

  global.SECTION = "";
  global.VERSION = "";
  global.BUGNUMBER = "";

  /*************************************************************************
   * HARNESS-CENTRIC EXPORTS (we should generally work to eliminate these) *
   *************************************************************************/

  var PASSED = " PASSED! ";
  global.PASSED = PASSED;

  var FAILED = " FAILED! ";
  global.FAILED = FAILED;

  /** Set up test environment. */
  function startTest() {
    if (global.BUGNUMBER)
      global.print("BUGNUMBER: " + global.BUGNUMBER);
  }
  global.startTest = startTest;

  var callStack = [];

  /**
   * Puts funcName at the top of the call stack.  This stack is used to show
   * a function-reported-from field when reporting failures.
   */
  function enterFunc(funcName) {
    assertEq(typeof funcName, "string",
             "enterFunc must be given a string funcName");

    if (!StringEndsWith(funcName, "()"))
      funcName += "()";

    ArrayPush(callStack, funcName);
  }
  global.enterFunc = enterFunc;

  /**
   * Pops the top funcName off the call stack.  funcName, if provided, is used
   * to check push-pop balance.
   */
  function exitFunc(funcName) {
    assertEq(typeof funcName === "string" || typeof funcName === "undefined",
             true,
             "exitFunc must be given no arguments or a string");

    var lastFunc = ArrayPop(callStack);
    assertEq(typeof lastFunc, "string", "exitFunc called too many times");

    if (funcName) {
      if (!StringEndsWith(funcName, "()"))
        funcName += "()";

      if (lastFunc !== funcName) {
        // XXX Eliminate this dependency on global.reportCompare's identity.
        global.reportCompare(funcName, lastFunc,
                             "Test driver failure wrong exit function ");
      }
    }
  }
  global.exitFunc = exitFunc;

  /** Peeks at the top of the call stack. */
  function currentFunc() {
    if (callStack.length == 0)
      return "top level script";

    return callStack[callStack.length - 1];
  }
  global.currentFunc = currentFunc;

  // XXX This function is *only* used in harness functions and really shouldn't
  //     be exported.
  var writeFormattedResult =
    function writeFormattedResult(expect, actual, string, passed) {
      print((passed ? PASSED : FAILED) + string + ' expected: ' + expect);
    };
  global.writeFormattedResult = writeFormattedResult;

  /*****************************************************
   * RHINO-SPECIFIC EXPORTS (are these used any more?) *
   *****************************************************/

  function inRhino() {
    return typeof global.defineClass === "function";
  }
  global.inRhino = inRhino;

  function GetContext() {
    return global.Packages.com.netscape.javascript.Context.getCurrentContext();
  }
  global.GetContext = GetContext;

  function OptLevel(i) {
    i = Number(i);
    var cx = GetContext();
    cx.setOptimizationLevel(i);
  }
  global.OptLevel = OptLevel;
})(this);


var STATUS = "STATUS: ";

var gDelayTestDriverEnd = false;

var gTestcases = new Array();
var gTc = gTestcases.length;
var summary = '';
var description = '';
var expected = '';
var actual = '';
var msg = '';

/*
 * constant strings
 */
var GLOBAL = this + '';

var DESCRIPTION;
var EXPECTED;

/*
 * Signals to results.py that the current test case should be considered to
 * have passed if it doesn't throw an exception.
 *
 * When the test suite is run in the browser, this function gets overridden by
 * the same-named function in browser.js.
 */
function testPassesUnlessItThrows() {
  print(PASSED);
}

/*
 * wrapper for test case constructor that doesn't require the SECTION
 * argument.
 */

function AddTestCase( description, expect, actual ) {
  new TestCase( SECTION, description, expect, actual );
}

function TestCase(n, d, e, a)
{
  this.name = n;
  this.description = d;
  this.expect = e;
  this.actual = a;
  this.passed = getTestCaseResult(e, a);
  this.reason = '';
  this.bugnumber = typeof(BUGNUMER) != 'undefined' ? BUGNUMBER : '';
  this.type = (typeof window == 'undefined' ? 'shell' : 'browser');
  ({}).constructor.defineProperty(
    gTestcases,
    gTc++,
    {
      value: this,
      enumerable: true,
      configurable: true,
      writable: true
    }
  );
}

gFailureExpected = false;

TestCase.prototype.dump = function () {
  // let reftest handle error reporting, otherwise
  // output a summary line.
  if (typeof document != "object" ||
      !document.location.href.match(/jsreftest.html/))
  {
    dump('\njstest: ' + this.path + ' ' +
         'bug: '         + this.bugnumber + ' ' +
         'result: '      + (this.passed ? 'PASSED':'FAILED') + ' ' +
         'type: '        + this.type + ' ' +
         'description: ' + toPrinted(this.description) + ' ' +
//       'expected: '    + toPrinted(this.expect) + ' ' +
//       'actual: '      + toPrinted(this.actual) + ' ' +
         'reason: '      + toPrinted(this.reason) + '\n');
  }
};

TestCase.prototype.testPassed = (function TestCase_testPassed() { return this.passed; });
TestCase.prototype.testFailed = (function TestCase_testFailed() { return !this.passed; });
TestCase.prototype.testDescription = (function TestCase_testDescription() { return this.description + ' ' + this.reason; });

function getTestCases()
{
  return gTestcases;
}

/*
 * The test driver searches for such a phrase in the test output.
 * If such phrase exists, it will set n as the expected exit code.
 */
function expectExitCode(n)
{
  print('--- NOTE: IN THIS TESTCASE, WE EXPECT EXIT CODE ' + n + ' ---');
}

/*
 * Statuses current section of a test
 */
function inSection(x)
{
  return "Section " + x + " of test - ";
}

/*
 * Report a failure in the 'accepted' manner
 */
function reportFailure (msg)
{
  var lines = msg.split ("\n");
  var l;
  var funcName = currentFunc();
  var prefix = (funcName) ? "[reported from " + funcName + "] ": "";

  for (var i=0; i<lines.length; i++)
    print (FAILED + prefix + lines[i]);
}

/*
 * Print a non-failure message.
 */
function printStatus (msg)
{
/* js1_6 had...
   msg = String(msg);
   msg = msg.toString();
*/
  msg = msg.toString();
  var lines = msg.split ("\n");
  var l;

  for (var i=0; i<lines.length; i++)
    print (STATUS + lines[i]);
}

/*
 * Print a bugnumber message.
 */
function printBugNumber (num)
{
  BUGNUMBER = num;
  print ('BUGNUMBER: ' + num);
}

function toPrinted(value)
{
  value = String(value);
  value = value.replace(/\\n/g, 'NL')
               .replace(/\n/g, 'NL')
               .replace(/\\r/g, 'CR')
               .replace(/[^\x20-\x7E]+/g, escapeString);
  return value;
}

function escapeString (str)
{
  var a, b, c, d;
  var len = str.length;
  var result = "";
  var digits = ["0", "1", "2", "3", "4", "5", "6", "7",
                "8", "9", "A", "B", "C", "D", "E", "F"];

  for (var i=0; i<len; i++)
  {
    var ch = str.charCodeAt(i);

    a = digits[ch & 0xf];
    ch >>= 4;
    b = digits[ch & 0xf];
    ch >>= 4;

    if (ch)
    {
      c = digits[ch & 0xf];
      ch >>= 4;
      d = digits[ch & 0xf];

      result += "\\u" + d + c + b + a;
    }
    else
    {
      result += "\\x" + b + a;
    }
  }

  return result;
}

/*
 * Compare expected result to actual result, if they differ (in value and/or
 * type) report a failure.  If description is provided, include it in the
 * failure report.
 */
function reportCompare (expected, actual, description) {
  var expected_t = typeof expected;
  var actual_t = typeof actual;
  var output = "";

  if (typeof description == "undefined")
    description = '';

  if (expected_t != actual_t) {
    output += "Type mismatch, expected type " + expected_t +
      ", actual type " + actual_t + " ";
  }

  if (expected != actual) {
    output += "Expected value '" + toPrinted(expected) +
      "', Actual value '" + toPrinted(actual) + "' ";
  }

  var testcase = new TestCase("unknown-test-name", description, expected, actual);
  testcase.reason = output;

  // if running under reftest, let it handle result reporting.
  if (typeof document != "object" ||
      !document.location.href.match(/jsreftest.html/)) {
    if (testcase.passed)
    {
      print(PASSED + description);
    }
    else
    {
      reportFailure (description + " : " + output);
    }
  }
  return testcase.passed;
}

/*
 * Attempt to match a regular expression describing the result to
 * the actual result, if they differ (in value and/or
 * type) report a failure.  If description is provided, include it in the
 * failure report.
 */
function reportMatch (expectedRegExp, actual, description) {
  var expected_t = "string";
  var actual_t = typeof actual;
  var output = "";

  if (typeof description == "undefined")
    description = '';

  if (expected_t != actual_t) {
    output += "Type mismatch, expected type " + expected_t +
      ", actual type " + actual_t + " ";
  }

  var matches = expectedRegExp.test(actual);
  if (!matches) {
    output += "Expected match to '" + toPrinted(expectedRegExp) +
      "', Actual value '" + toPrinted(actual) + "' ";
  }

  var testcase = new TestCase("unknown-test-name", description, true, matches);
  testcase.reason = output;

  // if running under reftest, let it handle result reporting.
  if (typeof document != "object" ||
      !document.location.href.match(/jsreftest.html/)) {
    if (testcase.passed)
    {
      print(PASSED + description);
    }
    else
    {
      reportFailure (description + " : " + output);
    }
  }
  return testcase.passed;
}

/*
 * An xorshift pseudo-random number generator see:
 * https://en.wikipedia.org/wiki/Xorshift#xorshift.2A
 * This generator will always produce a value, n, where
 * 0 <= n <= 255
 */
function *XorShiftGenerator(seed, size) {
    let x = seed;
    for (let i = 0; i < size; i++) {
        x ^= x >> 12;
        x ^= x << 25;
        x ^= x >> 27;
        yield x % 256;
    }
}

function compareSource(expect, actual, summary)
{
  // compare source
  var expectP = expect.
    replace(/([(){},.:\[\]])/mg, ' $1 ').
    replace(/(\w+)/mg, ' $1 ').
    replace(/<(\/)? (\w+) (\/)?>/mg, '<$1$2$3>').
    replace(/\s+/mg, ' ').
    replace(/new (\w+)\s*\(\s*\)/mg, 'new $1');

  var actualP = actual.
    replace(/([(){},.:\[\]])/mg, ' $1 ').
    replace(/(\w+)/mg, ' $1 ').
    replace(/<(\/)? (\w+) (\/)?>/mg, '<$1$2$3>').
    replace(/\s+/mg, ' ').
    replace(/new (\w+)\s*\(\s*\)/mg, 'new $1');

  print('expect:\n' + expectP);
  print('actual:\n' + actualP);

  reportCompare(expectP, actualP, summary);

  // actual must be compilable if expect is?
  try
  {
    var expectCompile = 'No Error';
    var actualCompile;

    eval(expect);
    try
    {
      eval(actual);
      actualCompile = 'No Error';
    }
    catch(ex1)
    {
      actualCompile = ex1 + '';
    }
    reportCompare(expectCompile, actualCompile,
                  summary + ': compile actual');
  }
  catch(ex)
  {
  }
}

function optionsInit() {

  // record initial values to support resetting
  // options to their initial values
  options.initvalues  = {};

  // record values in a stack to support pushing
  // and popping options
  options.stackvalues = [];

  var optionNames = options().split(',');

  for (var i = 0; i < optionNames.length; i++)
  {
    var optionName = optionNames[i];
    if (optionName)
    {
      options.initvalues[optionName] = '';
    }
  }
}

function optionsClear() {

  // turn off current settings
  // except jit.
  var optionNames = options().split(',');
  for (var i = 0; i < optionNames.length; i++)
  {
    var optionName = optionNames[i];
    if (optionName &&
        optionName != "methodjit" &&
        optionName != "methodjit_always" &&
        optionName != "ion")
    {
      options(optionName);
    }
  }
}

function optionsPush()
{
  var optionsframe = {};

  options.stackvalues.push(optionsframe);

  var optionNames = options().split(',');

  for (var i = 0; i < optionNames.length; i++)
  {
    var optionName = optionNames[i];
    if (optionName)
    {
      optionsframe[optionName] = '';
    }
  }

  optionsClear();
}

function optionsPop()
{
  var optionsframe = options.stackvalues.pop();

  optionsClear();

  for (optionName in optionsframe)
  {
    options(optionName);
  }

}

function optionsReset() {

  try
  {
    optionsClear();

    // turn on initial settings
    for (var optionName in options.initvalues)
    {
      if (!options.hasOwnProperty(optionName))
        continue;
      options(optionName);
    }
  }
  catch(ex)
  {
    print('optionsReset: caught ' + ex);
  }

}

if (typeof options == 'function')
{
  optionsInit();
  optionsClear();
}

function getTestCaseResult(expected, actual)
{
  if (typeof expected != typeof actual)
    return false;
  if (typeof expected != 'number')
    // Note that many tests depend on the use of '==' here, not '==='.
    return actual == expected;

  // Distinguish NaN from other values.  Using x != x comparisons here
  // works even if tests redefine isNaN.
  if (actual != actual)
    return expected != expected;
  if (expected != expected)
    return false;

  // Tolerate a certain degree of error.
  if (actual != expected)
    return Math.abs(actual - expected) <= 1E-10;

  // Here would be a good place to distinguish 0 and -0, if we wanted
  // to.  However, doing so would introduce a number of failures in
  // areas where they don't seem important.  For example, the WeekDay
  // function in ECMA-262 returns -0 for Sundays before the epoch, but
  // the Date functions in SpiderMonkey specified in terms of WeekDay
  // often don't.  This seems unimportant.
  return true;
}

function test() {
  for ( gTc=0; gTc < gTestcases.length; gTc++ ) {
    // temporary hack to work around some unknown issue in 1.7
    try
    {
      gTestcases[gTc].passed = writeTestCaseResult(
        gTestcases[gTc].expect,
        gTestcases[gTc].actual,
        gTestcases[gTc].description +" = "+ gTestcases[gTc].actual );
      gTestcases[gTc].reason += ( gTestcases[gTc].passed ) ? "" : "wrong value ";
    }
    catch(e)
    {
      print('test(): empty testcase for gTc = ' + gTc + ' ' + e);
    }
  }
  return ( gTestcases );
}

/*
 * Begin printing functions.  These functions use the shell's
 * print function.  When running tests in the browser, browser.js
 * overrides these functions to write to the page.
 */

function writeTestCaseResult( expect, actual, string ) {
  var passed = getTestCaseResult( expect, actual );
  // if running under reftest, let it handle result reporting.
  if (typeof document != "object" ||
      !document.location.href.match(/jsreftest.html/)) {
    writeFormattedResult( expect, actual, string, passed );
  }
  return passed;
}

function writeHeaderToLog( string ) {
  print( string );
}
/* end of print functions */


function jsTestDriverEnd()
{
  // gDelayTestDriverEnd is used to
  // delay collection of the test result and
  // signal to Spider so that tests can continue
  // to run after page load has fired. They are
  // responsible for setting gDelayTestDriverEnd = true
  // then when completed, setting gDelayTestDriverEnd = false
  // then calling jsTestDriverEnd()

  if (gDelayTestDriverEnd)
  {
    return;
  }

  try
  {
    optionsReset();
  }
  catch(ex)
  {
    dump('jsTestDriverEnd ' + ex);
  }

  for (var i = 0; i < gTestcases.length; i++)
  {
    gTestcases[i].dump();
  }

}
