package goja

import (
	"os"
	"sync"
	"testing"
	"unsafe"

	"github.com/dop251/goja/unistring"
)

const TESTLIB = `
function $ERROR(message) {
	throw new Error(message);
}

function Test262Error(message) {
  this.message = message || "";
}

Test262Error.prototype.toString = function () {
  return "Test262Error: " + this.message;
};

Test262Error.thrower = (message) => {
  throw new Test262Error(message);
};

function assert(mustBeTrue, message) {
    if (mustBeTrue === true) {
        return;
    }

    if (message === undefined) {
        message = 'Expected true but got ' + String(mustBeTrue);
    }
    $ERROR(message);
}

assert._isSameValue = function (a, b) {
    if (a === b) {
        // Handle +/-0 vs. -/+0
        return a !== 0 || 1 / a === 1 / b;
    }

    // Handle NaN vs. NaN
    return a !== a && b !== b;
};

assert.sameValue = function (actual, expected, message) {
    if (assert._isSameValue(actual, expected)) {
        return;
    }

    if (message === undefined) {
        message = '';
    } else {
        message += ' ';
    }

    message += 'Expected SameValue(«' + String(actual) + '», «' + String(expected) + '») to be true';

    $ERROR(message);
};

assert.throws = function (expectedErrorConstructor, func, message) {
  if (typeof func !== "function") {
    $ERROR('assert.throws requires two arguments: the error constructor ' +
      'and a function to run');
    return;
  }
  if (message === undefined) {
    message = '';
  } else {
    message += ' ';
  }

  try {
    func();
  } catch (thrown) {
    if (typeof thrown !== 'object' || thrown === null) {
      message += 'Thrown value was not an object!';
      $ERROR(message);
    } else if (thrown.constructor !== expectedErrorConstructor) {
      message += 'Expected a ' + expectedErrorConstructor.name + ' but got a ' + thrown.constructor.name;
      $ERROR(message);
    }
    return;
  }

  message += 'Expected a ' + expectedErrorConstructor.name + ' to be thrown but no exception was thrown at all';
  $ERROR(message);
};

function compareArray(a, b) {
  if (b.length !== a.length) {
    return false;
  }

  for (var i = 0; i < a.length; i++) {
    if (b[i] !== a[i]) {
      return false;
    }
  }
  return true;
}
`

const TESTLIBX = `
	function looksNative(fn) {
		return /native code/.test(Function.prototype.toString.call(fn));
	}

	function deepEqual(a, b) {
		if (typeof a === "object") {
			if (typeof b === "object") {
				if (a === b) {
					return true;
				}
				if (Reflect.getPrototypeOf(a) !== Reflect.getPrototypeOf(b)) {
					return false;
				}
				var keysA = Object.keys(a);
				var keysB = Object.keys(b);
				if (keysA.length !== keysB.length) {
					return false;
				}
				if (!compareArray(keysA.sort(), keysB.sort())) {
					return false;
				}
				for (var i = 0; i < keysA.length; i++) {
					var key = keysA[i];
					if (!deepEqual(a[key], b[key])) {
						return false;
					}
				}
				return true;
			} else {
				return false;
			}
		}
		return assert._isSameValue(a, b);
	}

	function assertStack(e, expected) {
		const lines = e.stack.split('\n');
		assert.sameValue(lines.length, expected.length + 2, "Stack lengths mismatch");
		let lnum = 1;
		for (const [file, func, line, col] of expected) {
			const expLine = func === "" ?
				"\tat " + file + ":" + line + ":" + col + "(" :
				"\tat " + func + " (" + file + ":" + line + ":" + col + "(";
			assert.sameValue(lines[lnum].substring(0, expLine.length), expLine, "line " + lnum);
			lnum++;
		}
	}
`

var (
	// The reason it's implemented this way rather than just as _testLib = MustCompile(...)
	// is because when you try to debug the compiler and set a breakpoint it gets triggered during the
	// initialisation which is annoying.
	_testLib, _testLibX       *Program
	testLibOnce, testLibXOnce sync.Once
)

func testLib() *Program {
	testLibOnce.Do(func() {
		_testLib = MustCompile("testlib.js", TESTLIB, false)
	})
	return _testLib
}

func testLibX() *Program {
	testLibXOnce.Do(func() {
		_testLibX = MustCompile("testlibx.js", TESTLIBX, false)
	})
	return _testLibX
}

func (r *Runtime) testPrg(p *Program, expectedResult Value, t *testing.T) {
	p.dumpCode(t.Logf)
	v, err := r.RunProgram(p)
	if err != nil {
		if ex, ok := err.(*Exception); ok {
			t.Fatalf("Exception: %v", ex.String())
		}
	}
	vm := r.vm
	t.Logf("stack size: %d", len(vm.stack))
	t.Logf("stashAllocs: %d", vm.stashAllocs)

	if v == nil && expectedResult != nil || !v.SameAs(expectedResult) {
		t.Fatalf("Result: %+v, expected: %+v", v, expectedResult)
	}

	if vm.sp != 0 {
		t.Fatalf("sp: %d", vm.sp)
	}

	if l := len(vm.iterStack); l > 0 {
		t.Fatalf("iter stack is not empty: %d", l)
	}
}

func (r *Runtime) testScriptWithTestLib(script string, expectedResult Value, t *testing.T) {
	_, err := r.RunProgram(testLib())
	if err != nil {
		t.Fatal(err)
	}

	r.testScript(script, expectedResult, t)
}

func (r *Runtime) testScriptWithTestLibX(script string, expectedResult Value, t *testing.T) {
	_, err := r.RunProgram(testLib())
	if err != nil {
		t.Fatal(err)
	}

	_, err = r.RunProgram(testLibX())
	if err != nil {
		t.Fatal(err)
	}

	r.testScript(script, expectedResult, t)
}

func (r *Runtime) testScript(script string, expectedResult Value, t *testing.T) {
	r.testPrg(MustCompile("test.js", script, false), expectedResult, t)
}

func testScript(script string, expectedResult Value, t *testing.T) {
	New().testScript(script, expectedResult, t)
}

func testScriptWithTestLib(script string, expectedResult Value, t *testing.T) {
	New().testScriptWithTestLib(script, expectedResult, t)
}

func testScriptWithTestLibX(script string, expectedResult Value, t *testing.T) {
	New().testScriptWithTestLibX(script, expectedResult, t)
}

func (r *Runtime) testAsyncFunc(src string, expectedResult Value, t *testing.T) {
	v, err := r.RunScript("test.js", "(async function test() {"+src+"\n})()")
	if err != nil {
		t.Fatal(err)
	}
	promise := v.Export().(*Promise)
	switch s := promise.State(); s {
	case PromiseStateFulfilled:
		if res := promise.Result(); res == nil && expectedResult != nil || !res.SameAs(expectedResult) {
			t.Fatalf("Result: %+v, expected: %+v", res, expectedResult)
		}
	case PromiseStateRejected:
		res := promise.Result()
		if resObj, ok := res.(*Object); ok {
			if stack := resObj.Get("stack"); stack != nil {
				t.Fatal(stack.String())
			}
		}
		t.Fatal(res.String())
	default:
		t.Fatalf("Unexpected promise state: %v", s)
	}
}

func (r *Runtime) testAsyncFuncWithTestLib(src string, expectedResult Value, t *testing.T) {
	_, err := r.RunProgram(testLib())
	if err != nil {
		t.Fatal(err)
	}

	r.testAsyncFunc(src, expectedResult, t)
}

func (r *Runtime) testAsyncFuncWithTestLibX(src string, expectedResult Value, t *testing.T) {
	_, err := r.RunProgram(testLib())
	if err != nil {
		t.Fatal(err)
	}

	_, err = r.RunProgram(testLibX())
	if err != nil {
		t.Fatal(err)
	}

	r.testAsyncFunc(src, expectedResult, t)
}

func testAsyncFunc(src string, expectedResult Value, t *testing.T) {
	New().testAsyncFunc(src, expectedResult, t)
}

func testAsyncFuncWithTestLib(src string, expectedResult Value, t *testing.T) {
	New().testAsyncFuncWithTestLib(src, expectedResult, t)
}

func testAsyncFuncWithTestLibX(src string, expectedResult Value, t *testing.T) {
	New().testAsyncFuncWithTestLibX(src, expectedResult, t)
}

func TestEmptyProgram(t *testing.T) {
	const SCRIPT = `
	`

	testScript(SCRIPT, _undefined, t)
}

func TestResultEmptyBlock(t *testing.T) {
	const SCRIPT = `
	undefined;
	{}
	`
	testScript(SCRIPT, _undefined, t)
}

func TestResultVarDecl(t *testing.T) {
	const SCRIPT = `
	7; var x = 1;
	`
	testScript(SCRIPT, valueInt(7), t)
}

func TestResultLexDecl(t *testing.T) {
	const SCRIPT = `
	7; {let x = 1};
	`
	testScript(SCRIPT, valueInt(7), t)
}

func TestResultLexDeclBreak(t *testing.T) {
	const SCRIPT = `
	L:{ 7; {let x = 1; break L;}};
	`
	testScript(SCRIPT, valueInt(7), t)
}

func TestResultLexDeclNested(t *testing.T) {
	const SCRIPT = `
	7; {let x = (function() { return eval("8; {let y = 9}")})()};
	`
	testScript(SCRIPT, valueInt(7), t)
}

func TestErrorProto(t *testing.T) {
	const SCRIPT = `
	var e = new TypeError();
	e.name;
	`

	testScript(SCRIPT, asciiString("TypeError"), t)
}

func TestThis1(t *testing.T) {
	const SCRIPT = `
	function independent() {
		return this.prop;
	}
	var o = {};
	o.b = {g: independent, prop: 42};

	o.b.g();
	`
	testScript(SCRIPT, intToValue(42), t)
}

func TestThis2(t *testing.T) {
	const SCRIPT = `
var o = {
  prop: 37,
  f: function() {
    return this.prop;
  }
};

o.f();
`

	testScript(SCRIPT, intToValue(37), t)
}

func TestThisStrict(t *testing.T) {
	const SCRIPT = `
	"use strict";

	Object.defineProperty(Object.prototype, "x", { get: function () { return this; } });

	(5).x === 5;
	`

	testScript(SCRIPT, valueTrue, t)
}

func TestThisNoStrict(t *testing.T) {
	const SCRIPT = `
	Object.defineProperty(Object.prototype, "x", { get: function () { return this; } });

	(5).x == 5;
	`

	testScript(SCRIPT, valueTrue, t)
}

func TestNestedFuncVarResolution(t *testing.T) {
	const SCRIPT = `
	(function outer() {
		var v = 42;
		function inner() {
			return v;
		}
		return inner();
	})();
`
	testScript(SCRIPT, valueInt(42), t)
}

func TestNestedFuncVarResolution1(t *testing.T) {
	const SCRIPT = `
	function outer(argOuter) {
		var called = 0;
	  var inner = function(argInner) {
		if (arguments.length !== 1) {
			throw new Error();
		}
		called++;
		if (argOuter !== 1) {
			throw new Error("argOuter");
		}
		if (argInner !== 2) {
			throw new Error("argInner");
		}
	  };
		inner(2);
	}
	outer(1);
	`
	testScript(SCRIPT, _undefined, t)
}

func TestCallFewerArgs(t *testing.T) {
	const SCRIPT = `
function A(a, b, c) {
	return String(a) + " " + String(b) + " " + String(c);
}

A(1, 2);
`
	testScript(SCRIPT, asciiString("1 2 undefined"), t)
}

func TestCallFewerArgsClosureNoArgs(t *testing.T) {
	const SCRIPT = `
	var x;
	function A(a, b, c) {
		var y = a;
		x = function() { return " " + y };
		return String(a) + " " + String(b) + " " + String(c);
	}

	A(1, 2) + x();
`
	testScript(SCRIPT, asciiString("1 2 undefined 1"), t)
}

func TestCallFewerArgsClosureArgs(t *testing.T) {
	const SCRIPT = `
	var x;
	function A(a, b, c) {
		var y = b;
		x = function() { return " " + a + " " + y };
		return String(a) + " " + String(b) + " " + String(c);
	}

	A(1, 2) + x();
`
	testScript(SCRIPT, asciiString("1 2 undefined 1 2"), t)
}

func TestCallMoreArgs(t *testing.T) {
	const SCRIPT = `
function A(a, b) {
	var c = 4;
	return a - b + c;
}

A(1, 2, 3);
`
	testScript(SCRIPT, intToValue(3), t)
}

func TestCallMoreArgsDynamic(t *testing.T) {
	const SCRIPT = `
function A(a, b) {
	var c = 4;
	if (false) {
		eval("");
	}
	return a - b + c;
}

A(1, 2, 3);
`
	testScript(SCRIPT, intToValue(3), t)
}

func TestCallLessArgsDynamic(t *testing.T) {
	const SCRIPT = `
function A(a, b, c) {
	// Make it stashful
	function B() {
		return a;
	}
	return String(a) + " " + String(b) + " " + String(c);
}

A(1, 2);
`
	testScript(SCRIPT, asciiString("1 2 undefined"), t)
}

func TestCallLessArgsDynamicLocalVar(t *testing.T) {
	const SCRIPT = `
	function f(param) {
		var a = 42;
		if (false) {
			eval("");
		}
		return a;
	}
	f();
`

	testScript(SCRIPT, intToValue(42), t)
}

/*
func TestFib(t *testing.T) {
	testScript(TEST_FIB, valueInt(9227465), t)
}
*/

func TestNativeCall(t *testing.T) {
	const SCRIPT = `
	var o = Object(1);
	Object.defineProperty(o, "test", {value: 42});
	o.test;
	`
	testScript(SCRIPT, intToValue(42), t)
}

func TestJSCall(t *testing.T) {
	const SCRIPT = `
	function getter() {
		return this.x;
	}
	var o = Object(1);
	o.x = 42;
	Object.defineProperty(o, "test", {get: getter});
	o.test;
	`
	testScript(SCRIPT, intToValue(42), t)

}

func TestLoop1(t *testing.T) {
	const SCRIPT = `
	function A() {
    		var x = 1;
    		for (var i = 0; i < 1; i++) {
        		var x = 2;
    		}
    		return x;
	}

	A();
	`
	testScript(SCRIPT, intToValue(2), t)
}

func TestLoopBreak(t *testing.T) {
	const SCRIPT = `
	function A() {
    		var x = 1;
    		for (var i = 0; i < 1; i++) {
        		break;
        		var x = 2;
    		}
    		return x;
	}

	A();
	`
	testScript(SCRIPT, intToValue(1), t)
}

func TestForLoopOptionalExpr(t *testing.T) {
	const SCRIPT = `
	function A() {
    		var x = 1;
    		for (;;) {
        		break;
        		var x = 2;
    		}
    		return x;
	}

	A();
	`
	testScript(SCRIPT, intToValue(1), t)
}

func TestBlockBreak(t *testing.T) {
	const SCRIPT = `
	var rv = 0;
	B1: {
		rv = 1;
		B2: {
			rv = 2;
			break B1;
		}
		rv = 3;
	}
	rv;
	`
	testScript(SCRIPT, intToValue(2), t)

}

func TestTry(t *testing.T) {
	const SCRIPT = `
	function A() {
		var x = 1;
		try {
			x = 2;
		} catch(e) {
			x = 3;
		} finally {
			x = 4;
		}
		return x;
	}

	A();
	`
	testScript(SCRIPT, intToValue(4), t)
}

func TestTryOptionalCatchBinding(t *testing.T) {
	const SCRIPT = `
	try {
		throw null;
	} catch {
	}
	`
	testScript(SCRIPT, _undefined, t)
}

func TestTryCatch(t *testing.T) {
	const SCRIPT = `
	function A() {
		var x;
		try {
			throw 4;
		} catch(e) {
			x = e;
		}
		return x;
	}

	A();
	`
	testScript(SCRIPT, intToValue(4), t)
}

func TestTryCatchDirectEval(t *testing.T) {
	const SCRIPT = `
	function A() {
		var x;
		try {
			throw 4;
		} catch(e) {
			eval("x = e");
		}
		return x;
	}

	A();
	`
	testScript(SCRIPT, intToValue(4), t)
}

func TestTryExceptionInCatch(t *testing.T) {
	const SCRIPT = `
	function A() {
		var x;
		try {
			throw 4;
		} catch(e) {
			throw 5;
		}
		return x;
	}

	var rv;
	try {
		A();
	} catch (e) {
		rv = e;
	}
	rv;
	`
	testScript(SCRIPT, intToValue(5), t)
}

