package goja

import (
	"errors"
	"fmt"
	"math"
	"math/big"
	"reflect"
	"runtime"
	"strconv"
	"strings"
	"testing"
	"time"

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

func TestGlobalObjectProto(t *testing.T) {
	const SCRIPT = `
	this instanceof Object
	`

	testScript(SCRIPT, valueTrue, t)
}

func TestUnicodeString(t *testing.T) {
	const SCRIPT = `
	var s = "Тест";
	s.length === 4 && s[1] === "е";

	`

	testScript(SCRIPT, valueTrue, t)
}

func Test2TierHierarchyProp(t *testing.T) {
	const SCRIPT = `
	var a = {};
	Object.defineProperty(a, "test", {
		value: 42,
		writable: false,
		enumerable: false,
		configurable: true
	});
	var b = Object.create(a);
	var c = Object.create(b);
	c.test = 43;
	c.test === 42 && !b.hasOwnProperty("test");

	`

	testScript(SCRIPT, valueTrue, t)
}

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

	var count = 0;

	for (var i in "1234") {
    		for (var j in "1234567") {
        		count++
    		}
	}

	count;
	`

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

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

	var s = "тест";
	var s1 = "test";
	var s2 = "абвгд";

	s.concat(s1) === "тестtest" && s.concat(s1, s2) === "тестtestабвгд" && s1.concat(s, s2) === "testтестабвгд"
		&& s.concat(s2) === "тестабвгд";

	`

	testScript(SCRIPT, valueTrue, t)
}

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

	"abc".indexOf("", 4)
	`

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

func TestUnicodeIndexOf(t *testing.T) {
	const SCRIPT = `
	"абвгд".indexOf("вг", 1) === 2 && '中国'.indexOf('国') === 1
	`

	testScript(SCRIPT, valueTrue, t)
}

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

	"abcabab".lastIndexOf("ab", 3)
	`

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