func TestTryContinueInCatch(t *testing.T) {
	const SCRIPT = `
	var c3 = 0, fin3 = 0;
	while (c3 < 2) {
		try {
			throw "ex1";
		} catch(er1) {
			c3 += 1;
			continue;
		} finally {
			fin3 = 1;
		}
		fin3 = 0;
	}

	fin3;
	`
	testScript(SCRIPT, intToValue(1), t)
}

func TestContinueInWith(t *testing.T) {
	const SCRIPT = `
	var x;
	var o = {x: 0};
	for (var i = 0; i < 2; i++) {
		with(o) {
			x = i;
			if (i === 0) {
				continue;
			}
		}
		break;
	}
	x;
	`
	testScript(SCRIPT, _undefined, t)
}

func TestTryContinueInFinally(t *testing.T) {
	const SCRIPT = `
	var c3 = 0, fin3 = 0;
	while (c3 < 2) {
  		try {
    			throw "ex1";
  		} catch(er1) {
    			c3 += 1;
  		} finally {
    			fin3 = 1;
    			continue;
  		}
  		fin3 = 0;
	}

	fin3;
	`
	testScript(SCRIPT, intToValue(1), t)
}

func TestTryBreakFinallyContinue(t *testing.T) {
	const SCRIPT = `
	for (var i = 0; i < 3; i++) {
	  try {
		break;
	  } finally {
		continue;
	  }
	}
	`
	testScript(SCRIPT, _undefined, t)
}

func TestTryBreakFinallyContinueWithResult(t *testing.T) {
	const SCRIPT = `
	for (var i = 0; i < 3; i++) {
	  try {
		true;
		break;
	  } finally {
		continue;
	  }
	}
	`
	testScript(SCRIPT, _undefined, t)
}

func TestTryBreakFinallyContinueWithResult1(t *testing.T) {
	const SCRIPT = `
	for (var i = 0; i < 3; i++) {
	  try {
		true;
		break;
	  } finally {
		var x = 1;
		continue;
	  }
	}
	`
	testScript(SCRIPT, _undefined, t)
}

func TestTryBreakFinallyContinueWithResultNested(t *testing.T) {
	const SCRIPT = `
LOOP:
	for (var i = 0; i < 3; i++) {
	  try {
		if (true) {
			false; break;
		}
	  } finally {
		if (true) {
			true; continue;
		}
	  }
	}
	`
	testScript(SCRIPT, valueTrue, t)
}

func TestTryBreakOuterFinallyContinue(t *testing.T) {
	const SCRIPT = `
	let iCount = 0, jCount = 0;
	OUTER: for (let i = 0; i < 1; i++) {
		iCount++;
		for (let j = 0; j < 2; j++) {
			jCount++;
			try {
				break OUTER;
			} finally {
				continue;
			}
		}
	}
	""+iCount+jCount;
	`
	testScript(SCRIPT, asciiString("12"), t)
}

func TestTryIllegalContinueWithFinallyOverride(t *testing.T) {
	const SCRIPT = `
	L: {
		while (Math.random() > 0.5) {
			try {
				continue L;
			} finally {
				break;
			}
		}
	}
	`
	_, err := Compile("", SCRIPT, false)
	if err == nil {
		t.Fatal("expected error")
	}
}

func TestTryIllegalContinueWithFinallyOverrideNoLabel(t *testing.T) {
	const SCRIPT = `
	L: {
		try {
			continue;
		} finally {
			break L;
		}
	}
	`
	_, err := Compile("", SCRIPT, false)
	if err == nil {
		t.Fatal("expected error")
	}
}

func TestTryIllegalContinueWithFinallyOverrideDummy(t *testing.T) {
	const SCRIPT = `
	L: {
		while (false) {
			try {
				continue L;
			} finally {
				break;
			}
		}
	}
	`
	_, err := Compile("", SCRIPT, false)
	if err == nil {
		t.Fatal("expected error")
	}
}

func TestTryNoResult(t *testing.T) {
	const SCRIPT = `
	true;
    L:
    try {
        break L;
    } finally {
    }
	`
	testScript(SCRIPT, _undefined, t)
}

func TestCatchLexicalEnv(t *testing.T) {
	const SCRIPT = `
	function F() {
		try {
			throw 1;
		} catch (e) {
			var x = e;
		}
		return x;
	}

	F();
	`
	testScript(SCRIPT, intToValue(1), t)
}

func TestThrowType(t *testing.T) {
	const SCRIPT = `
	function Exception(message) {
		this.message = message;
	}


	function A() {
		try {
			throw new Exception("boo!");
		} catch(e) {
			return e;
		}
	}
	var thrown = A();
	thrown !== null && typeof thrown === "object" && thrown.constructor === Exception;
	`
	testScript(SCRIPT, valueTrue, t)
}

func TestThrowConstructorName(t *testing.T) {
	const SCRIPT = `
	function Exception(message) {
		this.message = message;
	}


	function A() {
		try {
			throw new Exception("boo!");
		} catch(e) {
			return e;
		}
	}
	A().constructor.name;
	`

	testScript(SCRIPT, asciiString("Exception"), t)
}

func TestThrowNativeConstructorName(t *testing.T) {
	const SCRIPT = `


	function A() {
		try {
			throw new TypeError();
		} catch(e) {
			return e;
		}
	}
	A().constructor.name;
	`

	testScript(SCRIPT, asciiString("TypeError"), t)
}

func TestEmptyTryNoCatch(t *testing.T) {
	const SCRIPT = `
	var called = false;
	try {
	} finally {
		called = true;
	}
	called;
	`

	testScript(SCRIPT, valueTrue, t)
}

func TestTryReturnFromCatch(t *testing.T) {
	const SCRIPT = `
	function f(o) {
		var x = 42;

		function innerf(o) {
			try {
				throw o;
			} catch (e) {
				return x;
			}
		}

		return innerf(o);
	}
	f({});
	`

	testScript(SCRIPT, valueInt(42), t)
}

func TestTryCompletionResult(t *testing.T) {
	const SCRIPT = `
	99; do { -99; try { 39 } catch (e) { -1 } finally { break; -2 }; } while (false);
	`

	testScript(SCRIPT, _undefined, t)
}

func TestIfElse(t *testing.T) {
	const SCRIPT = `
	var rv;
	if (rv === undefined) {
		rv = "passed";
	} else {
		rv = "failed";
	}
	rv;
	`

	testScript(SCRIPT, asciiString("passed"), t)
}

func TestIfElseRetVal(t *testing.T) {
	const SCRIPT = `
	var x;
	if (x === undefined) {
		"passed";
	} else {
		"failed";
	}
	`

	testScript(SCRIPT, asciiString("passed"), t)
}

func TestWhileReturnValue(t *testing.T) {
	const SCRIPT = `
	var x = 0;
	while(true) {
		x = 1;
		break;
	}
	`
	testScript(SCRIPT, intToValue(1), t)
}

func TestIfElseLabel(t *testing.T) {
	const SCRIPT = `
	var x = 0;
	abc: if (true) {
		x = 1;
		break abc;
	}
	`
	testScript(SCRIPT, intToValue(1), t)
}

func TestIfMultipleLabels(t *testing.T) {
	const SCRIPT = `
	var x = 0;
	xyz:abc: if (true) {
		break xyz;
	}
	`
	testScript(SCRIPT, _undefined, t)
}

func TestBreakOutOfTry(t *testing.T) {
	const SCRIPT = `
	function A() {
		var x = 1;
		B: {
			try {
				x = 2;
			} catch(e) {
				x = 3;
			} finally {
				break B;
				x = 4;
			}
		}
		return x;
	}

	A();
	`
	testScript(SCRIPT, intToValue(2), t)
}

func TestReturnOutOfTryNested(t *testing.T) {
	const SCRIPT = `
	function A() {
		function nested() {
			try {
				return 1;
			} catch(e) {
				return 2;
			}
		}
		return nested();
	}

	A();
	`
	testScript(SCRIPT, intToValue(1), t)
}

func TestReturnOutOfTryWithFinally(t *testing.T) {
	const SCRIPT = `
	function test() {
		try {
			return 'Hello, world!';
		} finally {
			const dummy = 'unexpected';
		}
	}
	test();
	`
	testScript(SCRIPT, asciiString("Hello, world!"), t)
}

func TestContinueLoop(t *testing.T) {
	const SCRIPT = `
	function A() {
		var r = 0;
		for (var i = 0; i < 5; i++) {
			if (i > 1) {
				continue;
			}
			r++;
		}
		return r;
	}

	A();
	`
	testScript(SCRIPT, intToValue(2), t)
}

func TestContinueOutOfTry(t *testing.T) {
	const SCRIPT = `
	function A() {
		var r = 0;
		for (var i = 0; i < 5; i++) {
			try {
				if (i > 1) {
					continue;
				}
			} catch(e) {
				return 99;
			}
			r++;
		}
		return r;
	}

	A();
	`
	testScript(SCRIPT, intToValue(2), t)
}

func TestThisInCatch(t *testing.T) {
	const SCRIPT = `
	function O() {
		try {
			f();
		} catch (e) {
			this.value = e.toString();
		}
	}

	function f() {
		throw "ex";
	}

	var o = new O();
	o.value;
	`
	testScript(SCRIPT, asciiString("ex"), t)
}

func TestNestedTry(t *testing.T) {
	const SCRIPT = `
	var ex;
	try {
  		throw "ex1";
	} catch (er1) {
  		try {
    			throw "ex2";
  		} catch (er1) {
			ex = er1;
		}
	}
	ex;
	`
	testScript(SCRIPT, asciiString("ex2"), t)
}

func TestNestedTryInStashlessFunc(t *testing.T) {
	const SCRIPT = `
	function f() {
		var ex1, ex2;
		try {
			throw "ex1";
		} catch (er1) {
			try {
				throw "ex2";
			} catch (er1) {
				ex2 = er1;
			}
			ex1 = er1;
		}
		return ex1 == "ex1" && ex2 == "ex2";
	}
	f();
	`
	testScript(SCRIPT, valueTrue, t)
}

func TestEvalLexicalDecl(t *testing.T) {
	const SCRIPT = `
	eval("let x = true; x;");
	`
	testScript(SCRIPT, valueTrue, t)
}

func TestEvalInCatchInStashlessFunc(t *testing.T) {
	const SCRIPT = `
	function f() {
		var ex;
		try {
			throw "ex1";
		} catch (er1) {
			eval("ex = er1");
		}
		return ex;
	}
	f();
	`
	testScript(SCRIPT, asciiString("ex1"), t)
}

func TestCatchClosureInStashlessFunc(t *testing.T) {
	const SCRIPT = `
	function f() {
		var ex;
		try {
			throw "ex1";
		} catch (er1) {
			return function() {
				return er1;
			}
		}
	}
	f()();
	`
	testScript(SCRIPT, asciiString("ex1"), t)
}

func TestCatchVarNotUsedInStashlessFunc(t *testing.T) {
	const SCRIPT = `
	function f() {
		var ex;
		try {
			throw "ex1";
		} catch (er1) {
			ex = "ok";
		}
		return ex;
	}
	f();
	`
	testScript(SCRIPT, asciiString("ok"), t)
}

func TestNew(t *testing.T) {
	const SCRIPT = `
	function O() {
		this.x = 42;
	}

	new O().x;
	`

	testScript(SCRIPT, intToValue(42), t)
}

func TestStringConstructor(t *testing.T) {
	const SCRIPT = `
	function F() {
		return String(33) + " " + String("cows");
	}

	F();
	`
	testScript(SCRIPT, asciiString("33 cows"), t)
}

func TestError(t *testing.T) {
	const SCRIPT = `
	function F() {
		return new Error("test");
	}

	var e = F();
	e.message == "test" && e.name == "Error";
	`
	testScript(SCRIPT, valueTrue, t)
}

func TestTypeError(t *testing.T) {
	const SCRIPT = `
	function F() {
		return new TypeError("test");
	}

	var e = F();
	e.message == "test" && e.name == "TypeError";
	`

	testScript(SCRIPT, valueTrue, t)
}

func TestToString(t *testing.T) {
	const SCRIPT = `
	var o = {x: 42};
	o.toString = function() {
		return String(this.x);
	}

	var o1 = {};
	o.toString() + " ### " + o1.toString();
	`
	testScript(SCRIPT, asciiString("42 ### [object Object]"), t)
}

func TestEvalOrder(t *testing.T) {
	const SCRIPT = `
	var o = {f: function() {return 42}, x: 0};
	var trace = "";

	function F1() {
	    trace += "First!";
	    return o;
	}

	function F2() {
	    trace += "Second!";
	    return "f";
	}

	function F3() {
	    trace += "Third!";
	}

	var rv = F1()[F2()](F3());
	rv += trace;
	rv;
	`

	testScript(SCRIPT, asciiString("42First!Second!Third!"), t)
}

func TestPostfixIncBracket(t *testing.T) {
	const SCRIPT = `
	var o = {x: 42};
	var trace = "";

	function F1() {
	    trace += "First!";
	    return o;
	}

	function F2() {
	    trace += "Second!";
	    return "x";
	}


	var rv = F1()[F2()]++;
	rv + trace + o.x;
	`
	testScript(SCRIPT, asciiString("42First!Second!43"), t)
}

func TestPostfixIncDot(t *testing.T) {
	const SCRIPT = `
	var o = {x: 42};
	var trace = "";

	function F1() {
	    trace += "First!";
	    return o;
	}

	var rv = F1().x++;
	rv + trace + o.x;
	`
	testScript(SCRIPT, asciiString("42First!43"), t)
}

func TestPrefixIncBracket(t *testing.T) {
	const SCRIPT = `
	var o = {x: 42};
	var trace = "";

	function F1() {
	    trace += "First!";
	    return o;
	}

	function F2() {
	    trace += "Second!";
	    return "x";
	}


	var rv = ++F1()[F2()];
	rv + trace + o.x;
	`
	testScript(SCRIPT, asciiString("43First!Second!43"), t)
}

func TestPrefixIncDot(t *testing.T) {
	const SCRIPT = `
	var o = {x: 42};
	var trace = "";

	function F1() {
	    trace += "First!";
	    return o;
	}

	var rv = ++F1().x;
	rv + trace + o.x;
	`
	testScript(SCRIPT, asciiString("43First!43"), t)
}

func TestPostDecObj(t *testing.T) {
	const SCRIPT = `
	var object = {valueOf: function() {return 1}};
	var y = object--;
	var ok = false;
	if (y === 1) {
		ok = true;
	}
	ok;
	`

	testScript(SCRIPT, valueTrue, t)
}

func TestPropAcc1(t *testing.T) {
	const SCRIPT = `
	1..toString()
	`

	testScript(SCRIPT, asciiString("1"), t)
}

func TestEvalDirect(t *testing.T) {
	const SCRIPT = `
	var rv = false;
    function foo(){ rv = true; }

    var o = { };
    function f() {
        try {
	        eval("o.bar( foo() );");
		} catch (e) {
		}
    }
    f();
	rv;
	`
	testScript(SCRIPT, valueTrue, t)
}

func TestEvalRet(t *testing.T) {
	const SCRIPT = `
	eval("for (var i = 0; i < 3; i++) {i}")
	`

	testScript(SCRIPT, valueInt(2), t)
}

func TestEvalFunctionDecl(t *testing.T) {
	const SCRIPT = `
	eval("function F() {}")
	`

	testScript(SCRIPT, _undefined, t)
}

func TestEvalFunctionExpr(t *testing.T) {
	const SCRIPT = `
	eval("(function F() {return 42;})")()
	`

	testScript(SCRIPT, intToValue(42), t)
}

func TestEvalDirectScope(t *testing.T) {
	const SCRIPT = `
	var __10_4_2_1_3 = "str";
	function testcase() {
		var __10_4_2_1_3 = "str1";
		try {
			throw "error";
		} catch (e) {
			var __10_4_2_1_3 = "str2";
			return eval("__10_4_2_1_3");
		}
	}
	testcase();
	`

	testScript(SCRIPT, asciiString("str2"), t)
}

func TestEvalDirectScope1(t *testing.T) {
	const SCRIPT = `
	'use strict';
	var __10_4_2_1_5 = "str";
	function testcase() {
				var __10_4_2_1_5 = "str1";
				var r = eval("\
							  var __10_4_2_1_5 = \'str2\'; \
							  eval(\"\'str2\' === __10_4_2_1_5\")\
							");
				return r;
		}
	testcase();
	`

	testScript(SCRIPT, valueTrue, t)
}

func TestEvalDirectCreateBinding(t *testing.T) {
	const SCRIPT = `
	function f() {
		eval("var x = true");
		return x;
	}
	var res = f();
	var thrown = false;
	try {
		x;
	} catch(e) {
		if (e instanceof ReferenceError) {
			thrown = true;
		} else {
			throw e;
		}
	}
	res && thrown;
	`

	testScript(SCRIPT, valueTrue, t)
}

func TestEvalDirectCreateBinding1(t *testing.T) {
	const SCRIPT = `
	function f() {
		eval("let x = 1; var y = 2; function f1() {return x};");
		assert.throws(ReferenceError, function() { x });
		return ""+y+f1();
	}
	f();
	`

	testScriptWithTestLib(SCRIPT, asciiString("21"), t)
}

func TestEvalDirectCreateBinding3(t *testing.T) {
	const SCRIPT = `
	function f() {
		let x;
		try {
			eval("var y=1, x=2");
		} catch(e) {}
		return y;
	}
	assert.throws(ReferenceError, f);
	`

	testScriptWithTestLib(SCRIPT, _undefined, t)
}

func TestEvalGlobalStrict(t *testing.T) {
	const SCRIPT = `
	'use strict';
	var evalStr =
	'for (var x in this) {\n'+
	'  if ( x === \'Math\' ) {\n'+
	'  }\n'+
	'}\n';

	eval(evalStr);
	`

	testScript(SCRIPT, _undefined, t)
}

func TestEvalEmptyStrict(t *testing.T) {
	const SCRIPT = `
	'use strict';
	eval("");
	`

	testScript(SCRIPT, _undefined, t)
}

func TestEvalFuncDecl(t *testing.T) {
	const SCRIPT = `
	'use strict';
	var funcA = eval("function __funcA(__arg){return __arg;}; __funcA");
	typeof funcA;
	`

	testScript(SCRIPT, asciiString("function"), t)
}

func TestGetAfterSet(t *testing.T) {
	const SCRIPT = `
	function f() {
		var x = 1;
		return x;
	}
	`

	testScript(SCRIPT, _undefined, t)
}

func TestForLoopRet(t *testing.T) {
	const SCRIPT = `
	for (var i = 0; i < 20; i++) { if (i > 2) {break;} else { i }}
	`

	testScript(SCRIPT, _undefined, t)
}

func TestForLoopRet1(t *testing.T) {
	const SCRIPT = `
	for (var i = 0; i < 20; i++) { if (i > 2) {42;; {L:{break;}}} else { i }}
	`

	testScript(SCRIPT, intToValue(42), t)
}

func TestForInLoopRet(t *testing.T) {
	const SCRIPT = `
	var o = [1, 2, 3, 4];
	for (var i in o) { if (i > 2) {break;} else { i }}
	`

	testScript(SCRIPT, _undefined, t)
}

func TestForInLoopRet1(t *testing.T) {
	const SCRIPT = `
	var o = {};
	o.x = 1;
	o.y = 2;
	for (var i in o) {
		true;
	}

	`

	testScript(SCRIPT, valueTrue, t)
}

func TestDoWhileLoopRet(t *testing.T) {
	const SCRIPT = `
	var i = 0;
	do {
		if (i > 2) {
			break;
		} else {
			i;
		}
	} while (i++ < 20);
	`

	testScript(SCRIPT, _undefined, t)
}

func TestDoWhileContinueRet(t *testing.T) {
	const SCRIPT = `
	var i = 0;
	do {
		if (i > 2) {
			true;
			continue;
		} else {
			i;
		}
	} while (i++ < 20);
	`

	testScript(SCRIPT, valueTrue, t)
}

func TestWhileLoopRet(t *testing.T) {
	const SCRIPT = `
	var i; while (i < 20) { if (i > 2) {break;} else { i++ }}
	`

	testScript(SCRIPT, _undefined, t)
}

func TestLoopRet1(t *testing.T) {
	const SCRIPT = `
	for (var i = 0; i < 20; i++) { }
	`

	testScript(SCRIPT, _undefined, t)
}

func TestInstanceof(t *testing.T) {
	const SCRIPT = `
	var rv;
	try {
		true();
	} catch (e) {
		rv = e instanceof TypeError;
	}
	rv;
	`

	testScript(SCRIPT, valueTrue, t)
}

func TestStrictAssign(t *testing.T) {
	const SCRIPT = `
	'use strict';
	var rv;
	var called = false;
	function F() {
		called = true;
		return 1;
	}
	try {
		x = F();
	} catch (e) {
		rv = e instanceof ReferenceError;
	}
	rv + " " + called;
	`

	testScript(SCRIPT, asciiString("true true"), t)
}

func TestStrictScope(t *testing.T) {
	const SCRIPT = `
	var rv;
	var called = false;
	function F() {
		'use strict';
		x = 1;
	}
	try {
		F();
	} catch (e) {
		rv = e instanceof ReferenceError;
	}
	x = 1;
	rv + " " + x;
	`

	testScript(SCRIPT, asciiString("true 1"), t)
}

func TestStringObj(t *testing.T) {
	const SCRIPT = `
	var s = new String("test");
	s[0] + s[2] + s[1];
	`

	testScript(SCRIPT, asciiString("tse"), t)
}

func TestStringPrimitive(t *testing.T) {
	const SCRIPT = `
	var s = "test";
	s[0] + s[2] + s[1];
	`

	testScript(SCRIPT, asciiString("tse"), t)
}

func TestCallGlobalObject(t *testing.T) {
	const SCRIPT = `
	var rv;
	try {
		this();
	} catch (e) {
		rv = e instanceof TypeError
	}
	rv;
	`

	testScript(SCRIPT, valueTrue, t)
}

func TestFuncLength(t *testing.T) {
	const SCRIPT = `
	function F(x, y) {

	}
	F.length
	`

	testScript(SCRIPT, intToValue(2), t)
}

func TestNativeFuncLength(t *testing.T) {
	const SCRIPT = `
	eval.length + Object.defineProperty.length + String.length
	`

	testScript(SCRIPT, intToValue(5), t)
}

func TestArguments(t *testing.T) {
	const SCRIPT = `
	function F() {
		return arguments.length + " " + arguments[1];
	}

	F(1,2,3)
	`

	testScript(SCRIPT, asciiString("3 2"), t)
}

func TestArgumentsPut(t *testing.T) {
	const SCRIPT = `
	function F(x, y) {
		arguments[0] -= arguments[1];
		return x;
	}

	F(5, 2)
	`

	testScript(SCRIPT, intToValue(3), t)
}

func TestArgumentsPutStrict(t *testing.T) {
	const SCRIPT = `
	function F(x, y) {
		'use strict';
		arguments[0] -= arguments[1];
		return x;
	}

	F(5, 2)
	`

	testScript(SCRIPT, intToValue(5), t)
}

func TestArgumentsExtra(t *testing.T) {
	const SCRIPT = `
	function F(x, y) {
		return arguments[2];
	}

	F(1, 2, 42)
	`

	testScript(SCRIPT, intToValue(42), t)
}

func TestArgumentsExist(t *testing.T) {
	const SCRIPT = `
	function F(x, arguments) {
		return arguments;
	}

	F(1, 42)
	`

	testScript(SCRIPT, intToValue(42), t)
}

func TestArgumentsDelete(t *testing.T) {
	const SCRIPT = `
	function f(x) {
		delete arguments[0];
		arguments[0] = 42;
		return x;
	}
	f(1)
	`

	testScript(SCRIPT, intToValue(1), t)
}

func TestArgumentsInEval(t *testing.T) {
	const SCRIPT = `
	function f() {
		return eval("arguments");
	}
	f(1)[0];
	`

	testScript(SCRIPT, intToValue(1), t)
}

func TestArgumentsRedeclareInEval(t *testing.T) {
	const SCRIPT = `
	assert.sameValue("arguments" in this, false, "No global 'arguments' binding");

	function f(p = eval("var arguments = 'param'"), arguments) {}
	assert.throws(SyntaxError, f);

	assert.sameValue("arguments" in this, false, "No global 'arguments' binding");
	`

	testScriptWithTestLib(SCRIPT, _undefined, t)
}

func TestArgumentsRedeclareArrow(t *testing.T) {
	const SCRIPT = `
	const oldArguments = globalThis.arguments;
	let count = 0;
	const f = (p = eval("var arguments = 'param'"), q = () => arguments) => {
	  var arguments = "local";
	  assert.sameValue(arguments, "local", "arguments");
	  assert.sameValue(q(), "param", "q");
	  count++;
	}
	f();
	assert.sameValue(count, 1);
	assert.sameValue(globalThis.arguments, oldArguments, "globalThis.arguments unchanged");
	`
	testScriptWithTestLib(SCRIPT, _undefined, t)
}

func TestEvalParamWithDef(t *testing.T) {
	const SCRIPT = `
	function f(param = 0) {
		eval("var param = 1");
		return param;
	}
	f();
	`

	testScript(SCRIPT, valueInt(1), t)
}

func TestArgumentsRedefinedAsLetDyn(t *testing.T) {
	const SCRIPT = `
	function f() {
		let arguments;
		eval(""); // force dynamic scope
		return arguments;
	}
	
	f(1,2);
	`

	testScript(SCRIPT, _undefined, t)
}

func TestWith(t *testing.T) {
	const SCRIPT = `
	var b = 1;
	var o = {a: 41};
	with(o) {
		a += b;
	}
	o.a;

	`

	testScript(SCRIPT, intToValue(42), t)
}

func TestWithInFunc(t *testing.T) {
	const SCRIPT = `
	function F() {
		var b = 1;
		var c = 0;
		var o = {a: 40, c: 1};
		with(o) {
			a += b + c;
		}
		return o.a;
	}

	F();
	`

	testScript(SCRIPT, intToValue(42), t)
}

func TestAssignNonExtendable(t *testing.T) {
	const SCRIPT = `
	'use strict';

	function F() {
    		this.x = 1;
	}

	var o = new F();
	Object.preventExtensions(o);
	o.x = 42;
	o.x;
	`

	testScript(SCRIPT, intToValue(42), t)
}

func TestAssignNonExtendable1(t *testing.T) {
	const SCRIPT = `
	'use strict';

	function F() {
	}

	var o = new F();
	var rv;

	Object.preventExtensions(o);
	try {
		o.x = 42;
	} catch (e) {
		rv = e.constructor === TypeError;
	}

	rv += " " + o.x;
	rv;
	`

	testScript(SCRIPT, asciiString("true undefined"), t)
}

func TestAssignStrict(t *testing.T) {
	const SCRIPT = `
	'use strict';

	try {
		eval("eval = 42");
	} catch(e) {
		var rv = e instanceof SyntaxError
	}
	rv;
	`

	testScript(SCRIPT, valueTrue, t)
}

func TestIllegalArgmentName(t *testing.T) {
	const SCRIPT = `
	'use strict';

	try {
		eval("function F(eval) {}");
	} catch (e) {
		var rv = e instanceof SyntaxError
	}
	rv;
	`

	testScript(SCRIPT, valueTrue, t)
}

func TestFunction(t *testing.T) {
	const SCRIPT = `

	var f0 = Function("");
	var f1 = Function("return ' one'");
	var f2 = Function("arg", "return ' ' + arg");
	f0() + f1() + f2("two");
	`

	testScript(SCRIPT, asciiString("undefined one two"), t)
}

func TestFunction1(t *testing.T) {
	const SCRIPT = `

	var f = function f1(count) {
		if (count == 0) {
			return true;
		}
		return f1(count-1);
	}

	f(1);
	`

	testScript(SCRIPT, valueTrue, t)
}

func TestFunction2(t *testing.T) {
	const SCRIPT = `
	var trace = "";
	function f(count) {
    		trace += "f("+count+")";
    		if (count == 0) {
        		return;
    		}
    		return f(count-1);
	}

	function f1() {
    		trace += "f1";
	}

	var f2 = f;
	f = f1;
	f2(1);
	trace;

	`

	testScript(SCRIPT, asciiString("f(1)f1"), t)
}

func TestFunctionToString(t *testing.T) {
	const SCRIPT = `

	Function("arg1", "arg2", "return 42").toString();
	`

	testScript(SCRIPT, asciiString("function anonymous(arg1,arg2\n) {\nreturn 42\n}"), t)
}

func TestObjectLiteral(t *testing.T) {
	const SCRIPT = `
	var getterCalled = false;
	var setterCalled = false;

	var o = {get x() {getterCalled = true}, set x(_) {setterCalled = true}};

	o.x;
	o.x = 42;

	getterCalled && setterCalled;
	`

	testScript(SCRIPT, valueTrue, t)
}

func TestConst(t *testing.T) {
	const SCRIPT = `

	var v1 = true && true;
	var v2 = 1/(-1 * 0);
	var v3 = 1 == 2 || v1;
	var v4 = true && false
	v1 === true && v2 === -Infinity && v3 === v1 && v4 === false;
	`

	testScript(SCRIPT, valueTrue, t)
}

func TestConstWhile(t *testing.T) {
	const SCRIPT = `
	var c = 0;
	while (2 + 2 === 4) {
		if (++c > 9) {
			break;
		}
	}
	c === 10;
	`

	testScript(SCRIPT, valueTrue, t)
}

func TestConstWhileThrow(t *testing.T) {
	const SCRIPT = `
	var thrown = false;
	try {
		while ('s' in true) {
			break;
		}
	} catch (e) {
		thrown = e instanceof TypeError
	}
	thrown;
	`

	testScript(SCRIPT, valueTrue, t)
}

func TestDupParams(t *testing.T) {
	const SCRIPT = `
	function F(x, y, x) {
		return x;
	}

	F(1, 2);
	`

	testScript(SCRIPT, _undefined, t)
}

func TestUseUnsuppliedParam(t *testing.T) {
	const SCRIPT = `
	function getMessage(message) {
		if (message === undefined) {
			message = '';
		}
		message += " 123 456";
		return message;
	}

	getMessage();
	`

	testScript(SCRIPT, asciiString(" 123 456"), t)
}

func TestForInLetWithInitializer(t *testing.T) {
	const SCRIPT = `for (let x = 3 in {}) { }`
	_, err := Compile("", SCRIPT, false)
	if err == nil {
		t.Fatal("Expected error")
	}
}

func TestForInLoop(t *testing.T) {
	const SCRIPT = `
	function Proto() {}
	Proto.prototype.x = 42;
	var o = new Proto();
	o.y = 44;
	o.x = 45;
	var hasX = false;
	var hasY = false;

	for (var i in o) {
    		switch(i) {
    		case "x":
        		if (hasX) {
            			throw new Error("Already has X");
        		}
        		hasX = true;
        		break;
    		case "y":
        		if (hasY) {
            			throw new Error("Already has Y");
        		}
        		hasY = true;
        		break;
    		}
	}

	hasX && hasY;
	`

	testScript(SCRIPT, valueTrue, t)
}

func TestWhileLoopResult(t *testing.T) {
	const SCRIPT = `
	while(false);

	`

	testScript(SCRIPT, _undefined, t)
}

func TestEmptySwitch(t *testing.T) {
	const SCRIPT = `
	switch(1){}
	`

	testScript(SCRIPT, _undefined, t)
}

func TestEmptyDoWhile(t *testing.T) {
	const SCRIPT = `
	do {} while(false)
	`

	testScript(SCRIPT, _undefined, t)
}

func TestSwitch(t *testing.T) {
	const SCRIPT = `
	function F(x) {
		var i = 0;
		switch (x) {
		case 0:
			i++;
		case 1:
			i++;
		default:
			i++;
		case 2:
			i++;
			break;
		case 3:
			i++;
		}
		return i;
	}

	F(0) + F(1) + F(2) + F(4);

	`

	testScript(SCRIPT, intToValue(10), t)
}

func TestSwitchDefFirst(t *testing.T) {
	const SCRIPT = `
	function F(x) {
		var i = 0;
		switch (x) {
		default:
			i++;
		case 0:
			i++;
		case 1:
			i++;
		case 2:
			i++;
			break;
		case 3:
			i++;
		}
		return i;
	}

	F(0) + F(1) + F(2) + F(4);

	`

	testScript(SCRIPT, intToValue(10), t)
}

func TestSwitchResult(t *testing.T) {
	const SCRIPT = `
	var x = 2;

	switch (x) {
	case 0:
		"zero";
	case 1:
		"one";
	case 2:
		"two";
		break;
	case 3:
		"three";
	default:
		"default";
	}
	`

	testScript(SCRIPT, asciiString("two"), t)
}