func TestUnicodeLastIndexOf(t *testing.T) {
	const SCRIPT = `
	"абвабаб".lastIndexOf("аб", 3)
	`

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

func TestUnicodeLastIndexOf1(t *testing.T) {
	const SCRIPT = `
	"abꞐcde".lastIndexOf("cd");
	`

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

func TestNumber(t *testing.T) {
	const SCRIPT = `
	(new Number(100111122133144155)).toString()
	`

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

func TestFractionalNumberToStringRadix(t *testing.T) {
	const SCRIPT = `
	(new Number(123.456)).toString(36)
	`

	testScript(SCRIPT, asciiString("3f.gez4w97ry"), t)
}

func TestNumberFormatRounding(t *testing.T) {
	const SCRIPT = `
	assert.sameValue((123.456).toExponential(undefined), "1.23456e+2", "undefined");
	assert.sameValue((0.000001).toPrecision(2), "0.0000010")
	assert.sameValue((-7).toPrecision(1), "-7");
	assert.sameValue((-42).toPrecision(1), "-4e+1");
	assert.sameValue((0.000001).toPrecision(1), "0.000001");
	assert.sameValue((123.456).toPrecision(1), "1e+2", "1");
	assert.sameValue((123.456).toPrecision(2), "1.2e+2", "2");

	var n = new Number("0.000000000000000000001"); // 1e-21
	assert.sameValue((n).toPrecision(1), "1e-21");
	assert.sameValue((25).toExponential(0), "3e+1");
	assert.sameValue((-25).toExponential(0), "-3e+1");
	assert.sameValue((12345).toExponential(3), "1.235e+4");
	assert.sameValue((25.5).toFixed(0), "26");
	assert.sameValue((-25.5).toFixed(0), "-26");
	assert.sameValue((99.9).toFixed(0), "100");
	assert.sameValue((99.99).toFixed(1), "100.0");
	`
	testScriptWithTestLib(SCRIPT, _undefined, t)
}

func TestBinOctalNumbers(t *testing.T) {
	const SCRIPT = `
	0b111;
	`

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

func TestSetFunc(t *testing.T) {
	const SCRIPT = `
	sum(40, 2);
	`
	r := New()
	err := r.Set("sum", func(call FunctionCall) Value {
		return r.ToValue(call.Argument(0).ToInteger() + call.Argument(1).ToInteger())
	})
	if err != nil {
		t.Fatal(err)
	}
	v, err := r.RunString(SCRIPT)
	if err != nil {
		t.Fatal(err)
	}
	if i := v.ToInteger(); i != 42 {
		t.Fatalf("Expected 42, got: %d", i)
	}
}

func ExampleRuntime_Set_lexical() {
	r := New()
	_, err := r.RunString("let x")
	if err != nil {
		panic(err)
	}
	err = r.Set("x", 1)
	if err != nil {
		panic(err)
	}
	fmt.Print(r.Get("x"), r.GlobalObject().Get("x"))
	// Output: 1 <nil>
}

func TestRecursiveRun(t *testing.T) {
	// Make sure that a recursive call to Run*() correctly sets the environment and no stash or stack
	// corruptions occur.
	vm := New()
	vm.Set("f", func() (Value, error) {
		return vm.RunString("let x = 1; { let z = 100, z1 = 200, z2 = 300, z3 = 400; x = x + z3} x;")
	})
	res, err := vm.RunString(`
	function f1() {
		let x = 2;
		eval('');
		{
			let y = 3;
			let res = f();
			if (x !== 2) { // check for stash corruption
				throw new Error("x="+x);
			}
			if (y !== 3) { // check for stack corruption
				throw new Error("y="+y);
			}
			return res;
		}
	};
	f1();
	`)
	if err != nil {
		t.Fatal(err)
	}
	if !res.SameAs(valueInt(401)) {
		t.Fatal(res)
	}
}

func TestRecursiveRunWithNArgs(t *testing.T) {
	vm := New()
	vm.Set("f", func() (Value, error) {
		return vm.RunString(`
		{
			let a = 0;
			let b = 1;
			a = 2; // this used to to corrupt b, because its location on the stack was off by vm.args (1 in our case)
			b;
		}
	`)
	})
	_, err := vm.RunString(`
		(function(arg) { // need an ES function call with an argument to set vm.args
			let res = f();
			if (res !== 1) {
				throw new Error(res);
			}
		})(123);
	`)
	if err != nil {
		t.Fatal(err)
	}
}

func TestRecursiveRunCallee(t *testing.T) {
	// Make sure that a recursive call to Run*() correctly sets the callee (i.e. stack[sb-1])
	vm := New()
	vm.Set("f", func() (Value, error) {
		return vm.RunString("this; (() => 1)()")
	})
	res, err := vm.RunString(`
		f(123, 123);
	`)
	if err != nil {
		t.Fatal(err)
	}
	if !res.SameAs(valueInt(1)) {
		t.Fatal(res)
	}
}

func TestObjectGetSet(t *testing.T) {
	const SCRIPT = `
		input.test++;
		input;
	`
	r := New()
	o := r.NewObject()
	o.Set("test", 42)
	r.Set("input", o)

	v, err := r.RunString(SCRIPT)
	if err != nil {
		t.Fatal(err)
	}
	if o1, ok := v.(*Object); ok {
		if v1 := o1.Get("test"); v1.Export() != int64(43) {
			t.Fatalf("Unexpected test value: %v (%T)", v1, v1.Export())
		}
	}
}

func TestThrowFromNativeFunc(t *testing.T) {
	const SCRIPT = `
	var thrown;
	try {
		f();
	} catch (e) {
		thrown = e;
	}
	thrown;
	`
	r := New()
	r.Set("f", func(call FunctionCall) Value {
		panic(r.ToValue("testError"))
	})

	v, err := r.RunString(SCRIPT)
	if err != nil {
		t.Fatal(err)
	}

	if !v.Equals(asciiString("testError")) {
		t.Fatalf("Unexpected result: %v", v)
	}
}

func TestSetGoFunc(t *testing.T) {
	const SCRIPT = `
	f(40, 2)
	`
	r := New()
	r.Set("f", func(a, b int) int {
		return a + b
	})

	v, err := r.RunString(SCRIPT)
	if err != nil {
		t.Fatal(err)
	}

	if v.ToInteger() != 42 {
		t.Fatalf("Unexpected result: %v", v)
	}
}

func TestSetFuncVariadic(t *testing.T) {
	vm := New()
	vm.Set("f", func(s string, g ...Value) {
		something := g[0].ToObject(vm).Get(s).ToInteger()
		if something != 5 {
			t.Fatal()
		}
	})
	_, err := vm.RunString(`
           f("something", {something: 5})
	`)
	if err != nil {
		t.Fatal(err)
	}
}

func TestSetFuncVariadicFuncArg(t *testing.T) {
	vm := New()
	vm.Set("f", func(s string, g ...Value) {
		if f, ok := AssertFunction(g[0]); ok {
			v, err := f(nil)
			if err != nil {
				t.Fatal(err)
			}
			if v != valueTrue {
				t.Fatal(v)
			}
		}
	})
	_, err := vm.RunString(`
           f("something", () => true)
	`)
	if err != nil {
		t.Fatal(err)
	}
}

func TestArgsKeys(t *testing.T) {
	const SCRIPT = `
	function testArgs2(x, y, z) {
    		// Properties of the arguments object are enumerable.
    		return Object.keys(arguments);
	}

	testArgs2(1,2).length
	`

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

func TestIPowOverflow(t *testing.T) {
	const SCRIPT = `
	assert.sameValue(Math.pow(65536, 6), 7.922816251426434e+28);
	assert.sameValue(Math.pow(10, 19), 1e19);
	assert.sameValue(Math.pow(2097151, 3), 9223358842721534000);
	assert.sameValue(Math.pow(2097152, 3), 9223372036854776000);
	assert.sameValue(Math.pow(-2097151, 3), -9223358842721534000);
	assert.sameValue(Math.pow(-2097152, 3), -9223372036854776000);
	assert.sameValue(Math.pow(9007199254740992, 0), 1);
	assert.sameValue(Math.pow(-9007199254740992, 0), 1);
	assert.sameValue(Math.pow(0, 0), 1);
	`

	testScriptWithTestLib(SCRIPT, _undefined, t)
}

func TestIPow(t *testing.T) {
	if res := ipow(-9223372036854775808, 1); res != -9223372036854775808 {
		t.Fatal(res)
	}

	if res := ipow(9223372036854775807, 1); res != 9223372036854775807 {
		t.Fatal(res)
	}

	if res := ipow(-9223372036854775807, 1); res != -9223372036854775807 {
		t.Fatal(res)
	}

	if res := ipow(9223372036854775807, 0); res != 1 {
		t.Fatal(res)
	}

	if res := ipow(-9223372036854775807, 0); res != 1 {
		t.Fatal(res)
	}

	if res := ipow(-9223372036854775808, 0); res != 1 {
		t.Fatal(res)
	}
}

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

	vm := New()
	time.AfterFunc(200*time.Millisecond, func() {
		vm.Interrupt("halt")
	})

	_, err := vm.RunString(SCRIPT)
	if err == nil {
		t.Fatal("Err is nil")
	}
}

func TestRuntime_ExportToNumbers(t *testing.T) {
	vm := New()
	t.Run("int8/no overflow", func(t *testing.T) {
		var i8 int8
		err := vm.ExportTo(vm.ToValue(-123), &i8)
		if err != nil {
			t.Fatal(err)
		}
		if i8 != -123 {
			t.Fatalf("i8: %d", i8)
		}
	})

	t.Run("int8/overflow", func(t *testing.T) {
		var i8 int8
		err := vm.ExportTo(vm.ToValue(333), &i8)
		if err != nil {
			t.Fatal(err)
		}
		if i8 != 77 {
			t.Fatalf("i8: %d", i8)
		}
	})

	t.Run("int64/uint64", func(t *testing.T) {
		var ui64 uint64
		err := vm.ExportTo(vm.ToValue(-1), &ui64)
		if err != nil {
			t.Fatal(err)
		}
		if ui64 != math.MaxUint64 {
			t.Fatalf("ui64: %d", ui64)
		}
	})

	t.Run("int8/float", func(t *testing.T) {
		var i8 int8
		err := vm.ExportTo(vm.ToValue(333.9234), &i8)
		if err != nil {
			t.Fatal(err)
		}
		if i8 != 77 {
			t.Fatalf("i8: %d", i8)
		}
	})

	t.Run("int8/object", func(t *testing.T) {
		var i8 int8
		err := vm.ExportTo(vm.NewObject(), &i8)
		if err != nil {
			t.Fatal(err)
		}
		if i8 != 0 {
			t.Fatalf("i8: %d", i8)
		}
	})

	t.Run("int/object_cust_valueOf", func(t *testing.T) {
		var i int
		obj, err := vm.RunString(`
		({
			valueOf: function() { return 42; }
		})
		`)
		if err != nil {
			t.Fatal(err)
		}
		err = vm.ExportTo(obj, &i)
		if err != nil {
			t.Fatal(err)
		}
		if i != 42 {
			t.Fatalf("i: %d", i)
		}
	})

	t.Run("float32/no_trunc", func(t *testing.T) {
		var f float32
		err := vm.ExportTo(vm.ToValue(1.234567), &f)
		if err != nil {
			t.Fatal(err)
		}
		if f != 1.234567 {
			t.Fatalf("f: %f", f)
		}
	})

	t.Run("float32/trunc", func(t *testing.T) {
		var f float32
		err := vm.ExportTo(vm.ToValue(1.234567890), &f)
		if err != nil {
			t.Fatal(err)
		}
		if f != float32(1.234567890) {
			t.Fatalf("f: %f", f)
		}
	})

	t.Run("float64", func(t *testing.T) {
		var f float64
		err := vm.ExportTo(vm.ToValue(1.234567), &f)
		if err != nil {
			t.Fatal(err)
		}
		if f != 1.234567 {
			t.Fatalf("f: %f", f)
		}
	})

	t.Run("float32/object", func(t *testing.T) {
		var f float32
		err := vm.ExportTo(vm.NewObject(), &f)
		if err != nil {
			t.Fatal(err)
		}
		if f == f { // expecting NaN
			t.Fatalf("f: %f", f)
		}
	})

	t.Run("float64/object", func(t *testing.T) {
		var f float64
		err := vm.ExportTo(vm.NewObject(), &f)
		if err != nil {
			t.Fatal(err)
		}
		if f == f { // expecting NaN
			t.Fatalf("f: %f", f)
		}
	})

}

func TestRuntime_ExportToSlice(t *testing.T) {
	const SCRIPT = `
	var a = [1, 2, 3];
	a;
	`

	vm := New()
	v, err := vm.RunString(SCRIPT)
	if err != nil {
		t.Fatal(err)
	}
	var a []string
	err = vm.ExportTo(v, &a)
	if err != nil {
		t.Fatal(err)
	}
	if l := len(a); l != 3 {
		t.Fatalf("Unexpected len: %d", l)
	}
	if a[0] != "1" || a[1] != "2" || a[2] != "3" {
		t.Fatalf("Unexpected value: %+v", a)
	}
}

func TestRuntime_ExportToMap(t *testing.T) {
	const SCRIPT = `
	var m = {
		"0": 1,
		"1": 2,
		"2": 3,
	}
	m;
	`

	vm := New()
	v, err := vm.RunString(SCRIPT)
	if err != nil {
		t.Fatal(err)
	}
	var m map[int]string
	err = vm.ExportTo(v, &m)
	if err != nil {
		t.Fatal(err)
	}
	if l := len(m); l != 3 {
		t.Fatalf("Unexpected len: %d", l)
	}
	if m[0] != "1" || m[1] != "2" || m[2] != "3" {
		t.Fatalf("Unexpected value: %+v", m)
	}
}

func TestRuntime_ExportToMap1(t *testing.T) {
	const SCRIPT = `
	var m = {
		"0": 1,
		"1": 2,
		"2": 3,
	}
	m;
	`

	vm := New()
	v, err := vm.RunString(SCRIPT)
	if err != nil {
		t.Fatal(err)
	}
	var m map[string]string
	err = vm.ExportTo(v, &m)
	if err != nil {
		t.Fatal(err)
	}
	if l := len(m); l != 3 {
		t.Fatalf("Unexpected len: %d", l)
	}
	if m["0"] != "1" || m["1"] != "2" || m["2"] != "3" {
		t.Fatalf("Unexpected value: %+v", m)
	}
}

func TestRuntime_ExportToStruct(t *testing.T) {
	const SCRIPT = `
	var m = {
		Test: 1,
	}
	m;
	`
	vm := New()
	v, err := vm.RunString(SCRIPT)
	if err != nil {
		t.Fatal(err)
	}

	var o testGoReflectMethod_O
	err = vm.ExportTo(v, &o)
	if err != nil {
		t.Fatal(err)
	}

	if o.Test != "1" {
		t.Fatalf("Unexpected value: '%s'", o.Test)
	}

}

func TestRuntime_ExportToStructPtr(t *testing.T) {
	const SCRIPT = `
	var m = {
		Test: 1,
	}
	m;
	`
	vm := New()
	v, err := vm.RunString(SCRIPT)
	if err != nil {
		t.Fatal(err)
	}

	var o *testGoReflectMethod_O
	err = vm.ExportTo(v, &o)
	if err != nil {
		t.Fatal(err)
	}

	if o.Test != "1" {
		t.Fatalf("Unexpected value: '%s'", o.Test)
	}

}

func TestRuntime_ExportToStructAnonymous(t *testing.T) {
	type BaseTestStruct struct {
		A int64
		B int64
	}

	type TestStruct struct {
		BaseTestStruct
		C string
	}

	const SCRIPT = `
	var m = {
		A: 1,
		B: 2,
		C: "testC"
	}
	m;
	`
	vm := New()
	v, err := vm.RunString(SCRIPT)
	if err != nil {
		t.Fatal(err)
	}

	test := &TestStruct{}
	err = vm.ExportTo(v, test)
	if err != nil {
		t.Fatal(err)
	}

	if test.A != 1 {
		t.Fatalf("Unexpected value: '%d'", test.A)
	}
	if test.B != 2 {
		t.Fatalf("Unexpected value: '%d'", test.B)
	}
	if test.C != "testC" {
		t.Fatalf("Unexpected value: '%s'", test.C)
	}

}

func TestRuntime_ExportToStructFromPtr(t *testing.T) {
	vm := New()
	v := vm.ToValue(&testGoReflectMethod_O{
		field: "5",
		Test:  "12",
	})

	var o testGoReflectMethod_O
	err := vm.ExportTo(v, &o)
	if err != nil {
		t.Fatal(err)
	}

	if o.Test != "12" {
		t.Fatalf("Unexpected value: '%s'", o.Test)
	}
	if o.field != "5" {
		t.Fatalf("Unexpected value for field: '%s'", o.field)
	}
}

func TestRuntime_ExportToStructWithPtrValues(t *testing.T) {
	type BaseTestStruct struct {
		A int64
		B *int64
	}

	type TestStruct2 struct {
		E string
	}

	type TestStruct struct {
		BaseTestStruct
		C *string
		D *TestStruct2
	}

	const SCRIPT = `
	var m = {
		A: 1,
		B: 2,
		C: "testC",
		D: {
			E: "testE",
		}
	}
	m;
	`
	vm := New()
	v, err := vm.RunString(SCRIPT)
	if err != nil {
		t.Fatal(err)
	}

	test := &TestStruct{}
	err = vm.ExportTo(v, test)
	if err != nil {
		t.Fatal(err)
	}

	if test.A != 1 {
		t.Fatalf("Unexpected value: '%d'", test.A)
	}
	if test.B == nil || *test.B != 2 {
		t.Fatalf("Unexpected value: '%v'", test.B)
	}
	if test.C == nil || *test.C != "testC" {
		t.Fatalf("Unexpected value: '%v'", test.C)
	}
	if test.D == nil || test.D.E != "testE" {
		t.Fatalf("Unexpected value: '%s'", test.D.E)
	}

}

func TestRuntime_ExportToTime(t *testing.T) {
	const SCRIPT = `
	var dateStr = "2018-08-13T15:02:13+02:00";
	var str = "test123";
	`

	vm := New()
	_, err := vm.RunString(SCRIPT)
	if err != nil {
		t.Fatal(err)
	}

	var ti time.Time
	err = vm.ExportTo(vm.Get("dateStr"), &ti)
	if err != nil {
		t.Fatal(err)
	}
	if ti.Format(time.RFC3339) != "2018-08-13T15:02:13+02:00" {
		t.Fatalf("Unexpected value: '%s'", ti.Format(time.RFC3339))
	}

	err = vm.ExportTo(vm.Get("str"), &ti)
	if err == nil {
		t.Fatal("Expected err to not be nil")
	}

	var str string
	err = vm.ExportTo(vm.Get("dateStr"), &str)
	if err != nil {
		t.Fatal(err)
	}
	if str != "2018-08-13T15:02:13+02:00" {
		t.Fatalf("Unexpected value: '%s'", str)
	}

	d, err := vm.RunString(`new Date(1000)`)
	if err != nil {
		t.Fatal(err)
	}

	ti = time.Time{}
	err = vm.ExportTo(d, &ti)
	if err != nil {
		t.Fatal(err)
	}

	if ti.UnixNano() != 1000*1e6 {
		t.Fatal(ti)
	}
	if ti.Location() != time.Local {
		t.Fatalf("Wrong location: %v", ti)
	}
}

func ExampleRuntime_ExportTo_func() {
	const SCRIPT = `
	function f(param) {
		return +param + 2;
	}
	`

	vm := New()
	_, err := vm.RunString(SCRIPT)
	if err != nil {
		panic(err)
	}

	var fn func(string) string
	err = vm.ExportTo(vm.Get("f"), &fn)
	if err != nil {
		panic(err)
	}

	fmt.Println(fn("40")) // note, _this_ value in the function will be undefined.
	// Output: 42
}

func ExampleRuntime_ExportTo_funcThrow() {
	const SCRIPT = `
	function f(param) {
		throw new Error("testing");
	}
	`

	vm := New()
	_, err := vm.RunString(SCRIPT)
	if err != nil {
		panic(err)
	}

	var fn func(string) (string, error)
	err = vm.ExportTo(vm.Get("f"), &fn)
	if err != nil {
		panic(err)
	}
	_, err = fn("")

	fmt.Println(err)
	// Output: Error: testing at f (<eval>:3:9(3))
}

func ExampleRuntime_ExportTo_funcVariadic() {
	const SCRIPT = `
	function f(...args) {
		return args.join("#");
	}
	`
	vm := New()
	_, err := vm.RunString(SCRIPT)
	if err != nil {
		panic(err)
	}

	var fn func(args ...interface{}) string
	err = vm.ExportTo(vm.Get("f"), &fn)
	if err != nil {
		panic(err)
	}
	fmt.Println(fn("a", "b", 42))
	// Output: a#b#42
}

func TestRuntime_ExportTo_funcVariadic(t *testing.T) {
	const SCRIPT = `
	function f(...args) {
		return args.join("#");
	}
	`
	vm := New()
	_, err := vm.RunString(SCRIPT)
	if err != nil {
		panic(err)
	}

	t.Run("no args", func(t *testing.T) {
		var fn func(args ...any) string
		err = vm.ExportTo(vm.Get("f"), &fn)
		if err != nil {
			panic(err)
		}
		res := fn()
		if res != "" {
			t.Fatal(res)
		}
	})

	t.Run("non-variadic args", func(t *testing.T) {
		var fn func(firstArg any, args ...any) string
		err = vm.ExportTo(vm.Get("f"), &fn)
		if err != nil {
			panic(err)
		}
		res := fn("first")
		if res != "first" {
			t.Fatal(res)
		}
	})

	t.Run("non-variadic and variadic args", func(t *testing.T) {
		var fn func(firstArg any, args ...any) string
		err = vm.ExportTo(vm.Get("f"), &fn)
		if err != nil {
			panic(err)
		}
		res := fn("first", "second")
		if res != "first#second" {
			t.Fatal(res)
		}
	})

}

func TestRuntime_ExportToFuncFail(t *testing.T) {
	const SCRIPT = `
	function f(param) {
		return +param + 2;
	}
	`

	type T struct {
		Field1 int
	}

	var fn func(string) (T, error)

	vm := New()
	_, err := vm.RunString(SCRIPT)
	if err != nil {
		t.Fatal(err)
	}

	err = vm.ExportTo(vm.Get("f"), &fn)
	if err != nil {
		t.Fatal(err)
	}

	if _, err := fn("40"); err == nil {
		t.Fatal("Expected error")
	}
}

func TestRuntime_ExportToCallable(t *testing.T) {
	const SCRIPT = `
	function f(param) {
		return +param + 2;
	}
	`
	vm := New()
	_, err := vm.RunString(SCRIPT)
	if err != nil {
		t.Fatal(err)
	}

	var c Callable
	err = vm.ExportTo(vm.Get("f"), &c)
	if err != nil {
		t.Fatal(err)
	}

	res, err := c(Undefined(), vm.ToValue("40"))
	if err != nil {
		t.Fatal(err)
	} else if !res.StrictEquals(vm.ToValue(42)) {
		t.Fatalf("Unexpected value: %v", res)
	}
}

func TestRuntime_ExportToObject(t *testing.T) {
	const SCRIPT = `
	var o = {"test": 42};
	o;
	`
	vm := New()
	_, err := vm.RunString(SCRIPT)
	if err != nil {
		t.Fatal(err)
	}

	var o *Object
	err = vm.ExportTo(vm.Get("o"), &o)
	if err != nil {
		t.Fatal(err)
	}

	if v := o.Get("test"); !v.StrictEquals(vm.ToValue(42)) {
		t.Fatalf("Unexpected value: %v", v)
	}
}

func TestRuntime_SetGlobalObject(t *testing.T) {
	vm := New()

	_, err := vm.RunString(`var myVar = 123;`)
	if err != nil {
		t.Fatal(err)
	}

	oldGlobalObject := vm.GlobalObject()

	newGlobalObject, err := vm.RunString(`({oldGlobalReference:globalThis});`)
	if err != nil {
		t.Fatal(err)
	}

	vm.SetGlobalObject(newGlobalObject.(*Object))

	if vm.GlobalObject() != newGlobalObject {
		t.Fatal("Expected global object to be new object")
	}

	if vm.GlobalObject().Get("myVar") != nil {
		t.Fatal("Expected myVar to be undefined")
	}

	oldGlobalReference := vm.GlobalObject().Get("oldGlobalReference")
	if oldGlobalReference == nil {
		t.Fatal("Expected oldGlobalReference to be defined")
	}

	if oldGlobalReference != oldGlobalObject {
		t.Fatal("Expected reference to be to old global object")
	}
}

func TestRuntime_SetGlobalObject_Proxy(t *testing.T) {
	vm := New()

	globalObject := vm.GlobalObject()

	globalObjectProxy := vm.NewProxy(globalObject, &ProxyTrapConfig{
		Get: func(target *Object, property string, receiver Value) (value Value) {
			if target != globalObject {
				t.Fatal("Expected target to be global object")
			}

			if property != "testing" {
				t.Fatal("Expected property to be 'testing'")
			}

			return valueTrue
		},
	})

	vm.SetGlobalObject(vm.ToValue(globalObjectProxy).(*Object))

	ret, err := vm.RunString("testing")
	if err != nil {
		t.Fatal(err)
	}

	if ret != valueTrue {
		t.Fatal("Expected return value to equal true")
	}
}

func ExampleAssertFunction() {
	vm := New()
	_, err := vm.RunString(`
	function sum(a, b) {
		return a+b;
	}
	`)
	if err != nil {
		panic(err)
	}
	sum, ok := AssertFunction(vm.Get("sum"))
	if !ok {
		panic("Not a function")
	}

	res, err := sum(Undefined(), vm.ToValue(40), vm.ToValue(2))
	if err != nil {
		panic(err)
	}
	fmt.Println(res)
	// Output: 42
}

func TestGoFuncError(t *testing.T) {
	const SCRIPT = `
	try {
		f();
	} catch (e) {
		if (!(e instanceof GoError)) {
			throw(e);
		}
		if (e.value.Error() !== "Test") {
			throw("Unexpected value: " + e.value.Error());
		}
	}
	`

	f := func() error {
		return errors.New("Test")
	}

	vm := New()
	vm.Set("f", f)
	_, err := vm.RunString(SCRIPT)
	if err != nil {
		t.Fatal(err)
	}
}

func TestToValueNil(t *testing.T) {
	type T struct{}
	var a *T
	vm := New()

	if v := vm.ToValue(nil); !IsNull(v) {
		t.Fatalf("nil: %v", v)
	}

	if v := vm.ToValue(a); !IsNull(v) {
		t.Fatalf("struct ptr: %v", v)
	}

	var m map[string]interface{}
	if v := vm.ToValue(m); !IsNull(v) {
		t.Fatalf("map[string]interface{}: %v", v)
	}

	var ar []interface{}
	if v := vm.ToValue(ar); IsNull(v) {
		t.Fatalf("[]interface{}: %v", v)
	}

	var arptr *[]interface{}
	if v := vm.ToValue(arptr); !IsNull(v) {
		t.Fatalf("*[]interface{}: %v", v)
	}
}

func TestToValueFloat(t *testing.T) {
	vm := New()
	vm.Set("f64", float64(123))
	vm.Set("f32", float32(321))

	v, err := vm.RunString("f64 === 123 && f32 === 321")
	if err != nil {
		t.Fatal(err)
	}
	if v.Export().(bool) != true {
		t.Fatalf("StrictEquals for golang float failed")
	}
}

func TestToValueInterface(t *testing.T) {

	f := func(i interface{}) bool {
		return i == t
	}
	vm := New()
	vm.Set("f", f)
	vm.Set("t", t)
	v, err := vm.RunString(`f(t)`)
	if err != nil {
		t.Fatal(err)
	}
	if v != valueTrue {
		t.Fatalf("v: %v", v)
	}
}

func TestJSONEscape(t *testing.T) {
	const SCRIPT = `
	var a = "\\+1";
	JSON.stringify(a);
	`

	testScript(SCRIPT, asciiString(`"\\+1"`), t)
}

func TestJSONObjectInArray(t *testing.T) {
	const SCRIPT = `
	var a = "[{\"a\":1},{\"a\":2}]";
	JSON.stringify(JSON.parse(a)) == a;
	`

	testScript(SCRIPT, valueTrue, t)
}

func TestJSONQuirkyNumbers(t *testing.T) {
	const SCRIPT = `
	var s;
	s = JSON.stringify(NaN);
	if (s != "null") {
		throw new Error("NaN: " + s);
	}

	s = JSON.stringify(Infinity);
	if (s != "null") {
		throw new Error("Infinity: " + s);
	}

	s = JSON.stringify(-Infinity);
	if (s != "null") {
		throw new Error("-Infinity: " + s);
	}

	`

	testScript(SCRIPT, _undefined, t)
}

func TestJSONNil(t *testing.T) {
	const SCRIPT = `
	JSON.stringify(i);
	`

	vm := New()
	var i interface{}
	vm.Set("i", i)
	ret, err := vm.RunString(SCRIPT)
	if err != nil {
		t.Fatal(err)
	}

	if ret.String() != "null" {
		t.Fatalf("Expected 'null', got: %v", ret)
	}
}

type customJsonEncodable struct{}

func (*customJsonEncodable) JsonEncodable() interface{} {
	return "Test"
}

func TestJsonEncodable(t *testing.T) {
	var s customJsonEncodable

	vm := New()
	vm.Set("s", &s)

	ret, err := vm.RunString("JSON.stringify(s)")
	if err != nil {
		t.Fatal(err)
	}
	if !ret.StrictEquals(vm.ToValue("\"Test\"")) {
		t.Fatalf("Expected \"Test\", got: %v", ret)
	}
}

func TestSortComparatorReturnValues(t *testing.T) {
	const SCRIPT = `
	var a = [];
	for (var i = 0; i < 12; i++) {
	    a[i] = i;
	}

	a.sort(function(x, y) { return y - x });

	for (var i = 0; i < 12; i++) {
	    if (a[i] !== 11-i) {
		throw new Error("Value at index " + i + " is incorrect: " + a[i]);
	    }
	}
	`

	testScript(SCRIPT, _undefined, t)
}

func TestSortComparatorReturnValueFloats(t *testing.T) {
	const SCRIPT = `
	var a = [
		5.97,
		9.91,
		4.13,
		9.28,
		3.29,
	];
	a.sort( function(a, b) { return a - b; } );
	for (var i = 1; i < a.length; i++) {
		if (a[i] < a[i-1]) {
			throw new Error("Array is not sorted: " + a);
		}
	}
	`
	testScript(SCRIPT, _undefined, t)
}

func TestSortComparatorReturnValueNegZero(t *testing.T) {
	const SCRIPT = `
	var a = [2, 1];
	a.sort( function(a, b) { return a > b ? 0 : -0; } );
	for (var i = 1; i < a.length; i++) {
		if (a[i] < a[i-1]) {
			throw new Error("Array is not sorted: " + a);
		}
	}
	`
	testScript(SCRIPT, _undefined, t)
}

func TestNilApplyArg(t *testing.T) {
	const SCRIPT = `
	(function x(a, b) {
		return a === undefined && b === 1;
        }).apply(this, [,1])
	`

	testScript(SCRIPT, valueTrue, t)
}

func TestNilCallArg(t *testing.T) {
	const SCRIPT = `
	"use strict";
	function f(a) {
		return this === undefined && a === undefined;
	}
	`
	vm := New()
	prg := MustCompile("test.js", SCRIPT, false)
	vm.RunProgram(prg)
	if f, ok := AssertFunction(vm.Get("f")); ok {
		v, err := f(nil, nil)
		if err != nil {
			t.Fatal(err)
		}
		if !v.StrictEquals(valueTrue) {
			t.Fatalf("Unexpected result: %v", v)
		}
	}
}

func TestNullCallArg(t *testing.T) {
	const SCRIPT = `
	f(null);
	`
	vm := New()
	prg := MustCompile("test.js", SCRIPT, false)
	vm.Set("f", func(x *int) bool {
		return x == nil
	})

	v, err := vm.RunProgram(prg)
	if err != nil {
		t.Fatal(err)
	}

	if !v.StrictEquals(valueTrue) {
		t.Fatalf("Unexpected result: %v", v)
	}
}

func TestObjectKeys(t *testing.T) {
	const SCRIPT = `
	var o = { a: 1, b: 2, c: 3, d: 4 };
	o;
	`

	vm := New()
	prg := MustCompile("test.js", SCRIPT, false)

	res, err := vm.RunProgram(prg)
	if err != nil {
		t.Fatal(err)
	}

	if o, ok := res.(*Object); ok {
		keys := o.Keys()
		if !reflect.DeepEqual(keys, []string{"a", "b", "c", "d"}) {
			t.Fatalf("Unexpected keys: %v", keys)
		}
	}
}

func TestReflectCallExtraArgs(t *testing.T) {
	const SCRIPT = `
	f(41, "extra")
	`
	f := func(x int) int {
		return x + 1
	}

	vm := New()
	vm.Set("f", f)

	prg := MustCompile("test.js", SCRIPT, false)

	res, err := vm.RunProgram(prg)
	if err != nil {
		t.Fatal(err)
	}
	if !res.StrictEquals(intToValue(42)) {
		t.Fatalf("Unexpected result: %v", res)
	}
}

func TestReflectCallNotEnoughArgs(t *testing.T) {
	const SCRIPT = `
	f(42)
	`
	vm := New()

	f := func(x, y int, z *int, s string) (int, error) {
		if z != nil {
			return 0, fmt.Errorf("z is not nil")
		}
		if s != "" {
			return 0, fmt.Errorf("s is not \"\"")
		}
		return x + y, nil
	}

	vm.Set("f", f)

	prg := MustCompile("test.js", SCRIPT, false)

	res, err := vm.RunProgram(prg)
	if err != nil {
		t.Fatal(err)
	}
	if !res.StrictEquals(intToValue(42)) {
		t.Fatalf("Unexpected result: %v", res)
	}
}

func TestReflectCallVariadic(t *testing.T) {
	const SCRIPT = `
	var r = f("Hello %s, %d", "test", 42);
	if (r !== "Hello test, 42") {
		throw new Error("test 1 has failed: " + r);
	}

	r = f("Hello %s, %s", "test");
	if (r !== "Hello test, %!s(MISSING)") {
		throw new Error("test 2 has failed: " + r);
	}

	r = f();
	if (r !== "") {
		throw new Error("test 3 has failed: " + r);
	}

	`

	vm := New()
	vm.Set("f", fmt.Sprintf)

	prg := MustCompile("test.js", SCRIPT, false)

	_, err := vm.RunProgram(prg)
	if err != nil {
		t.Fatal(err)
	}
}

func TestReflectNullValueArgument(t *testing.T) {
	rt := New()
	rt.Set("fn", func(v Value) {
		if v == nil {
			t.Error("null becomes nil")
		}
		if !IsNull(v) {
			t.Error("null is not null")
		}
	})
	rt.RunString(`fn(null);`)
}

type testNativeConstructHelper struct {
	rt   *Runtime
	base int64
	// any other state
}

func (t *testNativeConstructHelper) calc(call FunctionCall) Value {
	return t.rt.ToValue(t.base + call.Argument(0).ToInteger())
}

func TestNativeConstruct(t *testing.T) {
	const SCRIPT = `
	var f = new F(40);
	f instanceof F && f.method() === 42 && f.calc(2) === 42;
	`

	rt := New()

	method := func(call FunctionCall) Value {
		return rt.ToValue(42)
	}

	rt.Set("F", func(call ConstructorCall) *Object { // constructor signature (as opposed to 'func(FunctionCall) Value')
		h := &testNativeConstructHelper{
			rt:   rt,
			base: call.Argument(0).ToInteger(),
		}
		call.This.Set("method", method)
		call.This.Set("calc", h.calc)
		return nil // or any other *Object which will be used instead of call.This
	})

	prg := MustCompile("test.js", SCRIPT, false)

	res, err := rt.RunProgram(prg)
	if err != nil {
		t.Fatal(err)
	}

	if !res.StrictEquals(valueTrue) {
		t.Fatalf("Unexpected result: %v", res)
	}

	if fn, ok := AssertFunction(rt.Get("F")); ok {
		v, err := fn(nil, rt.ToValue(42))
		if err != nil {
			t.Fatal(err)
		}
		if o, ok := v.(*Object); ok {
			if o.Get("method") == nil {
				t.Fatal("No method")
			}
		} else {
			t.Fatal("Not an object")
		}
	} else {
		t.Fatal("Not a function")
	}

	resp := &testNativeConstructHelper{}
	value := rt.ToValue(resp)
	if value.Export() != resp {
		t.Fatal("no")
	}
}

func TestCreateObject(t *testing.T) {
	const SCRIPT = `
	inst instanceof C;
	`

	r := New()
	c := r.ToValue(func(call ConstructorCall) *Object {
		return nil
	})

	proto := c.(*Object).Get("prototype").(*Object)

	inst := r.CreateObject(proto)

	r.Set("C", c)
	r.Set("inst", inst)

	prg := MustCompile("test.js", SCRIPT, false)

	res, err := r.RunProgram(prg)
	if err != nil {
		t.Fatal(err)
	}

	if !res.StrictEquals(valueTrue) {
		t.Fatalf("Unexpected result: %v", res)
	}
}

func TestInterruptInWrappedFunction(t *testing.T) {
	rt := New()
	v, err := rt.RunString(`
		var fn = function() {
			while (true) {}
		};
		fn;
	`)
	if err != nil {
		t.Fatal(err)
	}
	fn, ok := AssertFunction(v)
	if !ok {
		t.Fatal("Not a function")
	}
	go func() {
		<-time.After(10 * time.Millisecond)
		rt.Interrupt(errors.New("hi"))
	}()

	_, err = fn(nil)
	if err == nil {
		t.Fatal("expected error")
	}
	if _, ok := err.(*InterruptedError); !ok {
		t.Fatalf("Wrong error type: %T", err)
	}
}

func TestInterruptInWrappedFunction2(t *testing.T) {
	rt := New()
	// this test panics as otherwise goja will recover and possibly loop
	var called bool
	rt.Set("v", rt.ToValue(func() {
		if called {
			go func() {
				panic("this should never get called twice")
			}()
		}
		called = true
		rt.Interrupt("here is the error")
	}))

	rt.Set("s", rt.ToValue(func(a Callable) (Value, error) {
		return a(nil)
	}))

	rt.Set("k", rt.ToValue(func(e Value) {
		go func() {
			panic("this should never get called actually")
		}()
	}))
	_, err := rt.RunString(`
        Promise.resolve().then(()=>k()); // this should never resolve
        while(true) {
            try{
                s(() =>{
                    v();
                })
                break;
            } catch (e) {
                k(e);
            }
        }
	`)
	if err == nil {
		t.Fatal("expected error but got no error")
	}
	intErr := new(InterruptedError)
	if !errors.As(err, &intErr) {
		t.Fatalf("Wrong error type: %T", err)
	}
	if !strings.Contains(intErr.Error(), "here is the error") {
		t.Fatalf("Wrong error message: %q", intErr.Error())
	}
	_, err = rt.RunString(`Promise.resolve().then(()=>globalThis.S=5)`)
	if err != nil {
		t.Fatal(err)
	}
	s := rt.Get("S")
	if s == nil || s.ToInteger() != 5 {
		t.Fatalf("Wrong value for S %v", s)
	}
}

func TestInterruptInWrappedFunction2Recover(t *testing.T) {
	rt := New()
	// this test panics as otherwise goja will recover and possibly loop
	var vCalled int
	rt.Set("v", rt.ToValue(func() {
		if vCalled == 0 {
			rt.Interrupt("here is the error")
		}
		vCalled++
	}))

	rt.Set("s", rt.ToValue(func(a Callable) (Value, error) {
		v, err := a(nil)
		if err != nil {
			intErr := new(InterruptedError)
			if errors.As(err, &intErr) {
				rt.ClearInterrupt()
				return nil, errors.New("oops we got interrupted let's not that")
			}
		}
		return v, err
	}))
	var kCalled int

	rt.Set("k", rt.ToValue(func(e Value) {
		kCalled++
	}))
	_, err := rt.RunString(`
        Promise.resolve().then(()=>k());
        while(true) {
            try{
                s(() => {
                    v();
                })
                break;
            } catch (e) {
                k(e);
            }
        }
	`)
	if err != nil {
		t.Fatal(err)
	}
	if vCalled != 2 {
		t.Fatalf("v was not called exactly twice but %d times", vCalled)
	}
	if kCalled != 2 {
		t.Fatalf("k was not called exactly twice but %d times", kCalled)
	}
	_, err = rt.RunString(`Promise.resolve().then(()=>globalThis.S=5)`)
	if err != nil {
		t.Fatal(err)
	}
	s := rt.Get("S")
	if s == nil || s.ToInteger() != 5 {
		t.Fatalf("Wrong value for S %v", s)
	}
}

func TestInterruptInWrappedFunctionExpectInteruptError(t *testing.T) {
	rt := New()
	// this test panics as otherwise goja will recover and possibly loop
	rt.Set("v", rt.ToValue(func() {
		rt.Interrupt("here is the error")
	}))

	rt.Set("s", rt.ToValue(func(a Callable) (Value, error) {
		return a(nil)
	}))

	_, err := rt.RunString(`
        s(() =>{
            v();
        })
	`)
	if err == nil {
		t.Fatal("expected error but got no error")
	}
	var intErr *InterruptedError
	if !errors.As(err, &intErr) {
		t.Fatalf("Wrong error type: %T", err)
	}
	if !strings.Contains(intErr.Error(), "here is the error") {
		t.Fatalf("Wrong error message: %q", intErr.Error())
	}
}

func TestInterruptInWrappedFunctionExpectStackOverflowError(t *testing.T) {
	rt := New()
	rt.SetMaxCallStackSize(5)
	// this test panics as otherwise goja will recover and possibly loop
	rt.Set("v", rt.ToValue(func() {
		_, err := rt.RunString(`
		(function loop() { loop() })();
		`)
		if err != nil {
			panic(err)
		}
	}))

	rt.Set("s", rt.ToValue(func(a Callable) (Value, error) {
		return a(nil)
	}))

	_, err := rt.RunString(`
        s(() =>{
            v();
        })
	`)
	if err == nil {
		t.Fatal("expected error but got no error")
	}
	var soErr *StackOverflowError
	if !errors.As(err, &soErr) {
		t.Fatalf("Wrong error type: %T", err)
	}
}

func TestInterruptWithPromises(t *testing.T) {
	rt := New()
	rt.SetMaxCallStackSize(5)
	// this test panics as otherwise goja will recover and possibly loop
	rt.Set("abort", rt.ToValue(func() {
		// panic("waty")
		rt.Interrupt("abort this")
	}))
	var queue = make(chan func() error, 10)
	rt.Set("myPromise", func() Value {
		p, resolve, _ := rt.NewPromise()
		queue <- func() error {
			return resolve("some value")
		}

		return rt.ToValue(p)
	})

	_, err := rt.RunString(`
		let p = myPromise()
		p.then(() => { abort() });
	`)
	if err != nil {
		t.Fatal("expected noerror but got error")
	}
	f := <-queue
	err = f()
	if err == nil {
		t.Fatal("expected error but got no error")
	}
	t.Log(err)
	var soErr *InterruptedError
	if !errors.As(err, &soErr) {
		t.Fatalf("Wrong error type: %T", err)
	}
}

func TestRunLoopPreempt(t *testing.T) {
	vm := New()
	v, err := vm.RunString("(function() {for (;;) {}})")
	if err != nil {
		t.Fatal(err)
	}

	fn, ok := AssertFunction(v)
	if !ok {
		t.Fatal("Not a function")
	}

	go func() {
		<-time.After(100 * time.Millisecond)
		runtime.GC() // this hangs if the vm loop does not have any preemption points
		vm.Interrupt(errors.New("hi"))
	}()

	_, err = fn(nil)
	if err == nil {
		t.Fatal("expected error")
	}
	if _, ok := err.(*InterruptedError); !ok {
		t.Fatalf("Wrong error type: %T", err)
	}
}

func TestNaN(t *testing.T) {
	if !IsNaN(_NaN) {
		t.Fatal("IsNaN() doesn't detect NaN")
	}
	if IsNaN(Undefined()) {
		t.Fatal("IsNaN() says undefined is a NaN")
	}
	if !IsNaN(NaN()) {
		t.Fatal("NaN() doesn't return NaN")
	}
}

func TestInf(t *testing.T) {
	if !IsInfinity(_positiveInf) {
		t.Fatal("IsInfinity() doesn't detect +Inf")
	}
	if !IsInfinity(_negativeInf) {
		t.Fatal("IsInfinity() doesn't detect -Inf")
	}
	if IsInfinity(Undefined()) {
		t.Fatal("IsInfinity() says undefined is a Infinity")
	}
	if !IsInfinity(PositiveInf()) {
		t.Fatal("PositiveInfinity() doesn't return Inf")
	}
	if !IsInfinity(NegativeInf()) {
		t.Fatal("NegativeInfinity() doesn't return Inf")
	}
}

func TestRuntimeNew(t *testing.T) {
	vm := New()
	v, err := vm.New(vm.Get("Number"), vm.ToValue("12345"))
	if err != nil {
		t.Fatal(err)
	}
	if n, ok := v.Export().(int64); ok {
		if n != 12345 {
			t.Fatalf("n: %v", n)
		}
	} else {
		t.Fatalf("v: %T", v)
	}
}

func TestAutoBoxing(t *testing.T) {
	const SCRIPT = `
	function f() {
		'use strict';
		var a = 1;
		var thrown1 = false;
		var thrown2 = false;
		try {
			a.test = 42;
		} catch (e) {
			thrown1 = e instanceof TypeError;
		}
		try {
			a["test1"] = 42;
		} catch (e) {
			thrown2 = e instanceof TypeError;
		}
		return thrown1 && thrown2;
	}
	var a = 1;
	a.test = 42; // should not throw
	a["test1"] = 42; // should not throw
	a.test === undefined && a.test1 === undefined && f();
	`

	testScript(SCRIPT, valueTrue, t)
}

func TestProtoGetter(t *testing.T) {
	const SCRIPT = `
	({}).__proto__ === Object.prototype && [].__proto__ === Array.prototype;
	`
	testScript(SCRIPT, valueTrue, t)
}

func TestSymbol1(t *testing.T) {
	const SCRIPT = `
		Symbol.toPrimitive[Symbol.toPrimitive]() === Symbol.toPrimitive;
	`

	testScript(SCRIPT, valueTrue, t)
}

func TestFreezeSymbol(t *testing.T) {
	const SCRIPT = `
		var s = Symbol(1);
		var o = {};
		o[s] = 42;
		Object.freeze(o);
		o[s] = 43;
		o[s] === 42 && Object.isFrozen(o);
	`

	testScript(SCRIPT, valueTrue, t)
}

func TestToPropertyKey(t *testing.T) {
	const SCRIPT = `
	var sym = Symbol(42);
	var callCount = 0;

	var wrapper = {
	  toString: function() {
		callCount += 1;
		return sym;
	  },
	  valueOf: function() {
		$ERROR("valueOf() called");
	  }
	};

	var o = {};
	o[wrapper] = function() { return "test" };
	assert.sameValue(o[wrapper], o[sym], "o[wrapper] === o[sym]");
	assert.sameValue(o[wrapper](), "test", "o[wrapper]()");
	assert.sameValue(o[sym](), "test", "o[sym]()");

	var wrapper1 = {};
	wrapper1[Symbol.toPrimitive] = function(hint) {
		if (hint === "string" || hint === "default") {
			return "1";
		}
		if (hint === "number") {
			return 2;
		}
		$ERROR("Unknown hint value "+hint);
	};
	var a = [];
	a[wrapper1] = 42;
	assert.sameValue(a[1], 42, "a[1]");
	assert.sameValue(a[1], a[wrapper1], "a[1] === a[wrapper1]");
	`

	testScriptWithTestLib(SCRIPT, _undefined, t)
}

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

		Boolean.prototype.toString = function() {
		  return typeof this;
		};

		assert.sameValue(true.toLocaleString(), "boolean");

		Boolean.prototype[Symbol.iterator] = function() {
			return [typeof this][Symbol.iterator]();
		}
		var s = new Set(true);
		assert.sameValue(s.size, 1, "size");
		assert.sameValue(s.has("boolean"), true, "s.has('boolean')");
	}
	t();
	`

	testScriptWithTestLib(SCRIPT, _undefined, t)
}

func TestPrimThisValueGetter(t *testing.T) {
	const SCRIPT = `
	function t() {
		'use strict';
		Object.defineProperty(Boolean.prototype, "toString", {
		  get: function() {
			var v = typeof this;
			return function() {
			  return v;
			};
		  }
		});

		assert.sameValue(true.toLocaleString(), "boolean");
	}
	t();
	`

	testScriptWithTestLib(SCRIPT, _undefined, t)
}

func TestObjSetSym(t *testing.T) {
	const SCRIPT = `
	'use strict';
	var sym = Symbol(true);
	var p1 = Object.create(null);
	var p2 = Object.create(p1);
	
	Object.defineProperty(p1, sym, {
	value: 42
	});
	
	Object.defineProperty(p2, sym, {
	value: 43,
	writable: true,
	});
	var o = Object.create(p2);
	o[sym] = 44;
	o[sym];
	`
	testScript(SCRIPT, intToValue(44), t)
}

func TestObjSet(t *testing.T) {
	const SCRIPT = `
	'use strict';
	var p1 = Object.create(null);
	var p2 = Object.create(p1);
	
	Object.defineProperty(p1, "test", {
	value: 42
	});
	
	Object.defineProperty(p2, "test", {
	value: 43,
	writable: true,
	});
	var o = Object.create(p2);
	o.test = 44;
	o.test;
	`
	testScript(SCRIPT, intToValue(44), t)
}

func TestToValueNilValue(t *testing.T) {
	r := New()
	var a Value
	r.Set("a", a)
	ret, err := r.RunString(`
	""+a;
	`)
	if err != nil {
		t.Fatal(err)
	}
	if !asciiString("null").SameAs(ret) {
		t.Fatalf("ret: %v", ret)
	}
}

func TestDateConversion(t *testing.T) {
	now := time.Now()
	vm := New()
	val, err := vm.New(vm.Get("Date").ToObject(vm), vm.ToValue(now.UnixNano()/1e6))
	if err != nil {
		t.Fatal(err)
	}
	vm.Set("d", val)
	res, err := vm.RunString(`+d`)
	if err != nil {
		t.Fatal(err)
	}
	if exp := res.Export(); exp != now.UnixNano()/1e6 {
		t.Fatalf("Value does not match: %v", exp)
	}
	vm.Set("goval", now)
	res, err = vm.RunString(`+(new Date(goval.UnixNano()/1e6))`)
	if err != nil {
		t.Fatal(err)
	}
	if exp := res.Export(); exp != now.UnixNano()/1e6 {
		t.Fatalf("Value does not match: %v", exp)
	}
}

func TestNativeCtorNewTarget(t *testing.T) {
	const SCRIPT = `
	function NewTarget() {
	}

	var o = Reflect.construct(Number, [1], NewTarget);
	o.__proto__ === NewTarget.prototype && o.toString() === "[object Number]";
	`
	testScript(SCRIPT, valueTrue, t)
}

func TestNativeCtorNonNewCall(t *testing.T) {
	vm := New()
	vm.Set(`Animal`, func(call ConstructorCall) *Object {
		obj := call.This
		obj.Set(`name`, call.Argument(0).String())
		obj.Set(`eat`, func(call FunctionCall) Value {
			self := call.This.(*Object)
			return vm.ToValue(fmt.Sprintf("%s eat", self.Get(`name`)))
		})
		return nil
	})
	v, err := vm.RunString(`

	function __extends(d, b){
		function __() {
			this.constructor = d;
		}
		d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
	}

	var Cat = (function (_super) {
		__extends(Cat, _super);
		function Cat() {
			return _super.call(this, "cat") || this;
		}
		return Cat;
	}(Animal));

	var cat = new Cat();
	cat instanceof Cat && cat.eat() === "cat eat";
	`)
	if err != nil {
		t.Fatal(err)
	}
	if v != valueTrue {
		t.Fatal(v)
	}
}

func ExampleNewSymbol() {
	sym1 := NewSymbol("66")
	sym2 := NewSymbol("66")
	fmt.Printf("%s %s %v", sym1, sym2, sym1.Equals(sym2))
	// Output: 66 66 false
}

func ExampleObject_SetSymbol() {
	type IterResult struct {
		Done  bool
		Value Value
	}

	vm := New()
	vm.SetFieldNameMapper(UncapFieldNameMapper()) // to use IterResult

	o := vm.NewObject()
	o.SetSymbol(SymIterator, func() *Object {
		count := 0
		iter := vm.NewObject()
		iter.Set("next", func() IterResult {
			if count < 10 {
				count++
				return IterResult{
					Value: vm.ToValue(count),
				}
			}
			return IterResult{
				Done: true,
			}
		})
		return iter
	})
	vm.Set("o", o)

	res, err := vm.RunString(`
	var acc = "";
	for (var v of o) {
		acc += v + " ";
	}
	acc;
	`)
	if err != nil {
		panic(err)
	}
	fmt.Println(res)
	// Output: 1 2 3 4 5 6 7 8 9 10
}

func ExampleRuntime_NewArray() {
	vm := New()
	array := vm.NewArray(1, 2, true)
	vm.Set("array", array)
	res, err := vm.RunString(`
	var acc = "";
	for (var v of array) {
		acc += v + " ";
	}
	acc;
	`)
	if err != nil {
		panic(err)
	}
	fmt.Println(res)
	// Output: 1 2 true
}

func ExampleRuntime_SetParserOptions() {
	vm := New()
	vm.SetParserOptions(parser.WithDisableSourceMaps)

	res, err := vm.RunString(`
	"I did not hang!";
//# sourceMappingURL=/dev/zero`)

	if err != nil {
		panic(err)
	}
	fmt.Println(res.String())
	// Output: I did not hang!
}

func TestRuntime_SetParserOptions_Eval(t *testing.T) {
	vm := New()
	vm.SetParserOptions(parser.WithDisableSourceMaps)

	_, err := vm.RunString(`
	eval("//# sourceMappingURL=/dev/zero");
	`)
	if err != nil {
		t.Fatal(err)
	}
}

func TestNativeCallWithRuntimeParameter(t *testing.T) {
	vm := New()
	vm.Set("f", func(_ FunctionCall, r *Runtime) Value {
		if r == vm {
			return valueTrue
		}
		return valueFalse
	})
	ret, err := vm.RunString(`f()`)
	if err != nil {
		t.Fatal(err)
	}
	if ret != valueTrue {
		t.Fatal(ret)
	}
}

func TestNestedEnumerate(t *testing.T) {
	const SCRIPT = `
	var o = {baz: true, foo: true, bar: true};
	var res = "";
	for (var i in o) {
		delete o.baz;
		Object.defineProperty(o, "hidden", {value: true, configurable: true});
		for (var j in o) {
			Object.defineProperty(o, "0", {value: true, configurable: true});
			Object.defineProperty(o, "1", {value: true, configurable: true});
			for (var k in o) {}
			res += i + "-" + j + " ";
		}
	}
	assert(compareArray(Reflect.ownKeys(o), ["0","1","foo","bar","hidden"]), "keys");
	res;
	`
	testScriptWithTestLib(SCRIPT, asciiString("baz-foo baz-bar foo-foo foo-bar bar-foo bar-bar "), t)
}

func TestAbandonedEnumerate(t *testing.T) {
	const SCRIPT = `
	var o = {baz: true, foo: true, bar: true};
	var res = "";
	for (var i in o) {
		delete o.baz;
		for (var j in o) {
			res += i + "-" + j + " ";
			break;
		}
	}
	res;
	`
	testScript(SCRIPT, asciiString("baz-foo foo-foo bar-foo "), t)
}

func TestIterCloseThrows(t *testing.T) {
	const SCRIPT = `
	var returnCount = 0;
	var iterable = {};
	var iterator = {
	  next: function() {
		return { value: true };
	  },
	  return: function() {
		returnCount += 1;
		throw new Error();
	  }
	};
	iterable[Symbol.iterator] = function() {
	  return iterator;
	};

	try {
		for (var i of iterable) {
				break;
		}
	} catch (e) {};
	returnCount;
	`
	testScript(SCRIPT, valueInt(1), t)
}

func TestDeclareGlobalFunc(t *testing.T) {
	const SCRIPT = `
	var initial;

	Object.defineProperty(this, 'f', {
	  enumerable: true,
	  writable: true,
	  configurable: false
	});

	(0,eval)('initial = f; function f() { return 2222; }');
	var desc = Object.getOwnPropertyDescriptor(this, "f");
	assert(desc.enumerable, "enumerable");
	assert(desc.writable, "writable");
	assert(!desc.configurable, "configurable");
	assert.sameValue(initial(), 2222);
	`
	testScriptWithTestLib(SCRIPT, _undefined, t)
}

func TestStackOverflowError(t *testing.T) {
	vm := New()
	vm.SetMaxCallStackSize(3)
	_, err := vm.RunString(`
	function f() {
		f();
	}
	f();
	`)
	if _, ok := err.(*StackOverflowError); !ok {
		t.Fatal(err)
	}
}

func TestStacktraceLocationThrowFromCatch(t *testing.T) {
	vm := New()
	_, err := vm.RunString(`
	function main(arg) {
		try {
			if (arg === 1) {
				return f1();
			}
			if (arg === 2) {
				return f2();
			}
			if (arg === 3) {
				return f3();
			}
		} catch (e) {
			throw e;
		}
	}
	function f1() {}
	function f2() {
		throw new Error();
	}
	function f3() {}
	main(2);
	`)
	if err == nil {
		t.Fatal("Expected error")
	}
	stack := err.(*Exception).stack
	if len(stack) != 3 {
		t.Fatalf("Unexpected stack len: %v", stack)
	}
	if frame := stack[0]; frame.funcName != "f2" || frame.pc != 2 {
		t.Fatalf("Unexpected stack frame 0: %#v", frame)
	}
	if frame := stack[1]; frame.funcName != "main" || frame.pc != 17 {
		t.Fatalf("Unexpected stack frame 1: %#v", frame)
	}
	if frame := stack[2]; frame.funcName != "" || frame.pc != 7 {
		t.Fatalf("Unexpected stack frame 2: %#v", frame)
	}
}

func TestErrorStackRethrow(t *testing.T) {
	const SCRIPT = `
	function f(e) {
		throw e;
	}
	try {
		f(new Error());
	} catch(e) {
		assertStack(e, [["test.js", "", 6, 5]]);
	}
	`
	testScriptWithTestLibX(SCRIPT, _undefined, t)
}

func TestStacktraceLocationThrowFromGo(t *testing.T) {
	vm := New()
	f := func() {
		panic(vm.ToValue("Test"))
	}
	vm.Set("f", f)
	_, err := vm.RunString(`
	function main() {
		(function noop() {})();
		return callee();
	}
	function callee() {
		return f();
	}
	main();
	`)
	if err == nil {
		t.Fatal("Expected error")
	}
	stack := err.(*Exception).stack
	if len(stack) != 4 {
		t.Fatalf("Unexpected stack len: %v", stack)
	}
	if frame := stack[0]; !strings.HasSuffix(frame.funcName.String(), "TestStacktraceLocationThrowFromGo.func1") {
		t.Fatalf("Unexpected stack frame 0: %#v", frame)
	}
	if frame := stack[1]; frame.funcName != "callee" || frame.pc != 2 {
		t.Fatalf("Unexpected stack frame 1: %#v", frame)
	}
	if frame := stack[2]; frame.funcName != "main" || frame.pc != 6 {
		t.Fatalf("Unexpected stack frame 2: %#v", frame)
	}
	if frame := stack[3]; frame.funcName != "" || frame.pc != 4 {
		t.Fatalf("Unexpected stack frame 3: %#v", frame)
	}
}

func TestStacktraceLocationThrowNativeInTheMiddle(t *testing.T) {
	vm := New()
	v, err := vm.RunString(`(function f1() {
		throw new Error("test")
	})`)
	if err != nil {
		t.Fatal(err)
	}

	var f1 func()
	err = vm.ExportTo(v, &f1)
	if err != nil {
		t.Fatal(err)
	}

	f := func() {
		f1()
	}
	vm.Set("f", f)
	_, err = vm.RunString(`
	function main() {
		(function noop() {})();
		return callee();
	}
	function callee() {
		return f();
	}
	main();
	`)
	if err == nil {
		t.Fatal("Expected error")
	}
	stack := err.(*Exception).stack
	if len(stack) != 5 {
		t.Fatalf("Unexpected stack len: %v", stack)
	}
	if frame := stack[0]; frame.funcName != "f1" || frame.pc != 7 {
		t.Fatalf("Unexpected stack frame 0: %#v", frame)
	}
	if frame := stack[1]; !strings.HasSuffix(frame.funcName.String(), "TestStacktraceLocationThrowNativeInTheMiddle.func1") {
		t.Fatalf("Unexpected stack frame 1: %#v", frame)
	}
	if frame := stack[2]; frame.funcName != "callee" || frame.pc != 2 {
		t.Fatalf("Unexpected stack frame 2: %#v", frame)
	}
	if frame := stack[3]; frame.funcName != "main" || frame.pc != 6 {
		t.Fatalf("Unexpected stack frame 3: %#v", frame)
	}
	if frame := stack[4]; frame.funcName != "" || frame.pc != 4 {
		t.Fatalf("Unexpected stack frame 4: %#v", frame)
	}
}

func TestStrToInt64(t *testing.T) {
	if _, ok := strToInt64(""); ok {
		t.Fatal("<empty>")
	}
	if n, ok := strToInt64("0"); !ok || n != 0 {
		t.Fatal("0", n, ok)
	}
	if n, ok := strToInt64("-0"); ok {
		t.Fatal("-0", n, ok)
	}
	if n, ok := strToInt64("-1"); !ok || n != -1 {
		t.Fatal("-1", n, ok)
	}
	if n, ok := strToInt64("9223372036854775808"); ok {
		t.Fatal("max+1", n, ok)
	}
	if n, ok := strToInt64("9223372036854775817"); ok {
		t.Fatal("9223372036854775817", n, ok)
	}
	if n, ok := strToInt64("-9223372036854775818"); ok {
		t.Fatal("-9223372036854775818", n, ok)
	}
	if n, ok := strToInt64("9223372036854775807"); !ok || n != 9223372036854775807 {
		t.Fatal("max", n, ok)
	}
	if n, ok := strToInt64("-9223372036854775809"); ok {
		t.Fatal("min-1", n, ok)
	}
	if n, ok := strToInt64("-9223372036854775808"); !ok || n != -9223372036854775808 {
		t.Fatal("min", n, ok)
	}
	if n, ok := strToInt64("-00"); ok {
		t.Fatal("-00", n, ok)
	}
	if n, ok := strToInt64("-01"); ok {
		t.Fatal("-01", n, ok)
	}
}

func TestStrToInt32(t *testing.T) {
	if _, ok := strToInt32(""); ok {
		t.Fatal("<empty>")
	}
	if n, ok := strToInt32("0"); !ok || n != 0 {
		t.Fatal("0", n, ok)
	}
	if n, ok := strToInt32("-0"); ok {
		t.Fatal("-0", n, ok)
	}
	if n, ok := strToInt32("-1"); !ok || n != -1 {
		t.Fatal("-1", n, ok)
	}
	if n, ok := strToInt32("2147483648"); ok {
		t.Fatal("max+1", n, ok)
	}
	if n, ok := strToInt32("2147483657"); ok {
		t.Fatal("2147483657", n, ok)
	}
	if n, ok := strToInt32("-2147483658"); ok {
		t.Fatal("-2147483658", n, ok)
	}
	if n, ok := strToInt32("2147483647"); !ok || n != 2147483647 {
		t.Fatal("max", n, ok)
	}
	if n, ok := strToInt32("-2147483649"); ok {
		t.Fatal("min-1", n, ok)
	}
	if n, ok := strToInt32("-2147483648"); !ok || n != -2147483648 {
		t.Fatal("min", n, ok)
	}
	if n, ok := strToInt32("-00"); ok {
		t.Fatal("-00", n, ok)
	}
	if n, ok := strToInt32("-01"); ok {
		t.Fatal("-01", n, ok)
	}
}

func TestDestructSymbol(t *testing.T) {
	const SCRIPT = `
	var S = Symbol("S");
	var s, rest;

	({[S]: s, ...rest} = {[S]: true, test: 1});
	assert.sameValue(s, true, "S");
	assert(deepEqual(rest, {test: 1}), "rest");
	`
	testScriptWithTestLibX(SCRIPT, _undefined, t)
}

func TestAccessorFuncName(t *testing.T) {
	const SCRIPT = `
	const namedSym = Symbol('test262');
	const emptyStrSym = Symbol("");
	const anonSym = Symbol();

	const o = {
	  get id() {},
	  get [anonSym]() {},
	  get [namedSym]() {},
      get [emptyStrSym]() {},
	  set id(v) {},
	  set [anonSym](v) {},
	  set [namedSym](v) {},
      set [emptyStrSym](v) {}
	};

	let prop;
	prop = Object.getOwnPropertyDescriptor(o, 'id');
	assert.sameValue(prop.get.name, 'get id');
	assert.sameValue(prop.set.name, 'set id');

	prop = Object.getOwnPropertyDescriptor(o, anonSym);
	assert.sameValue(prop.get.name, 'get ');
	assert.sameValue(prop.set.name, 'set ');

	prop = Object.getOwnPropertyDescriptor(o, emptyStrSym);
	assert.sameValue(prop.get.name, 'get []');
	assert.sameValue(prop.set.name, 'set []');

	prop = Object.getOwnPropertyDescriptor(o, namedSym);
	assert.sameValue(prop.get.name, 'get [test262]');
	assert.sameValue(prop.set.name, 'set [test262]');
	`
	testScriptWithTestLib(SCRIPT, _undefined, t)
}

func TestCoverFuncName(t *testing.T) {
	const SCRIPT = `
	var namedSym = Symbol('');
	var anonSym = Symbol();
	var o;

	o = {
	  xId: (0, function() {}),
	  id: (function() {}),
      id1: function x() {},
	  [anonSym]: (function() {}),
	  [namedSym]: (function() {})
	};

	assert(o.xId.name !== 'xId');
	assert.sameValue(o.id1.name, 'x'); 
	assert.sameValue(o.id.name, 'id', 'via IdentifierName');
	assert.sameValue(o[anonSym].name, '', 'via anonymous Symbol');
	assert.sameValue(o[namedSym].name, '[]', 'via Symbol');
	`
	testScriptWithTestLib(SCRIPT, _undefined, t)
}

func TestAnonFuncName(t *testing.T) {
	const SCRIPT = `
	const d = Object.getOwnPropertyDescriptor((function() {}), 'name');
	d !== undefined && d.value === '';
	`
	testScript(SCRIPT, valueTrue, t)
}

func TestStringToBytesConversion(t *testing.T) {
	vm := New()
	v := vm.ToValue("Test")
	var b []byte
	err := vm.ExportTo(v, &b)
	if err != nil {
		t.Fatal(err)
	}
	if string(b) != "Test" {
		t.Fatal(b)
	}
}

func TestPromiseAll(t *testing.T) {
	const SCRIPT = `
var p1 = new Promise(function() {});
var p2 = new Promise(function() {});
var p3 = new Promise(function() {});
var callCount = 0;
var currentThis = p1;
var nextThis = p2;
var afterNextThis = p3;

p1.then = p2.then = p3.then = function(a, b) {
  assert.sameValue(typeof a, 'function', 'type of first argument');
  assert.sameValue(
    a.length,
    1,
    'ES6 25.4.1.3.2: The length property of a promise resolve function is 1.'
  );
  assert.sameValue(typeof b, 'function', 'type of second argument');
  assert.sameValue(
    b.length,
    1,
    'ES6 25.4.1.3.1: The length property of a promise reject function is 1.'
  );
  assert.sameValue(arguments.length, 2, '"then"" invoked with two arguments');
  assert.sameValue(this, currentThis, '"this" value');

  currentThis = nextThis;
  nextThis = afterNextThis;
  afterNextThis = null;

  callCount += 1;
};

Promise.all([p1, p2, p3]);

assert.sameValue(callCount, 3, '"then"" invoked once for every iterated value');
	`
	testScriptWithTestLib(SCRIPT, _undefined, t)
}

func TestPromiseExport(t *testing.T) {
	vm := New()
	p, _, _ := vm.NewPromise()
	pv := vm.ToValue(p)
	if actual := pv.ExportType(); actual != reflect.TypeOf((*Promise)(nil)) {
		t.Fatalf("Export type: %v", actual)
	}

	if ev := pv.Export(); ev != p {
		t.Fatalf("Export value: %v", ev)
	}
}

func TestErrorStack(t *testing.T) {
	const SCRIPT = `
	const err = new Error("test");
	if (!("stack" in err)) {
		throw new Error("in");
	}
	if (Reflect.ownKeys(err)[0] !== "stack") {
		throw new Error("property order");
	}
	const stack = err.stack;
	if (stack !== "Error: test\n\tat test.js:2:14(3)\n") {
		throw new Error(stack);
	}
	delete err.stack;
	if ("stack" in err) {
		throw new Error("stack still in err after delete");
	}
	`
	testScript(SCRIPT, _undefined, t)
}

func TestErrorFormatSymbols(t *testing.T) {
	vm := New()
	vm.Set("a", func() (Value, error) { return nil, errors.New("something %s %f") })
	_, err := vm.RunString("a()")
	if err == nil {
		t.Fatal("expected error")
	}
	if !strings.Contains(err.Error(), "something %s %f") {
		t.Fatalf("Wrong value %q", err.Error())
	}
}

func TestPanicPassthrough(t *testing.T) {
	const panicString = "Test panic"
	r := New()
	r.Set("f", func() {
		panic(panicString)
	})
	defer func() {
		if x := recover(); x != nil {
			if x != panicString {
				t.Fatalf("Wrong panic value: %v", x)
			}
			if len(r.vm.callStack) > 0 {
				t.Fatal("vm.callStack is not empty")
			}
		} else {
			t.Fatal("No panic")
		}
	}()
	_, _ = r.RunString("f()")
	t.Fatal("Should not reach here")
}

func TestSuspendResumeRelStackLen(t *testing.T) {
	const SCRIPT = `
	async function f2() {
		throw new Error("test");
	}

	async function f1() {
		let a = [1];
		for (let i of a) {
			try {
				await f2();
			} catch {
				return true;
			}
		}
	}

	async function f() {
		let a = [1];
		for (let i of a) {
			return await f1();
		}
	}
	return f();
	`
	testAsyncFunc(SCRIPT, valueTrue, t)
}

func TestSuspendResumeStacks(t *testing.T) {
	const SCRIPT = `
async function f1() {
	throw new Error();
}
async function f() {
  try {
	await f1();
  } catch {}
}

result = await f();
	`
	testAsyncFunc(SCRIPT, _undefined, t)
}

func TestNestedTopLevelConstructorCall(t *testing.T) {
	r := New()
	c := func(call ConstructorCall, rt *Runtime) *Object {
		if _, err := rt.RunString("(5)"); err != nil {
			panic(err)
		}
		return nil
	}
	if err := r.Set("C", c); err != nil {
		panic(err)
	}
	if _, err := r.RunString("new C()"); err != nil {
		panic(err)
	}
}

func TestNestedTopLevelConstructorPanicAsync(t *testing.T) {
	r := New()
	c := func(call ConstructorCall, rt *Runtime) *Object {
		c, ok := AssertFunction(rt.ToValue(func() {}))
		if !ok {
			panic("wat")
		}
		if _, err := c(Undefined()); err != nil {
			panic(err)
		}
		return nil
	}
	if err := r.Set("C", c); err != nil {
		panic(err)
	}
	if _, err := r.RunString("new C()"); err != nil {
		panic(err)
	}
}

func TestAsyncFuncThrow(t *testing.T) {
	const SCRIPT = `
	class TestError extends Error {
	}

	async function f() {
		throw new TestError();
	}

	async function f1() {
		try {
			await f();
		} catch (e) {
			assert.sameValue(e.constructor.name, TestError.name);
			return;
		}
		throw new Error("No exception was thrown");
	}
	await f1();
	return undefined;
	`
	testAsyncFuncWithTestLib(SCRIPT, _undefined, t)
}

func TestAsyncStacktrace(t *testing.T) {
	// Do not reformat, assertions depend on the line and column numbers
	const SCRIPT = `
	let ex;
	async function foo(x) {
	  await bar(x);
	}

	async function bar(x) {
	  await x;
	  throw new Error("Let's have a look...");
	}

	try {
		await foo(1);
	} catch (e) {
		assertStack(e, [
			["test.js", "bar", 9, 10],
			["test.js", "foo", 4, 13],
			["test.js", "test", 13, 12],
		]);
	}
	`
	testAsyncFuncWithTestLibX(SCRIPT, _undefined, t)
}

func TestPanicPropagation(t *testing.T) {
	r := New()
	r.Set("doPanic", func() {
		panic(true)
	})
	v, err := r.RunString(`(function() {
		doPanic();
	})`)
	if err != nil {
		t.Fatal(err)
	}
	f, ok := AssertFunction(v)
	if !ok {
		t.Fatal("not a function")
	}
	defer func() {
		if x := recover(); x != nil {
			if x != true {
				t.Fatal("Invalid panic value")
			}
		}
	}()
	_, _ = f(nil)
	t.Fatal("Expected panic")
}

func TestAwaitInParameters(t *testing.T) {
	_, err := Compile("", `
	async function g() {
	    async function inner(a = 1 + await 1) {
	    }
	}
	`, false)
	if err == nil {
		t.Fatal("Expected error")
	}
}

func ExampleRuntime_ForOf() {
	r := New()
	v, err := r.RunString(`
		new Map().set("a", 1).set("b", 2);
	`)
	if err != nil {
		panic(err)
	}

	var sb strings.Builder
	ex := r.Try(func() {
		r.ForOf(v, func(v Value) bool {
			o := v.ToObject(r)
			key := o.Get("0")
			value := o.Get("1")

			sb.WriteString(key.String())
			sb.WriteString("=")
			sb.WriteString(value.String())
			sb.WriteString(",")

			return true
		})
	})
	if ex != nil {
		panic(ex)
	}
	fmt.Println(sb.String())
	// Output: a=1,b=2,
}

func TestDestructAssignToSymbol(t *testing.T) {
	const SCRIPT = `
	const s = Symbol('s');
	const target = {};

	({a: target[s]} = {a: 42});
	assert.sameValue(target[s], 42);
`
	testScriptWithTestLib(SCRIPT, _undefined, t)
}

func TestToNumber(t *testing.T) {
	const SCRIPT = `
	assert(isNaN(Number("+")));
	assert(isNaN(Number("++")));
	assert(isNaN(Number("-")));
	assert(isNaN(Number("0xfp1")));
	assert(isNaN(Number("0Xfp1")));
	assert(isNaN(Number("+0xfp1")));
	assert(isNaN(Number(" +0xfp1")));
	assert(isNaN(Number(" + 0xfp1")));
	assert(isNaN(Number(" 0xfp1")));
	assert(isNaN(Number("-0xfp1")));
	assert(isNaN(Number("- 0xfp1")));
	assert(isNaN(Number(" - 0xfp1")));
	assert.sameValue(Number("0."), 0);
	assert.sameValue(Number(" "), 0);
	assert.sameValue(Number(" Infinity"), Infinity);

	let a = [1];
	assert.sameValue(1, a.at("0xfp1"));
	assert.sameValue(1, a.at(" 0xfp1"));
	`
	testScriptWithTestLib(SCRIPT, _undefined, t)
}

func TestToValueNilBigInt(t *testing.T) {
	vm := New()
	vm.Set("n", (*big.Int)(nil))
	vm.testScript(`n === 0n`, valueTrue, t)
}

func TestClassReexport(t *testing.T) {
	vm := New()
	vm.Set("m", map[string]any{})
	vm.testScript(`
	class C {
		constructor() {
			this.test = true;
		}
	}
	m.C = C; // this does Export()
	const c = new m.C() // 'm.C' does ToValue()
	c instanceof C && c.test;
	`, valueTrue, t)
}

/*
func TestArrayConcatSparse(t *testing.T) {
function foo(a,b,c)
  {
    arguments[0] = 1; arguments[1] = 'str'; arguments[2] = 2.1;
    if(1 === a && 'str' === b && 2.1 === c)
      return true;
  }


	const SCRIPT = `
	var a1 = [];
	var a2 = [];
	a1[500000] = 1;
	a2[1000000] = 2;
	var a3 = a1.concat(a2);
	a3.length === 1500002 && a3[500000] === 1 && a3[1500001] == 2;
	`

	testScript(SCRIPT, valueTrue, t)
}
*/

func BenchmarkCallReflect(b *testing.B) {
	vm := New()
	vm.Set("f", func(v Value) {

	})

	prg := MustCompile("test.js", "f(null)", true)

	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		vm.RunProgram(prg)
	}
}

func BenchmarkCallNative(b *testing.B) {
	vm := New()
	vm.Set("f", func(call FunctionCall) (ret Value) {
		return
	})

	prg := MustCompile("test.js", "f(null)", true)

	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		vm.RunProgram(prg)
	}
}