func TestSwitchResult1(t *testing.T) {
	const SCRIPT = `
	var x = 0;
	switch (x) { case 0: "two"; case 1: break}
	`

	testScript(SCRIPT, asciiString("two"), t)
}

func TestSwitchResult2(t *testing.T) {
	const SCRIPT = `
	6; switch ("a") { case "a": 7; case "b": }
	`

	testScript(SCRIPT, valueInt(7), t)
}

func TestSwitchResultJumpIntoEmptyEval(t *testing.T) {
	const SCRIPT = `
	function t(x) {
		return eval("switch(x) { case 1: 2; break; case 2: let x = 1; case 3: x+2; break; case 4: default: 9}");
	}
	""+t(2)+t();
	`

	testScript(SCRIPT, asciiString("39"), t)
}

func TestSwitchResultJumpIntoEmpty(t *testing.T) {
	const SCRIPT = `
	switch(2) { case 1: 2; break; case 2: let x = 1; case 3: x+2; case 4: {let y = 2}; break; default: 9};
	`

	testScript(SCRIPT, valueInt(3), t)
}

func TestSwitchLexical(t *testing.T) {
	const SCRIPT = `
	switch (true) { case true: let x = 1; }
	`

	testScript(SCRIPT, _undefined, t)
}

func TestSwitchBreakOuter(t *testing.T) {
	const SCRIPT = `
	LOOP:
	for (let i = 0; i < 10; i++) {
		switch (i) {
		case 0:
			continue;
		case 1:
			let x = 1;
			continue;
		case 2:
			try {
				x++;
			} catch (e) {
				if (e instanceof ReferenceError) {
					break LOOP;
				}
			}
			throw new Error("Exception was not thrown");
		}
	}
	`

	testScript(SCRIPT, _undefined, t)
}

func TestIfBreakResult(t *testing.T) {
	const SCRIPT = `
	L: {if (true) {42;} break L;}
	`

	testScript(SCRIPT, intToValue(42), t)
}

func TestSwitchNoMatch(t *testing.T) {
	const SCRIPT = `
	var result;
	var x;
	switch (x) {
	case 0:
		result = "2";
		break;
	}

	result;

	`

	testScript(SCRIPT, _undefined, t)
}

func TestSwitchNoMatchNoDefault(t *testing.T) {
	const SCRIPT = `
		switch (1) {
		case 0:
		}
	`

	testScript(SCRIPT, _undefined, t)
}

func TestSwitchNoMatchNoDefaultNoResult(t *testing.T) {
	const SCRIPT = `
		switch (1) {
		case 0:
		}
		42;
	`

	testScript(SCRIPT, intToValue(42), t)
}

func TestSwitchNoMatchNoDefaultNoResultMatch(t *testing.T) {
	const SCRIPT = `
		switch (1) {
		case 1:
		}
		42;
	`

	testScript(SCRIPT, intToValue(42), t)
}

func TestEmptySwitchNoResult(t *testing.T) {
	const SCRIPT = `
		switch (1) {}
		42;
	`

	testScript(SCRIPT, intToValue(42), t)
}

func TestGetOwnPropertyNames(t *testing.T) {
	const SCRIPT = `
	var o = {
		prop1: 42,
		prop2: "test"
	}

	var hasProp1 = false;
	var hasProp2 = false;

	var names = Object.getOwnPropertyNames(o);
	for (var i in names) {
		var p = names[i];
		switch(p) {
		case "prop1":
			hasProp1 = true;
			break;
		case "prop2":
			hasProp2 = true;
			break;
		}
	}

	hasProp1 && hasProp2;
	`

	testScript(SCRIPT, valueTrue, t)
}

func TestArrayLiteral(t *testing.T) {
	const SCRIPT = `

	var f1Called = false;
	var f2Called = false;
	var f3Called = false;
	var errorThrown = false;

	function F1() {
		f1Called = true;
	}

	function F2() {
		f2Called = true;
	}

	function F3() {
		f3Called = true;
	}


	try {
		var a = [F1(), x(F3()), F2()];
	} catch(e) {
		if (e instanceof ReferenceError) {
			errorThrown = true;
		} else {
			throw e;
		}
	}

	f1Called && !f2Called && f3Called && errorThrown && a === undefined;
	`

	testScript(SCRIPT, valueTrue, t)
}

func TestJumpOutOfReturn(t *testing.T) {
	const SCRIPT = `
	function f() {
		var a;
		if (a == 0) {
			return true;
		}
	}

	f();
	`

	testScript(SCRIPT, _undefined, t)
}

func TestSwitchJumpOutOfReturn(t *testing.T) {
	const SCRIPT = `
	function f(x) {
		switch(x) {
		case 0:
			break;
		default:
			return x;
		}
	}

	f(0);
	`

	testScript(SCRIPT, _undefined, t)
}

func TestSetToReadOnlyPropertyStrictBracket(t *testing.T) {
	const SCRIPT = `
	'use strict';

	var o = {};
	var thrown = false;
	Object.defineProperty(o, "test", {value: 42, configurable: true});
	try {
		o["test"] = 43;
	} catch (e) {
		thrown = e instanceof TypeError;
	}

	thrown;
	`

	testScript(SCRIPT, valueTrue, t)
}

func TestSetToReadOnlyPropertyStrictDot(t *testing.T) {
	const SCRIPT = `
	'use strict';

	var o = {};
	var thrown = false;
	Object.defineProperty(o, "test", {value: 42, configurable: true});
	try {
		o.test = 43;
	} catch (e) {
		thrown = e instanceof TypeError;
	}

	thrown;
	`

	testScript(SCRIPT, valueTrue, t)
}

func TestDeleteNonConfigurablePropertyStrictBracket(t *testing.T) {
	const SCRIPT = `
	'use strict';

	var o = {};
	var thrown = false;
	Object.defineProperty(o, "test", {value: 42});
	try {
		delete o["test"];
	} catch (e) {
		thrown = e instanceof TypeError;
	}

	thrown;
	`

	testScript(SCRIPT, valueTrue, t)
}

func TestDeleteNonConfigurablePropertyStrictDot(t *testing.T) {
	const SCRIPT = `
	'use strict';

	var o = {};
	var thrown = false;
	Object.defineProperty(o, "test", {value: 42});
	try {
		delete o.test;
	} catch (e) {
		thrown = e instanceof TypeError;
	}

	thrown;
	`

	testScript(SCRIPT, valueTrue, t)
}

func TestCompound1(t *testing.T) {
	const SCRIPT = `
	var x = 0;
  	var scope = {x: 1};
    	var f;
  	with (scope) {
    		f = function() {
        		x *= (delete scope.x, 2);
    		}
  	}
	f();

	scope.x === 2 && x === 0;

	`

	testScript(SCRIPT, valueTrue, t)
}

func TestCompound2(t *testing.T) {
	const SCRIPT = `

var x;
x = "x";
x ^= "1";

	`
	testScript(SCRIPT, intToValue(1), t)
}

func TestDeleteArguments(t *testing.T) {
	defer func() {
		if _, ok := recover().(*CompilerSyntaxError); !ok {
			t.Fatal("Expected syntax error")
		}
	}()
	const SCRIPT = `
	'use strict';

	function f() {
		delete arguments;
	}

	`
	testScript(SCRIPT, _undefined, t)
}

func TestReturnUndefined(t *testing.T) {
	const SCRIPT = `
	function f() {
    		return x;
	}

	var thrown = false;
	try {
		f();
	} catch (e) {
		thrown = e instanceof ReferenceError;
	}

	thrown;
	`
	testScript(SCRIPT, valueTrue, t)
}

func TestForBreak(t *testing.T) {
	const SCRIPT = `
	var supreme, count;
	supreme = 5;
	var __evaluated =  eval("for(count=0;;) {if (count===supreme)break;else count++; }");
    	if (__evaluated !== void 0) {
        	throw new Error('#1: __evaluated === 4. Actual:  __evaluated ==='+ __evaluated  );
    	}

	`
	testScript(SCRIPT, _undefined, t)
}

func TestLargeNumberLiteral(t *testing.T) {
	const SCRIPT = `
	var x = 0x800000000000000000000;
	x.toString();
	`
	testScript(SCRIPT, asciiString("9.671406556917033e+24"), t)
}

func TestIncDelete(t *testing.T) {
	const SCRIPT = `
	var o = {x: 1};
	o.x += (delete o.x, 1);
	o.x;
	`
	testScript(SCRIPT, intToValue(2), t)
}

func TestCompoundAssignRefError(t *testing.T) {
	const SCRIPT = `
	var thrown = false;
	try {
		a *= 1;
	} catch (e) {
		if (e instanceof ReferenceError) {
			thrown = true;
		} else {
			throw e;
		}
	}
	thrown;
	`
	testScript(SCRIPT, valueTrue, t)
}

func TestObjectLiteral__Proto__(t *testing.T) {
	const SCRIPT = `
	var o = {
		__proto__: null,
		test: 42
	}

	Object.getPrototypeOf(o);
	`

	testScript(SCRIPT, _null, t)
}

func TestEmptyCodeError(t *testing.T) {
	if _, err := New().RunString(`i`); err == nil {
		t.Fatal("Expected an error")
	} else {
		if e := err.Error(); e != "ReferenceError: i is not defined at <eval>:1:1(0)" {
			t.Fatalf("Unexpected error: '%s'", e)
		}
	}
}

func TestForOfArray(t *testing.T) {
	const SCRIPT = `
	var array = [0, 'a', true, false, null, /* hole */, undefined, NaN];
	var i = 0;
	
	for (var value of array) {
	  assert.sameValue(value, array[i], 'element at index ' + i);
	  i++;
	}
	
	assert.sameValue(i, 8, 'Visits all elements');
	`
	testScriptWithTestLib(SCRIPT, _undefined, t)
}

func TestForOfReturn(t *testing.T) {
	const SCRIPT = `
	var callCount = 0;
	var iterationCount = 0;
	var iterable = {};
	var x = {
	  set attr(_) {
		throw new Test262Error();
	  }
	};
	
	iterable[Symbol.iterator] = function() {
	  return {
		next: function() {
		  return { done: false, value: 0 };
		},
		return: function() {
		  callCount += 1;
		}
	  }
	};
	
	assert.throws(Test262Error, function() {
	  for (x.attr of iterable) {
		iterationCount += 1;
	  }
	});
	
	assert.sameValue(iterationCount, 0, 'The loop body is not evaluated');
	assert.sameValue(callCount, 1, 'Iterator is closed');
	`
	testScriptWithTestLib(SCRIPT, _undefined, t)
}

func TestForOfReturn1(t *testing.T) {
	const SCRIPT = `
	var iterable = {};
	var iterationCount = 0;

	iterable[Symbol.iterator] = function() {
	  return {
		next: function() {
		  return { done: false, value: null };
		},
		get return() {
		  throw new Test262Error();
		}
	  };
	};

	assert.throws(Test262Error, function() {
	  for (var x of iterable) {
		iterationCount += 1;
		break;
	  }
	});

	assert.sameValue(iterationCount, 1, 'The loop body is evaluated');
	`
	testScriptWithTestLib(SCRIPT, _undefined, t)
}

func TestForOfLet(t *testing.T) {
	const SCRIPT = `
	var iterCount = 0;
	function f() {}
	for (var let of [23]) {
		f(let);
		if (let != 23) {
			throw new Error("");
		}
		iterCount += 1;
	}

	iterCount;
`
	testScript(SCRIPT, valueInt(1), t)
}

func TestForOfLetLet(t *testing.T) {
	const SCRIPT = `
	for (let let of [23]) {
	}
`
	_, err := Compile("", SCRIPT, false)
	if err == nil {
		t.Fatal("Expected error")
	}
}

func TestForHeadLet(t *testing.T) {
	const SCRIPT = `
	for (let = 0; let < 2; let++);
`
	testScript(SCRIPT, _undefined, t)
}

func TestLhsLet(t *testing.T) {
	const SCRIPT = `
	let = 1;
	let;
	`
	testScript(SCRIPT, valueInt(1), t)
}

func TestLetPostfixASI(t *testing.T) {
	const SCRIPT = `
	let
	++
	`
	_, err := Compile("", SCRIPT, false)
	if err == nil {
		t.Fatal("Expected error")
	}
}

func TestIteratorReturnNormal(t *testing.T) {
	const SCRIPT = `
	var iterable = {};
	var iterationCount = 0;

	iterable[Symbol.iterator] = function() {
	  return {
		next: function() {
		  return { done: ++iterationCount > 2, value: null };
		},
		get return() {
		  throw new Test262Error();
		}
	  };
	};

	for (var x of iterable) {
	}
	`
	testScriptWithTestLib(SCRIPT, _undefined, t)
}

func TestIteratorReturnErrorNested(t *testing.T) {
	const SCRIPT = `
	var returnCalled = {};
	function iter(id) {
		return function() {
			var count = 0;
			return {
				next: function () {
					return {
						value: null,
						done: ++count > 2
					};
				},
				return: function () {
					returnCalled[id] = true;
					throw new Error(id);
				}
			};
		}
	}
	var iterable1 = {};
	iterable1[Symbol.iterator] = iter("1");
	var iterable2 = {};
	iterable2[Symbol.iterator] = iter("2");

	try {
		for (var i of iterable1) {
			for (var j of iterable2) {
				break;
			}
		}
		throw new Error("no exception was thrown");
	} catch (e) {
		if (e.message !== "2") {
			throw e;
		}
	}
	if (!returnCalled["1"]) {
		throw new Error("no return 1");
	}
	if (!returnCalled["2"]) {
		throw new Error("no return 2");
	}
	`
	testScript(SCRIPT, _undefined, t)
}

func TestReturnFromForInLoop(t *testing.T) {
	const SCRIPT = `
	(function f() {
		for (var i in {a: 1}) {
			return true;
		}
	})();
	`
	testScript(SCRIPT, valueTrue, t)
}

func TestReturnFromForOfLoop(t *testing.T) {
	const SCRIPT = `
	(function f() {
		for (var i of [1]) {
			return true;
		}
	})();
	`
	testScript(SCRIPT, valueTrue, t)
}

func TestIfStackLeaks(t *testing.T) {
	const SCRIPT = `
	var t = 0;
	if (t === 0) {
		t;
	}
	`
	testScript(SCRIPT, _positiveZero, t)
}

func TestWithCallee(t *testing.T) {
	const SCRIPT = `
	function O() {
		var that = this;
		this.m = function() {
			return this === that;
		}
	}
	with(new O()) {
		m();
	}
	`
	testScript(SCRIPT, valueTrue, t)
}

func TestWithScope(t *testing.T) {
	const SCRIPT = `
	function f(o) {
		var x = 42;

		function innerf(o) {
			with (o) {
				return x;
			}
		}

		return innerf(o);
	}
	f({});
	`
	testScript(SCRIPT, valueInt(42), t)
}

func TestEvalCallee(t *testing.T) {
	const SCRIPT = `
	(function () {
		'use strict';
		var v = function() {
			return this === undefined;
		};
		return eval('v()');
	})();
	`
	testScript(SCRIPT, valueTrue, t)
}

func TestEvalBindingDeleteVar(t *testing.T) {
	const SCRIPT = `
	(function () {
		eval("var x = 1");
		return x === 1 && delete x;
	})();
	`
	testScript(SCRIPT, valueTrue, t)
}

func TestEvalBindingDeleteFunc(t *testing.T) {
	const SCRIPT = `
	(function () {
		eval("function x(){}");
		return typeof x === "function" && delete x;
	})();
	`
	testScript(SCRIPT, valueTrue, t)
}

func TestDeleteGlobalLexical(t *testing.T) {
	const SCRIPT = `
	let x;
	delete x;
	`
	testScript(SCRIPT, valueFalse, t)
}

func TestDeleteGlobalEval(t *testing.T) {
	const SCRIPT = `
	eval("var x");
	delete x;
	`
	testScript(SCRIPT, valueTrue, t)
}

func TestTryResultEmpty(t *testing.T) {
	const SCRIPT = `
	1; try { } finally { }
	`
	testScript(SCRIPT, _undefined, t)
}

func TestTryResultEmptyCatch(t *testing.T) {
	const SCRIPT = `
	1; try { throw null } catch(e) { }
	`
	testScript(SCRIPT, _undefined, t)
}

func TestTryResultEmptyContinueLoop(t *testing.T) {
	const SCRIPT = `
	for (var i = 0; i < 2; i++) { try {throw null;} catch(e) {continue;} 'bad'}
	`
	testScript(SCRIPT, _undefined, t)
}