func BenchmarkCallJS(b *testing.B) {
	vm := New()
	_, err := vm.RunString(`
	function f() {
		return 42;
	}
	`)

	if err != nil {
		b.Fatal(err)
	}

	prg := MustCompile("test.js", "f(null)", true)

	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		vm.RunProgram(prg)
	}
}

func BenchmarkMainLoop(b *testing.B) {
	vm := New()

	const SCRIPT = `
		for (var i=0; i<100000; i++) {
		}
	`

	prg := MustCompile("test.js", SCRIPT, true)

	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		vm.RunProgram(prg)
	}
}

func BenchmarkStringMapGet(b *testing.B) {
	m := make(map[string]Value)
	for i := 0; i < 100; i++ {
		m[strconv.Itoa(i)] = intToValue(int64(i))
	}
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		if m["50"] == nil {
			b.Fatal()
		}
	}
}

func BenchmarkValueStringMapGet(b *testing.B) {
	m := make(map[String]Value)
	for i := 0; i < 100; i++ {
		m[asciiString(strconv.Itoa(i))] = intToValue(int64(i))
	}
	b.ResetTimer()
	var key String = asciiString("50")
	for i := 0; i < b.N; i++ {
		if m[key] == nil {
			b.Fatal()
		}
	}
}

func BenchmarkAsciiStringMapGet(b *testing.B) {
	m := make(map[asciiString]Value)
	for i := 0; i < 100; i++ {
		m[asciiString(strconv.Itoa(i))] = intToValue(int64(i))
	}
	b.ResetTimer()
	var key = asciiString("50")
	for i := 0; i < b.N; i++ {
		if m[key] == nil {
			b.Fatal()
		}
	}
}

func BenchmarkNew(b *testing.B) {
	b.ReportAllocs()
	for i := 0; i < b.N; i++ {
		New()
	}
}