func TestTryEmptyCatchStackLeak(t *testing.T) {
	const SCRIPT = `
	(function() {
		var f;
		// Make sure the outer function is not stashless.
		(function() {
			f++;
		})();
		try {
			throw new Error();
		} catch(e) {}
	})();
	`
	testScript(SCRIPT, _undefined, t)
}

func TestTryThrowEmptyCatch(t *testing.T) {
	const SCRIPT = `
	try {
		throw new Error();
	}
	catch (e) {}
	`
	testScript(SCRIPT, _undefined, t)
}

func TestFalsyLoopBreak(t *testing.T) {
	const SCRIPT = `
	while(false) {
	  	break;
	}
	for(;false;) {
		break;
	}
	undefined;
	`
	MustCompile("", SCRIPT, false)
}

func TestFalsyLoopBreakWithResult(t *testing.T) {
	const SCRIPT = `
	while(false) {
	  break;
	}
	`
	testScript(SCRIPT, _undefined, t)
}

func TestDummyCompile(t *testing.T) {
	const SCRIPT = `
	'use strict';
	
	for (;false;) {
		eval = 1;
	}
	`

	_, err := Compile("", SCRIPT, false)
	if err == nil {
		t.Fatal("expected error")
	}
}

func TestDummyCompileForUpdate(t *testing.T) {
	const SCRIPT = `
	'use strict';
	
	for (;false;eval=1) {
	}
	`

	_, err := Compile("", SCRIPT, false)
	if err == nil {
		t.Fatal("expected error")
	}
}

func TestObjectLiteralWithNumericKeys(t *testing.T) {
	const SCRIPT = `
	var o = {1e3: true};
	var keys = Object.keys(o);
	var o1 = {get 1e3() {return true;}};
	var keys1 = Object.keys(o1);
	var o2 = {1e21: true};
	var keys2 = Object.keys(o2);
	let o3 = {0(){return true}};
	keys.length === 1 && keys[0] === "1000" && 
	keys1.length === 1 && keys1[0] === "1000" && o1[1e3] === true &&
	keys2.length === 1 && keys2[0] === "1e+21" && o3[0]();
	`
	testScript(SCRIPT, valueTrue, t)
}

func TestEscapedObjectPropertyKeys(t *testing.T) {
	const SCRIPT = `
	var obj = {
		w\u0069th: 42
	};
	var obj = {
		with() {42}
	};
	`

	_, err := Compile("", SCRIPT, false)
	if err != nil {
		t.Fatal(err)
	}
}

func TestEscapedKeywords(t *testing.T) {
	const SCRIPT = `r\u0065turn;`
	_, err := Compile("", SCRIPT, false)
	if err == nil {
		t.Fatal("Expected error")
	}
}

func TestEscapedLet(t *testing.T) {
	const SCRIPT = `
this.let = 0;

l\u0065t // ASI
a;

// If the parser treated the previous escaped "let" as a lexical declaration,
// this variable declaration will result an early syntax error.
var a;
`
	_, err := Compile("", SCRIPT, false)
	if err != nil {
		t.Fatal(err)
	}
}

func TestObjectLiteralFuncProps(t *testing.T) {
	const SCRIPT = `
	(function() {
		'use strict';
		var o = {
			eval: function() {return 1;},
			arguments() {return 2;},
			test: function test1() {}
		}
		assert.sameValue(o.eval.name, "eval");
		assert.sameValue(o.arguments.name, "arguments");
		assert.sameValue(o.eval(), 1);
		assert.sameValue(o.arguments(), 2);
		assert.sameValue(o.test.name, "test1");
	})();
	`

	testScriptWithTestLib(SCRIPT, _undefined, t)
}

func TestFuncName(t *testing.T) {
	const SCRIPT = `
	var method = 1;
	var o = {
		method: function() {
			return method;
		},
		method1: function method() {
			return method;
		}
	}
	o.method() === 1 && o.method1() === o.method1;
	`

	testScript(SCRIPT, valueTrue, t)
}

func TestFuncNameAssign(t *testing.T) {
	const SCRIPT = `
	var f = function() {};
	var f1;
	f1 = function() {};
	let f2 = function() {};

	f.name === "f" && f1.name === "f1" && f2.name === "f2";
	`

	testScript(SCRIPT, valueTrue, t)
}

func TestLexicalDeclGlobal(t *testing.T) {
	const SCRIPT = `
	if (true) {
		let it = "be";
		if (it !== "be") {
			throw new Error(it);
		}
	}
	let thrown = false;
	try {
		it;
	} catch(e) {
		if (e instanceof ReferenceError) {
			thrown = true;
		}
	}
	thrown;
	`
	testScript(SCRIPT, valueTrue, t)
}

func TestLexicalDeclFunction(t *testing.T) {
	const SCRIPT = `
	function f() {
		if (true) {
			let it = "be";
			if (it !== "be") {
				throw new Error(it);
			}
		}
		let thrown = false;
		try {
			it;
		} catch(e) {
			if (e instanceof ReferenceError) {
				thrown = true;
			}
		}
		return thrown;
	}
	f();
	`
	testScript(SCRIPT, valueTrue, t)
}

func TestLexicalDynamicScope(t *testing.T) {
	const SCRIPT = `
	const global = 1;
	function f() {
		const func = global + 1;
		function inner() {
			function assertThrows(fn) {
				let thrown = false;
				try {
					fn();
				} catch (e) {
					if (e instanceof TypeError) {
						thrown = true;
					} else {
						throw e;
					}
				}
				if (!thrown) {
					throw new Error("Did not throw");
				}
			}

			assertThrows(function() {
				func++;
			});
			assertThrows(function() {
				global++;
			});

			assertThrows(function() {
				eval("func++");
			});
			assertThrows(function() {
				eval("global++");
			});

			return eval("func + 1");
		}
		return inner();
	}
	f();
	`
	testScript(SCRIPT, valueInt(3), t)
}

func TestLexicalDynamicScope1(t *testing.T) {
	const SCRIPT = `
	(function() {
		const x = 1 * 4;
		return (function() {
			eval("");
			return x;
		})();
	})();
	`
	testScript(SCRIPT, intToValue(4), t)
}

func TestLexicalDynamicScope2(t *testing.T) {
	const SCRIPT = `
	(function() {
		const x = 1 + 3;
		var y = 2 * 2;
		eval("");
		return x;
	})();
	`
	testScript(SCRIPT, intToValue(4), t)
}

func TestNonStrictLet(t *testing.T) {
	const SCRIPT = `
	var let = 1;
	`

	testScript(SCRIPT, _undefined, t)
}

func TestStrictLet(t *testing.T) {
	const SCRIPT = `
	var let = 1;
	`

	_, err := Compile("", SCRIPT, true)
	if err == nil {
		t.Fatal("Expected an error")
	}
}

func TestLetLet(t *testing.T) {
	const SCRIPT = `
	let let = 1;
	`

	_, err := Compile("", SCRIPT, false)
	if err == nil {
		t.Fatal("Expected an error")
	}
}

func TestLetASI(t *testing.T) {
	const SCRIPT = `
	while (false) let // ASI
	x = 1;
	`

	_, err := Compile("", SCRIPT, false)
	if err != nil {
		t.Fatal(err)
	}
}

func TestLetASI1(t *testing.T) {
	const SCRIPT = `
	let
	x = 1;
	`

	_, err := Compile("", SCRIPT, true)
	if err != nil {
		t.Fatal(err)
	}
}

func TestLetNoASI(t *testing.T) {
	const SCRIPT = `
	function f() {}let
x = 1;
	`

	_, err := Compile("", SCRIPT, true)
	if err != nil {
		t.Fatal(err)
	}
}

func TestLetNoASI1(t *testing.T) {
	const SCRIPT = `
let
let = 1;
	`

	_, err := Compile("", SCRIPT, false)
	if err == nil {
		t.Fatal("Expected error")
	}
}

func TestLetArrayWithNewline(t *testing.T) {
	const SCRIPT = `
    with ({}) let
    [a] = 0;
	`

	_, err := Compile("", SCRIPT, false)
	if err == nil {
		t.Fatal("Expected error")
	}
}

func TestDynamicUninitedVarAccess(t *testing.T) {
	const SCRIPT = `
	function f() {
		var x;
		return eval("x");
	}
	f();
	`
	testScript(SCRIPT, _undefined, t)
}

func TestLexicalForLoopNoClosure(t *testing.T) {
	const SCRIPT = `
	let sum = 0;
	for (let i = 0; i < 3; i++) {
		sum += i;
	}
	sum;
	`
	testScript(SCRIPT, valueInt(3), t)
}

func TestLexicalForLoopClosure(t *testing.T) {
	const SCRIPT = `
	var f = [];
	for (let i = 0; i < 3; i++) {
		f.push(function() {
			return i;
		});
	}
	f.length === 3 && f[0]() === 0 && f[1]() === 1 && f[2]() === 2;
	`
	testScript(SCRIPT, valueTrue, t)
}

func TestLexicalForLoopClosureInNext(t *testing.T) {
	const SCRIPT = `
	const a = [];
	for (let i = 0; i < 5; a.push(function () { return i; }), ++i) { }
	let res = "";
	for (let k = 0; k < 5; ++k) {
		res += ""+a[k]();
	}
	res;
	`
	testScript(SCRIPT, asciiString("12345"), t)
}

func TestVarForLoop(t *testing.T) {
	const SCRIPT = `
	var f = [];
	for (var i = 0, j = 0; i < 3; i++) {
		f.push(function() {
			return i;
		});
	}
	f.length === 3 && f[0]() === 3 && f[1]() === 3 && f[2]() === 3;
	`
	testScript(SCRIPT, valueTrue, t)
}

func TestLexicalForOfLoop(t *testing.T) {
	const SCRIPT = `
	var f = [];
	for (let i of [0, 1, 2]) {
		f.push(function() {
			return i;
		});
	}
	f.length === 3 && f[0]() === 0 && f[1]() === 1 && f[2]() === 2;
	`
	testScript(SCRIPT, valueTrue, t)
}

func TestLexicalForOfLoopContBreak(t *testing.T) {
	const SCRIPT = `
	const f = [];
	for (let i of [0, 1, 2, 3, 4, 5]) {
		if (i % 2) continue;
		f.push(function() {
			return i;
		});
		if (i > 2) break;
	}
	let res = "";
	f.forEach(function(item) {res += item()});
	f.length === 3 && res === "024";
	`
	testScript(SCRIPT, valueTrue, t)
}

func TestVarBlockConflict(t *testing.T) {
	const SCRIPT = `
	let x;
	{
		if (false) {
			var x;
		}
	}
	`
	_, err := Compile("", SCRIPT, false)
	if err == nil {
		t.Fatal("Expected an error")
	}
}

func TestVarBlockConflictEval(t *testing.T) {
	const SCRIPT = `
	assert.throws(SyntaxError, function() {
		let x;
		{
			if (true) {
				eval("var x");
			}
		}
	});
	`
	testScriptWithTestLib(SCRIPT, _undefined, t)
}

func TestVarBlockNoConflict(t *testing.T) {
	const SCRIPT = `
	function f() {
		let x;
		function ff() {
			{
				var x = 3;
			}
		}
		ff();
	}
	f();
	`
	testScript(SCRIPT, _undefined, t)
}

func TestVarBlockNoConflictEval(t *testing.T) {
	const SCRIPT = `
	function f() {
		let x;
		function ff() {
			{
				eval("var x = 3");
			}
		}
		ff();
	}
	f();
	`
	testScript(SCRIPT, _undefined, t)
}

func TestVarDeclCorrectScope(t *testing.T) {
	const SCRIPT = `
	function f() {
		{
			let z;
			eval("var x = 3");
		}
		return x;
	}
	f();
	`
	testScript(SCRIPT, valueInt(3), t)
}

func TestLexicalCatch(t *testing.T) {
	const SCRIPT = `
	try {
		throw null;
	} catch (e) {
		let x = 1;
		function f() {}
		e;
	}
	`
	testScript(SCRIPT, _null, t)
}

func TestArgumentsLexicalDecl(t *testing.T) {
	const SCRIPT = `
	function f1() {
		let arguments;
		return arguments;
	}
	f1(42);
	`
	testScript(SCRIPT, _undefined, t)
}

func TestArgumentsLexicalDeclAssign(t *testing.T) {
	const SCRIPT = `
	function f1() {
		let arguments = arguments;
		return a;
	}
	assert.throws(ReferenceError, function() {
		f1(42);
	});
	`
	testScriptWithTestLib(SCRIPT, _undefined, t)
}

func TestLexicalConstModifyFromEval(t *testing.T) {
	const SCRIPT = `
	const x = 1;
	function f() {
		eval("x = 2");
	}
	assert.throws(TypeError, function() {
		f();
	});
	`
	testScriptWithTestLib(SCRIPT, _undefined, t)
}

func TestLexicalStrictNames(t *testing.T) {
	const SCRIPT = `let eval = 1;`

	_, err := Compile("", SCRIPT, true)
	if err == nil {
		t.Fatal("Expected an error")
	}
}

func TestAssignAfterStackExpand(t *testing.T) {
	// make sure the reference to the variable x does not remain stale after the stack is copied
	const SCRIPT = `
	function f() {
		let sum = 0;
		for (let i = 0; i < arguments.length; i++) {
			sum += arguments[i];
		}
		return sum;
	}
	function testAssignment() {
	  var x = 0;
	  var scope = {};

	  with (scope) {
		x = (scope.x = f(0, 0, 0, 0, 0, 0, 1, 1), 1);
	  }

	  if (scope.x !== 2) {
		throw new Error('#1: scope.x === 2. Actual: ' + (scope.x));
	  }
	  if (x !== 1) {
		throw new Error('#2: x === 1. Actual: ' + (x));
	  }
	}
	testAssignment();
	`
	testScript(SCRIPT, _undefined, t)
}

func TestArgAccessFromDynamicStash(t *testing.T) {
	const SCRIPT = `
	function f(arg) {
		function test() {
			eval("");
			return a;
		}
		return arg;
	}
	f(true);
	`
	testScript(SCRIPT, valueTrue, t)
}

func TestLoadMixedLex(t *testing.T) {
	const SCRIPT = `
	function f() {
		let a = 1;
		{
			function inner() {
				eval("var a = true");
				return a;
			}
			return inner();
		}
	}
	f();
	`
	testScript(SCRIPT, valueTrue, t)
}

func TestObjectLiteralSpread(t *testing.T) {
	const SCRIPT = `
	let src = {prop1: 1};
	Object.defineProperty(src, "prop2", {value: 2, configurable: true});
	Object.defineProperty(src, "prop3", {value: 3, enumerable: true, configurable: true});
	let target = {prop4: 4, ...src};
	assert(deepEqual(target, {prop1: 1, prop3: 3, prop4: 4}));
	`
	testScriptWithTestLibX(SCRIPT, _undefined, t)
}

func TestArrayLiteralSpread(t *testing.T) {
	const SCRIPT = `
	let a1 = [1, 2];
	let a2 = [3, 4];
	let a = [...a1, 0, ...a2, 1];
	assert(compareArray(a, [1, 2, 0, 3, 4, 1]));
	`
	testScriptWithTestLib(SCRIPT, _undefined, t)
}

func TestObjectAssignmentPattern(t *testing.T) {
	const SCRIPT = `
	let a, b, c;
	({a, b, c=3} = {a: 1, b: 2});
	assert.sameValue(a, 1, "a");
	assert.sameValue(b, 2, "b");
	assert.sameValue(c, 3, "c");
	`
	testScriptWithTestLib(SCRIPT, _undefined, t)
}

func TestObjectAssignmentPatternNoDyn(t *testing.T) {
	const SCRIPT = `
	(function() {
		let a, b, c;
		({a, b, c=3} = {a: 1, b: 2});
		assert.sameValue(a, 1, "a");
		assert.sameValue(b, 2, "b");
		assert.sameValue(c, 3, "c");
	})();
	`
	testScriptWithTestLib(SCRIPT, _undefined, t)
}

func TestObjectAssignmentPatternNested(t *testing.T) {
	const SCRIPT = `
	let a, b, c, d;
	({a, b, c: {d} = 3} = {a: 1, b: 2, c: {d: 4}});
	assert.sameValue(a, 1, "a");
	assert.sameValue(b, 2, "b");
	assert.sameValue(c, undefined, "c");
	assert.sameValue(d, 4, "d");
	`
	testScriptWithTestLib(SCRIPT, _undefined, t)
}

func TestObjectAssignmentPatternEvalOrder(t *testing.T) {
	const SCRIPT = `
	let trace = "";
	let target_obj = {};

	function src() {
	    trace += "src(),";
		return {
			get a() {
				trace += "get a,";
				return "a";
			}
		}
	}
	
	function prop1() {
		trace += "prop1(),"
		return {
			toString: function() {
				trace += "prop1-to-string(),";
				return "a";
			}
		}
	}
	
	function prop2() {
		trace += "prop2(),";
		return {
			toString: function() {
				trace += "prop2-to-string(),";
				return "b";
			}
		}
	}
	
	function target() {
		trace += "target(),"
		return target_obj;
	}
	
	let a, b;
	
	({[prop1()]: target().a, [prop2()]: b} = src());
	if (target_obj.a !== "a") {
		throw new Error("target_obj.a="+target_obj.a);
	}
	trace;
	`
	testScript(SCRIPT, asciiString("src(),prop1(),prop1-to-string(),target(),get a,prop2(),prop2-to-string(),"), t)
}

func TestArrayAssignmentPatternEvalOrder(t *testing.T) {
	const SCRIPT = `
	let trace = "";

	let src_arr = {
		[Symbol.iterator]: function() {
			let done = false;
			return {
				next: function() {
					trace += "next,";
					if (!done) {
						done = true;
						return {value: 0};
					}
					return {done: true};
				},
				return: function() {
					trace += "return,";
				}
			}
		}
	}

	function src() {
		trace += "src(),";
		return src_arr;
	}

	let tgt = {
		get a() {
			trace += "get a,";
			return "a";
		},
		get b() {
			trace += "get b,";
			return "b";
		}
	}

	function target() {
		trace += "target(),";
		return tgt;
	}

	function default_a() {
		trace += "default a,";
		return "def_a";
	}

	function default_b() {
		trace += "default b,";
		return "def_b";
	}

	([target().a = default_a(), target().b = default_b()] = src());
	trace;
	`
	testScript(SCRIPT, asciiString("src(),target(),next,target(),next,default b,"), t)
}

func TestObjectAssignPatternRest(t *testing.T) {
	const SCRIPT = `
	let a, b, c, d;
	({a, b, c, ...d} = {a: 1, b: 2, d: 4});
	assert.sameValue(a, 1, "a");
	assert.sameValue(b, 2, "b");
	assert.sameValue(c, undefined, "c");
	assert(deepEqual(d, {d: 4}), "d");
	`
	testScriptWithTestLibX(SCRIPT, _undefined, t)
}

func TestObjectBindPattern(t *testing.T) {
	const SCRIPT = `
	let {a, b, c, ...d} = {a: 1, b: 2, d: 4};
	assert.sameValue(a, 1, "a");
	assert.sameValue(b, 2, "b");
	assert.sameValue(c, undefined, "c");
	assert(deepEqual(d, {d: 4}), "d");

	var { x: y, } = { x: 23 };
	
	assert.sameValue(y, 23);
	
	assert.throws(ReferenceError, function() {
	  x;
	});
	`
	testScriptWithTestLibX(SCRIPT, _undefined, t)
}

func TestObjLiteralShorthandWithInitializer(t *testing.T) {
	const SCRIPT = `
	o = {a=1};
	`
	_, err := Compile("", SCRIPT, false)
	if err == nil {
		t.Fatal("Expected an error")
	}
}

func TestObjLiteralShorthandLetStringLit(t *testing.T) {
	const SCRIPT = `
	o = {"let"};
	`
	_, err := Compile("", SCRIPT, false)
	if err == nil {
		t.Fatal("Expected an error")
	}
}

func TestObjLiteralComputedKeys(t *testing.T) {
	const SCRIPT = `
	let o = {
		get [Symbol.toString]() {
		}
	}
	`
	testScript(SCRIPT, _undefined, t)
}

func TestObjLiteralComputedKeysEvalOrder(t *testing.T) {
	const SCRIPT = `
	let trace = [];
	function key() {
		trace.push("key");
		return {
			toString: function() {
				trace.push("key-toString");
				return "key";
			}
		}
	}
	function val() {
		trace.push("val");
		return "val";
	}
	
	const _ = {
		[key()]: val(),
	}
	
	trace.join(",");
	`
	testScript(SCRIPT, asciiString("key,key-toString,val"), t)
}

func TestArrayAssignPattern(t *testing.T) {
	const SCRIPT = `
	let a, b;
	([a, b] = [1, 2]);
	a === 1 && b === 2;
	`
	testScript(SCRIPT, valueTrue, t)
}

func TestArrayAssignPattern1(t *testing.T) {
	const SCRIPT = `
	let a, b;
	([a = 3, b = 2] = [1]);
	a === 1 && b === 2;
	`
	testScript(SCRIPT, valueTrue, t)
}

func TestArrayAssignPatternLHS(t *testing.T) {
	const SCRIPT = `
	let a = {};
	[ a.b, a['c'] = 2 ] = [1];
	a.b === 1 && a.c === 2;
	`
	testScript(SCRIPT, valueTrue, t)
}

func TestArrayAssignPatternElision(t *testing.T) {
	const SCRIPT = `
	let a, b;
	([a,, b] = [1, 4, 2]);
	a === 1 && b === 2;
	`
	testScript(SCRIPT, valueTrue, t)
}

func TestArrayAssignPatternRestPattern(t *testing.T) {
	const SCRIPT = `
	let a, b, z;
	[ z, ...[a, b] ] = [0, 1, 2];
	z === 0 && a === 1 && b === 2;
	`
	testScript(SCRIPT, valueTrue, t)
}

func TestArrayBindingPattern(t *testing.T) {
	const SCRIPT = `
	let [a, b] = [1, 2];
	a === 1 && b === 2;
	`
	testScript(SCRIPT, valueTrue, t)
}

func TestObjectPatternShorthandInit(t *testing.T) {
	const SCRIPT = `
	[...{ x = 1 }] = [];
	x;
	`
	testScript(SCRIPT, valueInt(1), t)
}

func TestArrayBindingPatternRestPattern(t *testing.T) {
	const SCRIPT = `
	const [a, b, ...[c, d]] = [1, 2, 3, 4];
	a === 1 && b === 2 && c === 3 && d === 4;
	`
	testScript(SCRIPT, valueTrue, t)
}

func TestForVarPattern(t *testing.T) {
	const SCRIPT = `
	var o = {a: 1};
	var trace = "";
	for (var [key, value] of Object.entries(o)) {
		trace += key+":"+value;
	}
	trace;
	`
	testScript(SCRIPT, asciiString("a:1"), t)
}

func TestForLexPattern(t *testing.T) {
	const SCRIPT = `
	var o = {a: 1};
	var trace = "";
	for (const [key, value] of Object.entries(o)) {
		trace += key+":"+value;
	}
	trace;
	`
	testScript(SCRIPT, asciiString("a:1"), t)
}

func TestBindingPatternRestTrailingComma(t *testing.T) {
	const SCRIPT = `
	const [a, b, ...rest,] = [];
	`
	_, err := Compile("", SCRIPT, false)
	if err == nil {
		t.Fatal("Expected an error")
	}
}

func TestAssignPatternRestTrailingComma(t *testing.T) {
	const SCRIPT = `
	([a, b, ...rest,] = []);
	`
	_, err := Compile("", SCRIPT, false)
	if err == nil {
		t.Fatal("Expected an error")
	}
}

func TestFuncParamInitializerSimple(t *testing.T) {
	const SCRIPT = `
	function f(a = 1) {
		return a;
	}
	""+f()+f(2);
	`
	testScript(SCRIPT, asciiString("12"), t)
}

func TestFuncParamObjectPatternSimple(t *testing.T) {
	const SCRIPT = `
	function f({a, b} = {a: 1, b: 2}) {
		return "" + a + b;
	}
	""+f()+" "+f({a: 3, b: 4});
	`
	testScript(SCRIPT, asciiString("12 34"), t)
}

func TestFuncParamRestStackSimple(t *testing.T) {
	const SCRIPT = `
	function f(arg1, ...rest) {
		return rest;
	}
	let ar = f(1, 2, 3);
	ar.join(",");
	`
	testScript(SCRIPT, asciiString("2,3"), t)
}

func TestFuncParamRestStashSimple(t *testing.T) {
	const SCRIPT = `
	function f(arg1, ...rest) {
		eval("true");
		return rest;
	}
	let ar = f(1, 2, 3);
	ar.join(",");
	`
	testScript(SCRIPT, asciiString("2,3"), t)
}

func TestRestArgsNotInStash(t *testing.T) {
	const SCRIPT = `
	function f(...rest) {
		() => rest;
		return rest.length;
	}
	f(1,2);
	`
	testScript(SCRIPT, valueInt(2), t)
}

func TestRestArgsInStash(t *testing.T) {
	const SCRIPT = `
	function f(first, ...rest) {
		() => first;
		() => rest;
		return rest.length;
	}
	f(1,2);
	`
	testScript(SCRIPT, valueInt(1), t)
}

func TestRestArgsInStashFwdRef(t *testing.T) {
	const SCRIPT = `
	function f(first = eval(), ...rest) {
		() => first;
		() => rest;
		return rest.length === 1 && rest[0] === 2;
	}
	f(1,2);
	`
	testScript(SCRIPT, valueTrue, t)
}

func TestFuncParamRestPattern(t *testing.T) {
	const SCRIPT = `
	function f(arg1, ...{0: rest1, 1: rest2}) {
		return ""+arg1+" "+rest1+" "+rest2;
	}
	f(1, 2, 3);
	`
	testScript(SCRIPT, asciiString("1 2 3"), t)
}

func TestFuncParamForwardRef(t *testing.T) {
	const SCRIPT = `
	function f(a = b + 1, b) {
		return ""+a+" "+b;
	}
	f(1, 2);
	`
	testScript(SCRIPT, asciiString("1 2"), t)
}

func TestFuncParamForwardRefMissing(t *testing.T) {
	const SCRIPT = `
	function f(a = b + 1, b) {
		return ""+a+" "+b;
	}
	assert.throws(ReferenceError, function() {
		f();
	});
	`
	testScriptWithTestLib(SCRIPT, _undefined, t)
}

func TestFuncParamInnerRef(t *testing.T) {
	const SCRIPT = `
	function f(a = inner) {
		var inner = 42;
		return a;
	}
	assert.throws(ReferenceError, function() {
		f();
	});
	`
	testScriptWithTestLib(SCRIPT, _undefined, t)
}

func TestFuncParamInnerRefEval(t *testing.T) {
	const SCRIPT = `
	function f(a = eval("inner")) {
		var inner = 42;
		return a;
	}
	assert.throws(ReferenceError, function() {
		f();
	});
	`
	testScriptWithTestLib(SCRIPT, _undefined, t)
}

func TestFuncParamCalleeName(t *testing.T) {
	const SCRIPT = `
	function f(a = f) {
		var f;
		return f;
	}
	typeof f();
	`
	testScript(SCRIPT, asciiString("undefined"), t)
}

func TestFuncParamVarCopy(t *testing.T) {
	const SCRIPT = `
	function f(a = f) {
		var a;
		return a;
	}
	typeof f();
	`
	testScript(SCRIPT, asciiString("function"), t)
}

func TestFuncParamScope(t *testing.T) {
	const SCRIPT = `
	var x = 'outside';
	var probe1, probe2;
	
	function f(
		_ = probe1 = function() { return x; },
		__ = (eval('var x = "inside";'), probe2 = function() { return x; })
	) {
	}
	f();
	probe1()+" "+probe2();
	`
	testScript(SCRIPT, asciiString("inside inside"), t)
}

func TestDefParamsStackPtr(t *testing.T) {
	const SCRIPT = `
	function A() {};
	A.B = function () {};
	function D(message = '') {
	  var C = A.B;
	  C([1,2,3]);
	};
	
	D();
	`
	testScript(SCRIPT, _undefined, t)
}

func TestNestedVariadicCalls(t *testing.T) {
	const SCRIPT = `
	function f() {
		return Array.prototype.join.call(arguments, ",");
	}
	f(...[1], "a", f(...[2]));
	`
	testScript(SCRIPT, asciiString("1,a,2"), t)
}

func TestVariadicNew(t *testing.T) {
	const SCRIPT = `
	function C() {
		this.res = Array.prototype.join.call(arguments, ",");
	}
	var c = new C(...[1], "a", new C(...[2]).res);
	c.res;
	`
	testScript(SCRIPT, asciiString("1,a,2"), t)
}

func TestVariadicUseStackVars(t *testing.T) {
	const SCRIPT = `
	function A(message) { return message; }
	function B(...args){
			return A(...args);
	}
	B("C");
	`
	testScript(SCRIPT, asciiString("C"), t)
}

func TestCatchParamPattern(t *testing.T) {
	const SCRIPT = `
	function f() {
		let x = 3;
		try {
			throw {a: 1, b: 2};
		} catch ({a, b, c = x}) {
			let x = 99;
			return ""+a+" "+b+" "+c;
		}
	}
	f();
	`
	testScript(SCRIPT, asciiString("1 2 3"), t)
}

func TestArrowUseStrict(t *testing.T) {
	// simple parameter list -- ok
	_, err := Compile("", "(a) => {'use strict';}", false)
	if err != nil {
		t.Fatal(err)
	}
	// non-simple parameter list -- syntax error
	_, err = Compile("", "(a=0) => {'use strict';}", false)
	if err == nil {
		t.Fatal("expected error")
	}
}

func TestArrowBoxedThis(t *testing.T) {
	const SCRIPT = `
	var context;
	fn = function() {
		return (arg) => { var local; context = this; };
	};
	
	fn()();
	context === this;
	`

	testScript(SCRIPT, valueTrue, t)
}

func TestParameterOverride(t *testing.T) {
	const SCRIPT = `
	function f(arg) {
		var arg = arg || "default"
		return arg
	}
	f()
	`
	testScript(SCRIPT, asciiString("default"), t)
}

func TestEvalInIterScope(t *testing.T) {
	const SCRIPT = `
	for (let a = 0; a < 1; a++) {
		eval("a");
	}
	`

	testScript(SCRIPT, valueInt(0), t)
}

func TestTemplateLiterals(t *testing.T) {
	vm := New()
	_, err := vm.RunString("const a = 1, b = 'b';")
	if err != nil {
		t.Fatal(err)
	}
	f := func(t *testing.T, template, expected string) {
		res, err := vm.RunString(template)
		if err != nil {
			t.Fatal(err)
		}
		if actual := res.Export(); actual != expected {
			t.Fatalf("Expected: %q, actual: %q", expected, actual)
		}
	}
	t.Run("empty", func(t *testing.T) {
		f(t, "``", "")
	})
	t.Run("noSub", func(t *testing.T) {
		f(t, "`test`", "test")
	})
	t.Run("emptyTail", func(t *testing.T) {
		f(t, "`a=${a},b=${b}`", "a=1,b=b")
	})
	t.Run("emptyHead", func(t *testing.T) {
		f(t, "`${a},b=${b}$`", "1,b=b$")
	})
	t.Run("headAndTail", func(t *testing.T) {
		f(t, "`a=${a},b=${b}$`", "a=1,b=b$")
	})
}

func TestTaggedTemplate(t *testing.T) {
	const SCRIPT = `
		let res;
		const o = {
			tmpl() {
				res = this;
				return () => {};
			}
		}
		` +
		"o.tmpl()`test`;" + `
		res === o;
		`

	testScript(SCRIPT, valueTrue, t)
}

func TestDuplicateGlobalFunc(t *testing.T) {
	const SCRIPT = `
	function a(){}
	function b(){ return "b" }
	function c(){ return "c" }
	function a(){}
	b();
	`

	testScript(SCRIPT, asciiString("b"), t)
}

func TestDuplicateFunc(t *testing.T) {
	const SCRIPT = `
	function f() {
		function a(){}
		function b(){ return "b" }
		function c(){ return "c" }
		function a(){}
		return b();
	}
	f();
	`

	testScript(SCRIPT, asciiString("b"), t)
}

func TestSrcLocations(t *testing.T) {
	// Do not reformat, assertions depend on the line and column numbers
	const SCRIPT = `
	let i = {
		valueOf() {
			throw new Error();
		}
	};
	try {
		i++;
	} catch(e) {
		assertStack(e, [["test.js", "valueOf", 4, 10],
						["test.js", "", 8, 3]
						]);
	}

	Object.defineProperty(globalThis, "x", {
		get() {
			throw new Error();
		},
		set() {
			throw new Error();
		}
	});

	try {
		x;
	} catch(e) {
		assertStack(e, [["test.js", "get", 17, 10],
						["test.js", "", 25, 3]
						]);
	}

	try {
		x++;
	} catch(e) {
		assertStack(e, [["test.js", "get", 17, 10],
						["test.js", "", 33, 3]
						]);
	}

	try {
		x = 2;
	} catch(e) {
		assertStack(e, [["test.js", "set", 20, 10],
						["test.js", "", 41, 3]
						]);
	}

	try {
		+i;
	} catch(e) {
		assertStack(e, [["test.js", "valueOf", 4, 10],
						["test.js", "", 49, 4]
						]);
	}

	try {
		let n;
		n.field = {
	        "key1": "test",
	        "key2": {},
		}
	} catch(e) {
		assertStack(e, [["test.js", "", 58, 5]
						]);
	}
	`
	testScriptWithTestLibX(SCRIPT, _undefined, t)
}

func TestSrcLocationThrowLiteral(t *testing.T) {
	vm := New()
	_, err := vm.RunString(`
	const z = 1;
	throw "";
	`)
	if ex, ok := err.(*Exception); ok {
		pos := ex.stack[0].Position()
		if pos.Line != 3 {
			t.Fatal(pos)
		}
	} else {
		t.Fatal(err)
	}
}

func TestSrcLocation(t *testing.T) {
	prg := MustCompile("test.js", `
f();
var x = 1;
let y = 1;
let [z1, z2] = [0, 0];

var [z3, z4] = [0, 0];
	`, false)
	const (
		varLine     = 3
		letLine     = 4
		dstrLetLine = 5
		dstrVarLine = 7
	)
	linesOfInterest := map[int]string{
		varLine:     "var",
		letLine:     "let",
		dstrLetLine: "destruct let",
		dstrVarLine: "destruct var",
	}
	for i := range prg.code {
		loc := prg.src.Position(prg.sourceOffset(i))
		delete(linesOfInterest, loc.Line)
		if len(linesOfInterest) == 0 {
			break
		}
	}
	for _, v := range linesOfInterest {
		t.Fatalf("no %s line", v)
	}
}

func TestBadObjectKey(t *testing.T) {
	_, err := Compile("", "({!:0})", false)
	if err == nil {
		t.Fatal("expected error")
	}
}

func TestConstantFolding(t *testing.T) {
	testValues := func(prg *Program, result Value, t *testing.T) {
		values := make(map[unistring.String]struct{})
		for _, ins := range prg.code {
			if lv, ok := ins.(loadVal); ok {
				values[lv.v.string()] = struct{}{}
			}
		}
		if len(values) != 1 {
			prg.dumpCode(t.Logf)
			t.Fatalf("values: %v", values)
		}
	}
	f := func(src string, result Value, t *testing.T) {
		prg := MustCompile("test.js", src, false)
		testValues(prg, result, t)
		New().testPrg(prg, result, t)
	}
	ff := func(src string, result Value, t *testing.T) {
		prg := MustCompile("test.js", src, false)
		fl := prg.code[0].(*newFunc)
		testValues(fl.prg, result, t)
		New().testPrg(prg, result, t)
	}

	t.Run("lexical binding", func(t *testing.T) {
		f("const x = 1 + 2; x", valueInt(3), t)
	})
	t.Run("var binding", func(t *testing.T) {
		f("var x = 1 + 2; x", valueInt(3), t)
	})
	t.Run("assignment", func(t *testing.T) {
		f("x = 1 + 2; x", valueInt(3), t)
	})
	t.Run("object pattern", func(t *testing.T) {
		f("const {x = 1 + 2} = {}; x", valueInt(3), t)
	})
	t.Run("array pattern", func(t *testing.T) {
		f("const [x = 1 + 2] = []; x", valueInt(3), t)
	})
	t.Run("object literal", func(t *testing.T) {
		f("var o = {x: 1 + 2}; o.x", valueInt(3), t)
	})
	t.Run("array literal", func(t *testing.T) {
		f("var a = [3, 3, 3, 1 + 2]; a[3]", valueInt(3), t)
	})
	t.Run("default function parameter", func(t *testing.T) {
		ff("function f(arg = 1 + 2) {return arg}; f()", valueInt(3), t)
	})
	t.Run("return", func(t *testing.T) {
		ff("function f() {return 1 + 2}; f()", valueInt(3), t)
	})
}

func TestStringInterning(t *testing.T) {
	const SCRIPT = `
	const str1 = "Test";
	function f() {
		return "Test";
	}
	[str1, f()];
	`
	vm := New()
	res, err := vm.RunString(SCRIPT)
	if err != nil {
		t.Fatal(err)
	}
	str1 := res.(*Object).Get("0").String()
	str2 := res.(*Object).Get("1").String()
	if unsafe.StringData(str1) != unsafe.StringData(str2) {
		t.Fatal("not interned")
	}
}

func TestAssignBeforeInit(t *testing.T) {
	const SCRIPT = `
	assert.throws(ReferenceError, () => {
		a = 1;
		let a;
	});

	assert.throws(ReferenceError, () => {
	    ({a, b} = {a: 1, b: 2});
	    let a, b;
	});

	assert.throws(ReferenceError, () => {
		(function() {
			eval("");
			({a} = {a: 1});
		})();
		let a;
	});

	assert.throws(ReferenceError, () => {
	    const ctx = {x: 1};
	    function t() {
	        delete ctx.x;
	        return 42;
	    }
	    with(ctx) {
	        (function() {
	            'use strict';
	            ({x} = {x: t()});
	        })();
	    }
	    return ctx.x;
	});

	assert.throws(ReferenceError, () => {
	    const ctx = {x: 1};
	    function t() {
	        delete ctx.x;
	        return 42;
	    }
	    with(ctx) {
	        (function() {
	            'use strict';
				const src = {};
				Object.defineProperty(src, "x", {
					get() {
						return t();
					}
				});
	            ({x} = src);
	        })();
	    }
	    return ctx.x;
	});
	`
	testScriptWithTestLib(SCRIPT, _undefined, t)
}

func TestOptChainCallee(t *testing.T) {
	const SCRIPT = `
	var a;
	assert.sameValue(a?.(true), undefined);
	a = null;
	assert.sameValue(a?.(), undefined);
	var o = {n: null};
	assert.sameValue(o.m?.(true), undefined);
	assert.sameValue(o.n?.(true), undefined);
	`
	testScriptWithTestLib(SCRIPT, _undefined, t)
}

func TestObjectLiteralSuper(t *testing.T) {
	const SCRIPT = `
	const proto = {
		m() {
			return 40;
		}
	}
	const o = {
		m() {
			return super.m() + 2;
		}
	}
	o.__proto__ = proto;
	o.m();
	`
	testScript(SCRIPT, intToValue(42), t)
}

func TestClassCaptureThisInFieldInit(t *testing.T) {
	const SCRIPT = `
	let capture;

	class C {
		a = () => this
	}

	let c = new C();
	c.a() === c;
	`
	testScript(SCRIPT, valueTrue, t)
}

func TestClassUseThisInFieldInit(t *testing.T) {
	const SCRIPT = `
	let capture;

	class C {
		a = this
	}

	let c = new C();
	c.a === c;
	`
	testScript(SCRIPT, valueTrue, t)
}

func TestClassCaptureThisInStaticFieldInit(t *testing.T) {
	const SCRIPT = `
	let capture;

	class C {
		static a = (capture = () => this, 0)
	}

	let c = new C();
	capture() === C;
	`
	testScript(SCRIPT, valueTrue, t)
}

func TestClassDynCaptureThisInStaticFieldInit(t *testing.T) {
	const SCRIPT = `
	class C {
		static a = eval("this")
	}

	C.a === C;
	`
	testScript(SCRIPT, valueTrue, t)
}

func TestCompileClass(t *testing.T) {
	const SCRIPT = `
	class C extends Error {
		a = true;
		b = 1;
		["b".toUpperCase()] = 2
		static A = Math.random() < 1
		constructor(message = "My Error") {
			super(message);
		}
		static M() {
		}
		static M1() {
		}
		m() {
			//return C.a;
		}
		m1() {
			return true;
		}
		static {
			this.supername = super.name;
		}
	}
	let c = new C();
	c.a === true && c.b === 1 && c.B === 2 && c.m1() && C.A && C.supername === "Error";
	`
	testScript(SCRIPT, valueTrue, t)
}

func TestSuperInEval(t *testing.T) {
	const SCRIPT = `
	class C extends Error {
		constructor() {
			eval("super()");
		}
		m() {
			return eval("super.name");
		}
	}
	let c = new C();
	c.m() === "Error";
	`
	testScript(SCRIPT, valueTrue, t)
}

func TestSuperRefDot(t *testing.T) {
	const SCRIPT = `
	let thisGet, thisSet;
	class P {
		_p = 0
	    get p() {
			thisGet = this;
	        return this._p;
	    }
		set p(v) {
			thisSet = this;
			this._p = v;
		}
	}

	class C extends P {
	    g() {
	        return super.p;
	    }
		s(v) {
			super.p = v;
		}

		inc() {
			super.p++;
		}
		incR() {
			return super.p++;
		}

		inc1() {
			++super.p;
		}

		inc1R() {
			return ++super.p;
		}
		unary() {
			return +super.p;
		}
		pattern() {
			[super.p] = [9];
		}
	}

	let o = new C();
	assert.sameValue(o.g(), 0, "get value");
	assert.sameValue(thisGet, o, "get this");
	o.s(1);
	assert.sameValue(o._p, 1, "set value");
	assert.sameValue(thisSet, o, "set this");

	thisGet = undefined;
	thisSet = undefined;
	o.inc();
	assert.sameValue(o._p, 2, "inc value");
	assert.sameValue(thisGet, o, "inc thisGet");
	assert.sameValue(thisSet, o, "inc thisSet");

	thisGet = undefined;
	thisSet = undefined;
	assert.sameValue(o.incR(), 2, "incR result");
	assert.sameValue(o._p, 3, "incR value");
	assert.sameValue(thisGet, o, "incR thisGet");
	assert.sameValue(thisSet, o, "incR thisSet");

	thisGet = undefined;
	thisSet = undefined;
	o.inc1();
	assert.sameValue(o._p, 4, "inc1 value");
	assert.sameValue(thisGet, o, "inc1 thisGet");
	assert.sameValue(thisSet, o, "inc1 thisSet");

	thisGet = undefined;
	thisSet = undefined;
	assert.sameValue(o.inc1R(), 5, "inc1R result");
	assert.sameValue(o._p, 5, "inc1R value");
	assert.sameValue(thisGet, o, "inc1R thisGet");
	assert.sameValue(thisSet, o, "inc1R thisSet");

	assert.sameValue(o.unary(), 5, "unary");

	o.pattern();
	assert.sameValue(o._p, 9, "pattern");
	`
	testScriptWithTestLib(SCRIPT, _undefined, t)
}

func TestPrivateRefDot(t *testing.T) {
	const SCRIPT = `
	class C {
		#p = 0;
	    g() {
	        return this.#p;
	    }
		s(v) {
			this.#p = v;
		}

		inc() {
			this.#p++;
		}
		incR() {
			return this.#p++;
		}

		inc1() {
			++this.#p;
		}

		inc1R() {
			return ++this.#p;
		}
		pattern() {
			[this.#p] = [9];
		}
	}

	let o = new C();
	assert.sameValue(o.g(), 0, "get value");
	o.s(1);
	assert.sameValue(o.g(), 1, "set value");

	o.inc();
	assert.sameValue(o.g(), 2, "inc value");

	assert.sameValue(o.incR(), 2, "incR result");
	assert.sameValue(o.g(), 3, "incR value");

	o.inc1();
	assert.sameValue(o.g(), 4, "inc1 value");

	assert.sameValue(o.inc1R(), 5, "inc1R result");
	assert.sameValue(o.g(), 5, "inc1R value");

	o.pattern();
	assert.sameValue(o.g(), 9, "pattern");
	`
	testScriptWithTestLib(SCRIPT, _undefined, t)
}

func TestPrivateRefDotEval(t *testing.T) {
	const SCRIPT = `
	class C {
		#p = 0;
	    g() {
	        return eval("this.#p");
	    }
		s(v) {
			eval("this.#p = v");
		}

		incR() {
			return eval("this.#p++");
		}

		inc1R() {
			return eval("++this.#p");
		}

		pattern() {
			eval("[this.#p] = [9]");
		}
	}

	let o = new C();
	assert.sameValue(o.g(), 0, "get value");
	o.s(1);
	assert.sameValue(o.g(), 1, "set value");

	assert.sameValue(o.incR(), 1, "incR result");
	assert.sameValue(o.g(), 2, "incR value");

	assert.sameValue(o.inc1R(), 3, "inc1R result");
	assert.sameValue(o.g(), 3, "inc1R value");

	o.pattern();
	assert.sameValue(o.g(), 9, "pattern");
	`
	testScriptWithTestLib(SCRIPT, _undefined, t)
}

func TestSuperRefDotCallee(t *testing.T) {
	const SCRIPT = `
	class P {
	    get p() {
	        return function() {
	            return this;
	        };
	    }
	}

	class C extends P {
	    m() {
	        return super.p();
	    }
	}

	let o = new C();
	o.m() === o;
	`
	testScript(SCRIPT, valueTrue, t)
}

func TestSuperRefBracket(t *testing.T) {
	const SCRIPT = `
	let PROP = "p";
	let thisGet, thisSet;
	class P {
		_p = 0
	    get p() {
			thisGet = this;
	        return this._p;
	    }
		set p(v) {
			thisSet = this;
			this._p = v;
		}
	}

	class C extends P {
	    g() {
	        return super[PROP];
	    }
		s(v) {
			super[PROP] = v;
		}

		inc() {
			super[PROP]++;
		}
		incR() {
			return super[PROP]++;
		}

		inc1() {
			++super[PROP];
		}

		inc1R() {
			return ++super[PROP];
		}
		pattern() {
			[super[PROP]] = [9];
		}
	}

	let o = new C();
	assert.sameValue(o.g(), 0, "get value");
	assert.sameValue(thisGet, o, "get this");
	o.s(1);
	assert.sameValue(o._p, 1, "set value");
	assert.sameValue(thisSet, o, "set this");

	thisGet = undefined;
	thisSet = undefined;
	o.inc();
	assert.sameValue(o._p, 2, "inc value");
	assert.sameValue(thisGet, o, "inc thisGet");
	assert.sameValue(thisSet, o, "inc thisSet");

	thisGet = undefined;
	thisSet = undefined;
	assert.sameValue(o.incR(), 2, "incR result");
	assert.sameValue(o._p, 3, "incR value");
	assert.sameValue(thisGet, o, "incR thisGet");
	assert.sameValue(thisSet, o, "incR thisSet");

	thisGet = undefined;
	thisSet = undefined;
	o.inc1();
	assert.sameValue(o._p, 4, "inc1 value");
	assert.sameValue(thisGet, o, "inc1 thisGet");
	assert.sameValue(thisSet, o, "inc1 thisSet");

	thisGet = undefined;
	thisSet = undefined;
	assert.sameValue(o.inc1R(), 5, "inc1R result");
	assert.sameValue(o._p, 5, "inc1R value");
	assert.sameValue(thisGet, o, "inc1R thisGet");
	assert.sameValue(thisSet, o, "inc1R thisSet");

	o.pattern();
	assert.sameValue(o._p, 9, "pattern");
	`
	testScriptWithTestLib(SCRIPT, _undefined, t)
}

func TestSuperRefBracketEvalOrder(t *testing.T) {
	const SCRIPT = `
	let keyCallCount = 0;

	function key() {
		keyCallCount++;
		C.prototype.__proto__ = null;
	    return "k";
	}

	class C {
	    constructor() {
	        super[key()]++;
	    }
	}

	assert.throws(TypeError, () => new C());
	assert.sameValue(keyCallCount, 1);
	`
	testScriptWithTestLib(SCRIPT, _undefined, t)
}

func TestSuperRefBracketCallee(t *testing.T) {
	const SCRIPT = `
	let PROP = "p";
	class P {
	    get p() {
	        return function() {
	            return this;
	        };
	    }
	}

	class C extends P {
	    m() {
	        return super[PROP]();
	    }
	}

	let o = new C();
	o.m() === o;
	`
	testScript(SCRIPT, valueTrue, t)
}

func TestSuperBaseInCtor(t *testing.T) {
	const SCRIPT = `
	let result;
	class Derived extends Object {
	    constructor() {
	        super();
	        result = super.constructor === Object;
	    }
	}
	new Derived();
	result;
	`
	testScript(SCRIPT, valueTrue, t)
}

func TestClassNamedEval(t *testing.T) {
	const SCRIPT = `
	const C = class {
	}

	C.name === "C";
	`
	testScript(SCRIPT, valueTrue, t)
}

func TestClassNonDerived(t *testing.T) {
	const SCRIPT = `
	function initF() {
	}
	class C {
	    f = initF()
	}
	let c = new C();
	`

	testScript(SCRIPT, _undefined, t)
}

func TestClassExpr(t *testing.T) {
	const SCRIPT = `
	typeof Object.getOwnPropertyDescriptor(class {get f() {}}.prototype, "f").get === "function";
	`

	testScript(SCRIPT, valueTrue, t)
}

func TestClassSuperInHeritage(t *testing.T) {
	const SCRIPT = `
	class P {
	    a() {
	        return Error;
	    }
	}

	class C extends P {
	    m() {
	        class Inner extends super.a() {
	        }
	        return new Inner();
	    }
	}

	new C().m() instanceof Error;
	`

	testScript(SCRIPT, valueTrue, t)
}

func TestClassSuperInHeritageExpr(t *testing.T) {
	const SCRIPT = `
	class P {
	    a() {
	        return Error;
	    }
	}

	class C extends P {
	    m() {
			function f(cls) {
				return new cls();
			}
	        return f(class Inner extends super.a() {
	        })
	    }
	}

	new C().m() instanceof Error;
	`

	testScript(SCRIPT, valueTrue, t)
}

func TestClassReferToBinding(t *testing.T) {
	const SCRIPT = `
	const CC = class C {
		static T = 40
	    f = C.T + 2
	}
	let c = new CC();
	c.f;
	`

	testScript(SCRIPT, intToValue(42), t)
}

func TestClassReferToBindingInStaticDecl(t *testing.T) {
	const SCRIPT = `
	class C {
		static T = C.name
	}
	C.T;
	`

	testScript(SCRIPT, asciiString("C"), t)
}

func TestClassReferToBindingInStaticEval(t *testing.T) {
	const SCRIPT = `
	const CC = class C {
		static T = eval("C.name")
	}
	CC.T;
	`

	testScript(SCRIPT, asciiString("C"), t)
}

func TestClassReferToBindingFromHeritage(t *testing.T) {
	const SCRIPT = `
	assert.throws(ReferenceError, () => {
		class C extends C {
		}
	});
	`

	testScriptWithTestLib(SCRIPT, _undefined, t)
}

func TestClassCaptureSuperCallInArrowFunc(t *testing.T) {
	const SCRIPT = `
	let f;
	class C extends class {} {
		constructor() {
			f = () => super();
			f();
		}
	}
	let c = new C();
	`

	testScript(SCRIPT, _undefined, t)
}

func TestClassCaptureSuperCallInNestedArrowFunc(t *testing.T) {
	const SCRIPT = `
	let f;
	class P {
	}
	class C extends P {
		constructor() {
			f = () => () => super();
			f()();
		}
	}
	new C() instanceof P;
	`

	testScript(SCRIPT, valueTrue, t)
}

func TestThisInEval(t *testing.T) {
	const SCRIPT = `
	assert.sameValue(eval("this"), this, "global");

	let o = {
		f() {
			return eval("this");
		}
	}
	assert.sameValue(o.f(), o, "obj literal");
	`

	testScriptWithTestLib(SCRIPT, _undefined, t)
}

func TestStaticAsBindingTarget(t *testing.T) {
	const SCRIPT = `
	let [static] = [];
	`
	testScript(SCRIPT, _undefined, t)
}

func TestEvalInStaticFieldInit(t *testing.T) {
	const SCRIPT = `
	var C = class {
		static f = 'test';
		static g = this.f + '262';
		static h = eval('this.g') + 'test';
	}
	C.f === "test" && C.g === "test262" && C.h === "test262test";
	`
	testScript(SCRIPT, valueTrue, t)
}

func TestClassPrivateElemInEval(t *testing.T) {
	const SCRIPT = `
	let f1, f2;

	class C extends (f1 = o => eval("o.#a"), Object) {
	    static #a = 42;
	    static {
	        f2 = o => eval("o.#a");
			assert.sameValue(C.#a, 42);
			assert.sameValue((() => C.#a)(), 42);
	    }
	}

	assert.throws(SyntaxError, () => f1(C));
	assert.sameValue(f2(C), 42);
	`
	testScriptWithTestLib(SCRIPT, _undefined, t)
}

func TestClassPrivateElemInIndirectEval(t *testing.T) {
	const SCRIPT = `
	let f1, f2;

	class C extends (f1 = o => (0, eval)("o.#a"), Object) {
	    static #a = 42;
	    static {
	        f2 = o => (0, eval)("o.#a");
			assert.throws(SyntaxError, () => (0, eval)("C.#a"));
	    }
	}

	assert.throws(SyntaxError, () => f1(C));
	assert.throws(SyntaxError, () => f2(C));
	`
	testScriptWithTestLib(SCRIPT, _undefined, t)
}

func TestClassPrivateElemInFunction(t *testing.T) {
	const SCRIPT = `
	assert.throws(SyntaxError, () => {
		class C {
		    static #a = 42;
		    static {
		        Function("o", "return o.#a");
		    }
		}
	});
	`
	testScriptWithTestLib(SCRIPT, _undefined, t)
}

func TestClassPrivateElementsDecl(t *testing.T) {
	const SCRIPT = `
	class C {
		#a = 42;
		get #b() {}
		set #b(_) {}
		get c() {
			return this.#a;
		}
		#m() {
			return this.#a;
		}
		static getter(inst) {
			return inst.#m();
		}
	}
	let c = new C();
	c.c + C.getter(c);
	`
	testScript(SCRIPT, intToValue(84), t)
}

func TestPrivateIn(t *testing.T) {
	const SCRIPT = `
	class C {
		#a = 42;
		static check(inst) {
			return #a in inst;
		}
	}
	let c = new C();
	C.check(c);
	`
	testScript(SCRIPT, valueTrue, t)
}

func TestDeletePropOfNonObject(t *testing.T) {
	const SCRIPT = `
	delete 'Test262'[100] && delete 'Test262'.a && delete 'Test262'['@'];
	`
	testScript(SCRIPT, valueTrue, t)
}

func TestKeywordsAsLabels(t *testing.T) {
	const SCRIPT = `
	let: for (let i = 0; i < 2; i++) {
		if (i === 0) continue let;
		break let;
	}

	\u006Cet: for (let i = 0; i < 2; i++) {
		if (i === 0) continue \u006Cet;
		break \u006Cet;
	}

	yield: for (let i = 0; i < 2; i++) {
		if (i === 0) continue yield;
		break yield;
	}

	yi\u0065ld: for (let i = 0; i < 2; i++) {
		if (i === 0) continue yi\u0065ld;
		break yi\u0065ld;
	}
`
	testScript(SCRIPT, _undefined, t)
}

func TestThisResolutionWithArg(t *testing.T) {
	const SCRIPT = `
	let capture;
	function f(arg) {
		capture = () => this; // move 'this' to stash
		return [this, arg];
	}
	const _this = {};
	const arg = {};
	const [_this1, arg1] = f.call(_this, arg);
	_this1 === _this && arg1 === arg;
	`
	testScript(SCRIPT, valueTrue, t)
}

func TestThisResolutionArgInStash(t *testing.T) {
	const SCRIPT = `
	let capture;
	function f(arg) {
		capture = () => this + arg; // move 'this' and arguments to stash
		return [this, arg];
	}
	const _this = {};
	const arg = {};
	const [_this1, arg1] = f.call(_this, arg);
	_this1 === _this && arg1 === arg;
	`
	testScript(SCRIPT, valueTrue, t)
}

func TestThisResolutionWithStackVar(t *testing.T) {
	const SCRIPT = `
	let capture;
	function f(arg) {
		const _ = 1; // a stack variable
		capture = () => this + arg; // move 'this' and arguments to stash
		return [this, arg];
	}
	const _this = {};
	const arg = {};
	const [_this1, arg1] = f.call(_this, arg);
	_this1 === _this && arg1 === arg;
	`
	testScript(SCRIPT, valueTrue, t)
}

func TestForInLoopContinue(t *testing.T) {
	const SCRIPT = `
	var globalSink;
	(function() {
	    const data = [{disabled: true}, {}];
		function dummy() {}
	    function f1() {}

	    function f() {
			dummy(); // move dummy to stash (so that f1 is at index 1)
	        for (const d of data) {
	            if (d.disabled) continue;
	            globalSink = () => d; // move d to stash
	            f1();
	        }
	    }

	    f();
	})();
	`
	testScript(SCRIPT, _undefined, t)
}

func TestForInLoopContinueOuter(t *testing.T) {
	const SCRIPT = `
	var globalSink;
	(function() {
	    const data = [{disabled: true}, {}];
		function dummy1() {}
	    function f1() {}

	    function f() {
			dummy1();
			let counter = 0;
			OUTER: for (let i = 0; i < 1; i++) {
		        for (const d of data) {
		            if (d.disabled) continue OUTER;
		            globalSink = () => d;
		        }
				counter++;
			}
			f1();
			if (counter !== 0) {
				throw new Error(counter);
			}
	    }

	    f();
	})();
	`
	testScript(SCRIPT, _undefined, t)
}

func TestLexicalDeclInSwitch(t *testing.T) {
	const SCRIPT = `
	switch(0) {
	    case 1:
	        if (false) b = 3;
	    case 2:
	        const c = 1;
	}
	`
	testScript(SCRIPT, _undefined, t)
}

func TestClassFieldSpecial(t *testing.T) {
	const SCRIPT = `
	class C {
		get;
		set;
		async;
		static;
	}
	`
	testScript(SCRIPT, _undefined, t)
}

func TestClassMethodSpecial(t *testing.T) {
	const SCRIPT = `
	class C {
		get() {}
		set() {}
		async() {}
		static() {}
	}
	`
	testScript(SCRIPT, _undefined, t)
}

func TestClassMethodNumLiteral(t *testing.T) {
	const SCRIPT = `
	class C {
		0() {
			return true;
		}
	}
	new C()[0]();
	`
	testScript(SCRIPT, valueTrue, t)
}

func TestAsyncFunc(t *testing.T) {
	const SCRIPT = `
	async (x = true, y) => {};
	async x => {};
	let passed = false;
	async function f() {
		return true;
	}
	async function f1(arg = true) {
		passed = await f();
	}
	await f1();
	return passed;
	`
	testAsyncFunc(SCRIPT, valueTrue, t)
}

func TestObjectLiteralComputedMethodKeys(t *testing.T) {
	_, err := Compile("", `
		({
		    ["__proto__"]() {},
		    ["__proto__"]() {}
		})
	`, false)
	if err != nil {
		t.Fatal(err)
	}

	_, err = Compile("", `
		({
		    get ["__proto__"]() {},
		    get ["__proto__"]() {}
		})
	`, false)
	if err != nil {
		t.Fatal(err)
	}
}

func TestGeneratorFunc(t *testing.T) {
	const SCRIPT = `
	let trace = "";
	function defParam() {
		trace += "1";
		return "def";
	}
	function* g(param = defParam()) {
		const THREE = 3;
		trace += "2";
		assert.sameValue(Math.floor(yield 1), THREE);
		return 42;
	}
	let iter = g();
	assert.sameValue(trace, "1");

	let next = iter.next();
	assert.sameValue(next.value, 1);
	assert.sameValue(next.done, false);

	next = iter.next(Math.PI);
	assert.sameValue(next.value, 42);
	assert.sameValue(next.done, true);

	assert.sameValue(trace, "12");
	`
	testScriptWithTestLib(SCRIPT, _undefined, t)
}

func TestGeneratorMethods(t *testing.T) {
	const SCRIPT = `
	class C {
		*g(param) {
			yield 1;
			yield 2;
		}
	}
	let c = new C();
	let iter = c.g();
	let res = iter.next();
	assert.sameValue(res.value, 1);
	assert.sameValue(res.done, false);

	res = iter.next();
	assert.sameValue(res.value, 2);
	assert.sameValue(res.done, false);

	res = iter.next();
	assert.sameValue(res.value, undefined);
	assert.sameValue(res.done, true);
	`
	testScriptWithTestLib(SCRIPT, _undefined, t)
}

func TestFunctionBodyClassDecl(t *testing.T) {
	const SCRIPT = `
	function as(requiredArgument = {}) {
		class something { }
	};
	`
	_, err := Compile("", SCRIPT, false)
	if err != nil {
		t.Fatal(err)
	}
}

func TestNestedDestructArray(t *testing.T) {
	const SCRIPT = `
	var [
		[ h = 0 ] = [ 0 ]
	] = [];
	assert.sameValue(h, 0);

	var [
		[ h1 = 1 ] = []
	] = [];
	assert.sameValue(h1, 1);

	var [
		[ h2 = 1 ] = []
	] = [ [ 2 ] ];
	assert.sameValue(h2, 2);
	`
	testScriptWithTestLib(SCRIPT, _undefined, t)
}

func TestThisInStash(t *testing.T) {
	const SCRIPT = `
	function f() {
		globalThis.x = () => this; // move 'this' to stash
		
		{
            try {
                throw new Error("boo!");
            } catch (e) {
                if (e.message !== 'boo!') {
					throw new Error("unexpected exception value");
				}
            }
        }
	}

	function f1() {
		globalThis.x = () => this; // move 'this' to stash
		var v; // introduce a stack variable

		{
            try {
                throw new Error("boo!");
            } catch (e) {
                if (e.message !== 'boo!') {
					throw new Error("unexpected exception value");
				}
            }
        }
	}

	f();
	f1();
`
	testScript(SCRIPT, _undefined, t)
}

func TestThisInStashCtor(t *testing.T) {
	const SCRIPT = `
	class C extends Object {
		constructor() {
			super();
			globalThis.x = () => this; // move 'this' to stash
			{
				try {
					throw new Error("boo!");
				} catch (e) {
					if (e.message !== 'boo!') {
						throw new Error("unexpected exception value");
					}
				}
			}	
		}
	}

	class C1 extends Object {
		constructor() {
			super();
			globalThis.x = () => this; // move 'this' to stash
			var v; // introduce a stack variable
			{
				try {
					throw new Error("boo!");
				} catch (e) {
					if (e.message !== 'boo!') {
						throw new Error("unexpected exception value");
					}
				}
			}	
		}
	}

	new C();
	new C1();
	undefined;
`
	testScript(SCRIPT, _undefined, t)
}

func TestWrongThisRest(t *testing.T) {
	const SCRIPT = `
	class mine {
		constructor (arg) {
			this.field = arg
		}
		f(...argument) {
			if (this.field != "something") {
				throw "wrong " + this.field + " Object.keys:" + Object.keys(this)
			}
			let s = () => {
				for (const arg of argument) arg.call()
				return this.field
			}
		}
	}
	const a = new mine("something")
	a.f({"SomeKey": "here"})
`
	testScript(SCRIPT, _undefined, t)
}

func TestWrongThisExtraArg(t *testing.T) {
	const SCRIPT = `
	class mine {
		constructor (arg) {
			this.field = arg
		}
		f() {
			if (this.field != "something") {
				throw "wrong " + this.field + " Object.keys:" + Object.keys(this)
			}
			globalThis.x = () => this.field;
		}
	}
	const a = new mine("something")
	a.f({"SomeKey": "here"}) // pass extra arg
`
	testScript(SCRIPT, _undefined, t)
}

func TestWrongThisForwardRef(t *testing.T) {
	const SCRIPT = `
	const expectedThis = {};
	function f(a = b + 1, b) { // forward ref argument to make sure args remain on stack
		if (this != expectedThis) {
			throw new Error("unexpected 'this'");
		}
		eval("true"); // make the scope dynamic which copies args to stash
	}

	f.call(expectedThis, 1, 2);
`
	testScript(SCRIPT, _undefined, t)
}

/*
func TestBabel(t *testing.T) {
	src, err := os.ReadFile("babel7.js")
	if err != nil {
		t.Fatal(err)
	}
	vm := New()
	_, err = vm.RunString(string(src))
	if err != nil {
		t.Fatal(err)
	}
	_, err = vm.RunString(`var result = Babel.transform("", {presets: ["es2015"]});`)
	if err != nil {
		t.Fatal(err)
	}
}*/

func BenchmarkCompile(b *testing.B) {
	data, err := os.ReadFile("testdata/S15.10.2.12_A1_T1.js")
	if err != nil {
		b.Fatal(err)
	}

	src := string(data)

	for i := 0; i < b.N; i++ {
		_, err := Compile("test.js", src, false)
		if err != nil {
			b.Fatal(err)
		}
	}
}
