package goja

import (
	"errors"
	"fmt"
	"math"
	"reflect"
	"strings"
	"testing"
	"time"
)

func TestGoReflectGet(t *testing.T) {
	const SCRIPT = `
	o.X + o.Y;
	`
	type O struct {
		X int
		Y string
	}
	r := New()
	o := O{X: 4, Y: "2"}
	r.Set("o", o)

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

	if s, ok := v.(String); ok {
		if s.String() != "42" {
			t.Fatalf("Unexpected string: %s", s)
		}
	} else {
		t.Fatalf("Unexpected type: %s", v)
	}
}

func TestGoReflectSet(t *testing.T) {
	const SCRIPT = `
	o.X++;
	o.Y += "P";
	`
	type O struct {
		X int
		Y string
	}
	r := New()
	o := O{X: 4, Y: "2"}
	r.Set("o", &o)

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

	if o.X != 5 {
		t.Fatalf("Unexpected X: %d", o.X)
	}

	if o.Y != "2P" {
		t.Fatalf("Unexpected Y: %s", o.Y)
	}

	r.Set("o", o)
	_, err = r.RunString(SCRIPT)
	if err != nil {
		t.Fatal(err)
	}

	if res, ok := r.Get("o").Export().(O); ok {
		if res.X != 6 {
			t.Fatalf("Unexpected res.X: %d", res.X)
		}

		if res.Y != "2PP" {
			t.Fatalf("Unexpected res.Y: %s", res.Y)
		}
	}
}

func TestGoReflectEnumerate(t *testing.T) {
	const SCRIPT = `
	var hasX = false;
	var hasY = false;
	for (var key in o) {
		switch (key) {
		case "X":
			if (hasX) {
				throw "Already have X";
			}
			hasX = true;
			break;
		case "Y":
			if (hasY) {
				throw "Already have Y";
			}
			hasY = true;
			break;
		default:
			throw "Unexpected property: " + key;
		}
	}
	hasX && hasY;
	`

	type S struct {
		X, Y int
	}

	r := New()
	r.Set("o", S{X: 40, Y: 2})
	v, err := r.RunString(SCRIPT)
	if err != nil {
		t.Fatal(err)
	}

	if !v.StrictEquals(valueTrue) {
		t.Fatalf("Expected true, got %v", v)
	}

}

func TestGoReflectCustomIntUnbox(t *testing.T) {
	const SCRIPT = `
	i + 2;
	`

	type CustomInt int
	var i CustomInt = 40

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

	if !v.StrictEquals(intToValue(42)) {
		t.Fatalf("Expected int 42, got %v", v)
	}
}

func TestGoReflectPreserveCustomType(t *testing.T) {
	const SCRIPT = `
	i;
	`

	type CustomInt int
	var i CustomInt = 42

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

	ve := v.Export()

	if ii, ok := ve.(CustomInt); ok {
		if ii != i {
			t.Fatalf("Wrong value: %v", ii)
		}
	} else {
		t.Fatalf("Wrong type: %v", ve)
	}
}

func TestGoReflectCustomIntValueOf(t *testing.T) {
	const SCRIPT = `
	if (i instanceof Number) {
		i.valueOf();
	} else {
		throw new Error("Value is not a number");
	}
	`

	type CustomInt int
	var i CustomInt = 42

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

	if !v.StrictEquals(intToValue(42)) {
		t.Fatalf("Expected int 42, got %v", v)
	}
}

func TestGoReflectEqual(t *testing.T) {
	const SCRIPT = `
	x === y;
	`

	type CustomInt int
	var x CustomInt = 42
	var y CustomInt = 42

	r := New()
	r.Set("x", x)
	r.Set("y", y)
	v, err := r.RunString(SCRIPT)
	if err != nil {
		t.Fatal(err)
	}

	if !v.StrictEquals(valueTrue) {
		t.Fatalf("Expected true, got %v", v)
	}
}

type testGoReflectMethod_O struct {
	field string
	Test  string
}

func (o testGoReflectMethod_O) Method(s string) string {
	return o.field + s
}

func TestGoReflectMethod(t *testing.T) {
	const SCRIPT = `
	o.Method(" 123")
	`

	o := testGoReflectMethod_O{
		field: "test",
	}

	r := New()
	r.Set("o", &o)
	v, err := r.RunString(SCRIPT)
	if err != nil {
		t.Fatal(err)
	}

	if !v.StrictEquals(asciiString("test 123")) {
		t.Fatalf("Expected 'test 123', got %v", v)
	}
}

func (o *testGoReflectMethod_O) Set(s string) {
	o.field = s
}

func (o *testGoReflectMethod_O) Get() string {
	return o.field
}

func TestGoReflectMethodPtr(t *testing.T) {
	const SCRIPT = `
	o.Set("42")
	o.Get()
	`

	o := testGoReflectMethod_O{
		field: "test",
	}

	r := New()
	r.Set("o", &o)
	v, err := r.RunString(SCRIPT)
	if err != nil {
		t.Fatal(err)
	}

	if !v.StrictEquals(asciiString("42")) {
		t.Fatalf("Expected '42', got %v", v)
	}
}

func (b *testBoolS) Method() bool {
	return bool(*b)
}

func TestGoReflectPtrMethodOnNonPtrValue(t *testing.T) {
	var o testGoReflectMethod_O
	o.Get()
	vm := New()
	vm.Set("o", o)
	_, err := vm.RunString(`o.Get()`)
	if err != nil {
		t.Fatal(err)
	}
	_, err = vm.RunString(`o.Method()`)
	if err != nil {
		t.Fatal(err)
	}

	var b testBoolS
	vm.Set("b", b)
	_, err = vm.RunString(`b.Method()`)
	if err != nil {
		t.Fatal(err)
	}
}

func TestGoReflectStructField(t *testing.T) {
	type S struct {
		F testGoReflectMethod_O
		B testBoolS
	}
	var s S
	vm := New()
	vm.Set("s", &s)

	const SCRIPT = `
	s.F.Set("Test");
	assert.sameValue(s.F.Method(""), "Test", "1");

	s.B = true;
	assert.sameValue(s.B.Method(), true, "2");

	assert.sameValue(s.B.toString(), "B", "3");
	`

	vm.testScriptWithTestLib(SCRIPT, _undefined, t)
}

func TestGoReflectProp(t *testing.T) {
	const SCRIPT = `
	var d1 = Object.getOwnPropertyDescriptor(o, "Get");
	var d2 = Object.getOwnPropertyDescriptor(o, "Test");
	!d1.writable && !d1.configurable && d2.writable && !d2.configurable;
	`

	o := testGoReflectMethod_O{
		field: "test",
	}

	r := New()
	r.Set("o", &o)
	v, err := r.RunString(SCRIPT)
	if err != nil {
		t.Fatal(err)
	}

	if !v.StrictEquals(valueTrue) {
		t.Fatalf("Expected true, got %v", v)
	}
}

func TestGoReflectRedefineFieldSuccess(t *testing.T) {
	const SCRIPT = `
	Object.defineProperty(o, "Test", {value: "AAA"}) === o;
	`

	o := testGoReflectMethod_O{}

	r := New()
	r.Set("o", &o)
	v, err := r.RunString(SCRIPT)
	if err != nil {
		t.Fatal(err)
	}

	if !v.StrictEquals(valueTrue) {
		t.Fatalf("Expected true, got %v", v)
	}

	if o.Test != "AAA" {
		t.Fatalf("Expected 'AAA', got '%s'", o.Test)
	}

}

func TestGoReflectRedefineFieldNonWritable(t *testing.T) {
	const SCRIPT = `
	var thrown = false;
	try {
		Object.defineProperty(o, "Test", {value: "AAA", writable: false});
	} catch (e) {
		if (e instanceof TypeError) {
			thrown = true;
		} else {
			throw e;
		}
	}
	thrown;
	`

	o := testGoReflectMethod_O{Test: "Test"}

	r := New()
	r.Set("o", &o)
	v, err := r.RunString(SCRIPT)
	if err != nil {
		t.Fatal(err)
	}

	if !v.StrictEquals(valueTrue) {
		t.Fatalf("Expected true, got %v", v)
	}

	if o.Test != "Test" {
		t.Fatalf("Expected 'Test', got: '%s'", o.Test)
	}
}

func TestGoReflectRedefineFieldConfigurable(t *testing.T) {
	const SCRIPT = `
	var thrown = false;
	try {
		Object.defineProperty(o, "Test", {value: "AAA", configurable: true});
	} catch (e) {
		if (e instanceof TypeError) {
			thrown = true;
		} else {
			throw e;
		}
	}
	thrown;
	`

	o := testGoReflectMethod_O{Test: "Test"}

	r := New()
	r.Set("o", &o)
	v, err := r.RunString(SCRIPT)
	if err != nil {
		t.Fatal(err)
	}

	if !v.StrictEquals(valueTrue) {
		t.Fatalf("Expected true, got %v", v)
	}

	if o.Test != "Test" {
		t.Fatalf("Expected 'Test', got: '%s'", o.Test)
	}
}

func TestGoReflectRedefineMethod(t *testing.T) {
	const SCRIPT = `
	var thrown = false;
	try {
		Object.defineProperty(o, "Method", {value: "AAA", configurable: true});
	} catch (e) {
		if (e instanceof TypeError) {
			thrown = true;
		} else {
			throw e;
		}
	}
	thrown;
	`

	o := testGoReflectMethod_O{Test: "Test"}

	r := New()
	r.Set("o", &o)
	v, err := r.RunString(SCRIPT)
	if err != nil {
		t.Fatal(err)
	}

	if !v.StrictEquals(valueTrue) {
		t.Fatalf("Expected true, got %v", v)
	}
}

func TestGoReflectEmbeddedStruct(t *testing.T) {
	const SCRIPT = `
	if (o.ParentField2 !== "ParentField2") {
		throw new Error("ParentField2 = " + o.ParentField2);
	}

	if (o.Parent.ParentField2 !== 2) {
		throw new Error("o.Parent.ParentField2 = " + o.Parent.ParentField2);
	}

	if (o.ParentField1 !== 1) {
		throw new Error("o.ParentField1 = " + o.ParentField1);

	}

	if (o.ChildField !== 3) {
		throw new Error("o.ChildField = " + o.ChildField);
	}

	var keys = {};
	for (var k in o) {
		if (keys[k]) {
			throw new Error("Duplicate key: " + k);
		}
		keys[k] = true;
	}

	var expectedKeys = ["ParentField2", "ParentField1", "Parent", "ChildField"];
	for (var i in expectedKeys) {
		if (!keys[expectedKeys[i]]) {
			throw new Error("Missing key in enumeration: " + expectedKeys[i]);
		}
		delete keys[expectedKeys[i]];
	}

	var remainingKeys = Object.keys(keys);
	if (remainingKeys.length > 0) {
		throw new Error("Unexpected keys: " + remainingKeys);
	}

	o.ParentField2 = "ParentField22";
	o.Parent.ParentField2 = 22;
	o.ParentField1 = 11;
	o.ChildField = 33;
	`

	type Parent struct {
		ParentField1 int
		ParentField2 int
	}

	type Child struct {
		ParentField2 string
		Parent
		ChildField int
	}

	vm := New()
	o := Child{
		Parent: Parent{
			ParentField1: 1,
			ParentField2: 2,
		},
		ParentField2: "ParentField2",
		ChildField:   3,
	}
	vm.Set("o", &o)

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

	if o.ParentField2 != "ParentField22" {
		t.Fatalf("ParentField2 = %q", o.ParentField2)
	}

	if o.Parent.ParentField2 != 22 {
		t.Fatalf("Parent.ParentField2 = %d", o.Parent.ParentField2)
	}

	if o.ParentField1 != 11 {
		t.Fatalf("ParentField1 = %d", o.ParentField1)
	}

	if o.ChildField != 33 {
		t.Fatalf("ChildField = %d", o.ChildField)
	}
}

type jsonTagNamer struct{}

func (jsonTagNamer) FieldName(_ reflect.Type, field reflect.StructField) string {
	if jsonTag := field.Tag.Get("json"); jsonTag != "" {
		return jsonTag
	}
	return field.Name
}

func (jsonTagNamer) MethodName(_ reflect.Type, method reflect.Method) string {
	return method.Name
}

func TestGoReflectCustomNaming(t *testing.T) {

	type testStructWithJsonTags struct {
		A string `json:"b"` // <-- script sees field "A" as property "b"
	}

	o := &testStructWithJsonTags{"Hello world"}
	r := New()
	r.SetFieldNameMapper(&jsonTagNamer{})
	r.Set("fn", func() *testStructWithJsonTags { return o })

	t.Run("get property", func(t *testing.T) {
		v, err := r.RunString(`fn().b`)
		if err != nil {
			t.Fatal(err)
		}
		if !v.StrictEquals(newStringValue(o.A)) {
			t.Fatalf("Expected %q, got %v", o.A, v)
		}
	})

	t.Run("set property", func(t *testing.T) {
		_, err := r.RunString(`fn().b = "Hello universe"`)
		if err != nil {
			t.Fatal(err)
		}
		if o.A != "Hello universe" {
			t.Fatalf("Expected \"Hello universe\", got %q", o.A)
		}
	})

	t.Run("enumerate properties", func(t *testing.T) {
		v, err := r.RunString(`Object.keys(fn())`)
		if err != nil {
			t.Fatal(err)
		}
		if !reflect.DeepEqual(v.Export(), []interface{}{"b"}) {
			t.Fatalf("Expected [\"b\"], got %v", v.Export())
		}
	})
}

func TestGoReflectCustomObjNaming(t *testing.T) {

	type testStructWithJsonTags struct {
		A string `json:"b"` // <-- script sees field "A" as property "b"
	}

	r := New()
	r.SetFieldNameMapper(&jsonTagNamer{})

	t.Run("Set object in slice", func(t *testing.T) {
		testSlice := &[]testStructWithJsonTags{{"Hello world"}}
		r.Set("testslice", testSlice)
		_, err := r.RunString(`testslice[0] = {b:"setted"}`)
		if err != nil {
			t.Fatal(err)
		}
		if (*testSlice)[0].A != "setted" {
			t.Fatalf("Expected \"setted\", got %q", (*testSlice)[0])
		}
	})

	t.Run("Set object in map", func(t *testing.T) {
		testMap := map[string]testStructWithJsonTags{"key": {"Hello world"}}
		r.Set("testmap", testMap)
		_, err := r.RunString(`testmap["key"] = {b:"setted"}`)
		if err != nil {
			t.Fatal(err)
		}
		if testMap["key"].A != "setted" {
			t.Fatalf("Expected \"setted\", got %q", testMap["key"])
		}
	})

	t.Run("Add object to map", func(t *testing.T) {
		testMap := map[string]testStructWithJsonTags{}
		r.Set("testmap", testMap)
		_, err := r.RunString(`testmap["newkey"] = {b:"setted"}`)
		if err != nil {
			t.Fatal(err)
		}
		if testMap["newkey"].A != "setted" {
			t.Fatalf("Expected \"setted\", got %q", testMap["newkey"])
		}
	})
}

type fieldNameMapper1 struct{}

func (fieldNameMapper1) FieldName(_ reflect.Type, f reflect.StructField) string {
	return strings.ToLower(f.Name)
}

func (fieldNameMapper1) MethodName(_ reflect.Type, m reflect.Method) string {
	return m.Name
}

func TestNonStructAnonFields(t *testing.T) {
	type Test1 struct {
		M bool
	}
	type test3 []int
	type Test4 []int
	type Test2 struct {
		test3
		Test4
		*Test1
	}

	const SCRIPT = `
	JSON.stringify(a);
	a.m && a.test3 === undefined && a.test4.length === 2
	`
	vm := New()
	vm.SetFieldNameMapper(fieldNameMapper1{})
	vm.Set("a", &Test2{Test1: &Test1{M: true}, Test4: []int{1, 2}, test3: nil})
	v, err := vm.RunString(SCRIPT)
	if err != nil {
		t.Fatal(err)
	}
	if !v.StrictEquals(valueTrue) {
		t.Fatalf("Unexepected result: %v", v)
	}
}

func TestStructNonAddressable(t *testing.T) {
	type S struct {
		Field int
	}

	const SCRIPT = `
	"use strict";
	
	if (!Object.getOwnPropertyDescriptor(s, "Field").writable) {
		throw new Error("s.Field is non-writable");
	}

	if (!Object.getOwnPropertyDescriptor(s1, "Field").writable) {
		throw new Error("s1.Field is non-writable");
	}

	s1.Field = 42;
	s.Field = 43;
	s;
`

	var s S
	vm := New()
	vm.Set("s", s)
	vm.Set("s1", &s)
	v, err := vm.RunString(SCRIPT)
	if err != nil {
		t.Fatal(err)
	}
	exp := v.Export()
	if s1, ok := exp.(S); ok {
		if s1.Field != 43 {
			t.Fatal(s1)
		}
	} else {
		t.Fatalf("Wrong type: %T", exp)
	}
	if s.Field != 42 {
		t.Fatalf("Unexpected s.Field value: %d", s.Field)
	}
}

type testFieldMapper struct {
}

func (testFieldMapper) FieldName(_ reflect.Type, f reflect.StructField) string {
	if tag := f.Tag.Get("js"); tag != "" {
		if tag == "-" {
			return ""
		}
		return tag
	}

	return f.Name
}

func (testFieldMapper) MethodName(_ reflect.Type, m reflect.Method) string {
	return m.Name
}

func TestHidingAnonField(t *testing.T) {
	type InnerType struct {
		AnotherField string
	}

	type OuterType struct {
		InnerType `js:"-"`
		SomeField string
	}

	const SCRIPT = `
	var a = Object.getOwnPropertyNames(o);
	if (a.length !== 2) {
		throw new Error("unexpected length: " + a.length);
	}

	if (a.indexOf("SomeField") === -1) {
		throw new Error("no SomeField");
	}

	if (a.indexOf("AnotherField") === -1) {
		throw new Error("no SomeField");
	}
	`

	var o OuterType

	vm := New()
	vm.SetFieldNameMapper(testFieldMapper{})
	vm.Set("o", &o)

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

func TestFieldOverriding(t *testing.T) {
	type InnerType struct {
		AnotherField  string
		AnotherField1 string
	}

	type OuterType struct {
		InnerType     `js:"-"`
		SomeField     string
		AnotherField  string `js:"-"`
		AnotherField1 string
	}

	const SCRIPT = `
	if (o.SomeField !== "SomeField") {
		throw new Error("SomeField");
	}

	if (o.AnotherField !== "AnotherField inner") {
		throw new Error("AnotherField");
	}

	if (o.AnotherField1 !== "AnotherField1 outer") {
		throw new Error("AnotherField1");
	}

	if (o.InnerType) {
		throw new Error("InnerType is present");
	}
	`

	o := OuterType{
		InnerType: InnerType{
			AnotherField:  "AnotherField inner",
			AnotherField1: "AnotherField1 inner",
		},
		SomeField:     "SomeField",
		AnotherField:  "AnotherField outer",
		AnotherField1: "AnotherField1 outer",
	}

	vm := New()
	vm.SetFieldNameMapper(testFieldMapper{})
	vm.Set("o", &o)

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

func TestDefinePropertyUnexportedJsName(t *testing.T) {
	type T struct {
		Field      int
		unexported int
	}

	vm := New()
	vm.SetFieldNameMapper(fieldNameMapper1{})
	vm.Set("f", &T{unexported: 0})

	_, err := vm.RunString(`
	"use strict";
	Object.defineProperty(f, "field", {value: 42});
	if (f.field !== 42) {
		throw new Error("Unexpected value: " + f.field);
	}
	if (f.hasOwnProperty("unexported")) {
		throw new Error("hasOwnProperty('unexported') is true");
	}
	var thrown;
	try {
		Object.defineProperty(f, "unexported", {value: 1});
	} catch (e) {
		thrown = e;
	}
	if (!(thrown instanceof TypeError)) {
		throw new Error("Unexpected error: ", thrown);
	}
	`)
	if err != nil {
		t.Fatal(err)
	}
}

type fieldNameMapperToLower struct{}

func (fieldNameMapperToLower) FieldName(_ reflect.Type, f reflect.StructField) string {
	return strings.ToLower(f.Name)
}

func (fieldNameMapperToLower) MethodName(_ reflect.Type, m reflect.Method) string {
	return strings.ToLower(m.Name)
}

func TestHasOwnPropertyUnexportedJsName(t *testing.T) {
	vm := New()
	vm.SetFieldNameMapper(fieldNameMapperToLower{})
	vm.Set("f", &testGoReflectMethod_O{})

	_, err := vm.RunString(`
	"use strict";
	if (!f.hasOwnProperty("test")) {
		throw new Error("hasOwnProperty('test') returned false");
	}
	if (!f.hasOwnProperty("method")) {
		throw new Error("hasOwnProperty('method') returned false");
	}
	`)
	if err != nil {
		t.Fatal(err)
	}
}

func BenchmarkGoReflectGet(b *testing.B) {
	type parent struct {
		field, Test1, Test2, Test3, Test4, Test5, Test string
	}

	type child struct {
		parent
		Test6 string
	}

	b.StopTimer()
	vm := New()

	b.StartTimer()
	for i := 0; i < b.N; i++ {
		v := vm.ToValue(child{parent: parent{Test: "Test", field: ""}}).(*Object)
		v.Get("Test")
	}
}

func TestNestedStructSet(t *testing.T) {
	type B struct {
		Field int
	}
	type A struct {
		B B
	}

	const SCRIPT = `
	'use strict';
	a.B.Field++;
	if (a1.B.Field != 1) {
		throw new Error("a1.B.Field = " + a1.B.Field);
	}
	var d = Object.getOwnPropertyDescriptor(a1.B, "Field");
	if (!d.writable) {
		throw new Error("a1.B is not writable");
	}
	a1.B.Field = 42;
	a1;
	`
	a := A{
		B: B{
			Field: 1,
		},
	}
	vm := New()
	vm.Set("a", &a)
	vm.Set("a1", a)
	v, err := vm.RunString(SCRIPT)
	if err != nil {
		t.Fatal(err)
	}
	exp := v.Export()
	if v, ok := exp.(A); ok {
		if v.B.Field != 42 {
			t.Fatal(v)
		}
	} else {
		t.Fatalf("Wrong type: %T", exp)
	}

	if v := a.B.Field; v != 2 {
		t.Fatalf("Unexpected a.B.Field: %d", v)
	}
}

func TestStructNonAddressableAnonStruct(t *testing.T) {

	type C struct {
		Z int64
		X string
	}

	type B struct {
		C
		Y string
	}

	type A struct {
		B B
	}

	a := A{
		B: B{
			C: C{
				Z: 1,
				X: "X2",
			},
			Y: "Y3",
		},
	}
	const SCRIPT = `
	"use strict";
	var s = JSON.stringify(a);
	s;
`

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

	expected := `{"B":{"C":{"Z":1,"X":"X2"},"Z":1,"X":"X2","Y":"Y3"}}`
	if expected != v.String() {
		t.Fatalf("Expected '%s', got '%s'", expected, v.String())
	}

}

func TestTagFieldNameMapperInvalidId(t *testing.T) {
	vm := New()
	vm.SetFieldNameMapper(TagFieldNameMapper("json", true))
	type S struct {
		Field int `json:"-"`
	}
	vm.Set("s", S{Field: 42})
	res, err := vm.RunString(`s.hasOwnProperty("field") || s.hasOwnProperty("Field")`)
	if err != nil {
		t.Fatal(err)
	}
	if res != valueFalse {
		t.Fatalf("Unexpected result: %v", res)
	}
}

func TestPrimitivePtr(t *testing.T) {
	vm := New()
	s := "test"
	vm.Set("s", &s)
	res, err := vm.RunString(`s instanceof String && s == "test"`) // note non-strict equality
	if err != nil {
		t.Fatal(err)
	}
	if v := res.ToBoolean(); !v {
		t.Fatalf("value: %#v", res)
	}
	s = "test1"
	res, err = vm.RunString(`s == "test1"`)
	if err != nil {
		t.Fatal(err)
	}
	if v := res.ToBoolean(); !v {
		t.Fatalf("value: %#v", res)
	}
}

func TestStringer(t *testing.T) {
	vm := New()
	vm.Set("e", errors.New("test"))
	res, err := vm.RunString("e.toString()")
	if err != nil {
		t.Fatal(err)
	}
	if v := res.Export(); v != "test" {
		t.Fatalf("v: %v", v)
	}
}

func ExampleTagFieldNameMapper() {
	vm := New()
	vm.SetFieldNameMapper(TagFieldNameMapper("json", true))
	type S struct {
		Field int `json:"field"`
	}
	vm.Set("s", S{Field: 42})
	res, _ := vm.RunString(`s.field`)
	fmt.Println(res.Export())
	// Output: 42
}

func ExampleUncapFieldNameMapper() {
	vm := New()
	s := testGoReflectMethod_O{
		Test: "passed",
	}
	vm.SetFieldNameMapper(UncapFieldNameMapper())
	vm.Set("s", s)
	res, _ := vm.RunString(`s.test + " and " + s.method("passed too")`)
	fmt.Println(res.Export())
	// Output: passed and passed too
}

func TestGoReflectWithProto(t *testing.T) {
	type S struct {
		Field int
	}
	var s S
	vm := New()
	vm.Set("s", &s)
	vm.testScriptWithTestLib(`
	(function() {
	'use strict';
	var proto = {
		Field: "protoField",
		test: 42
	};
	var test1Holder;
	Object.defineProperty(proto, "test1", {
		set: function(v) {
			test1Holder = v;
		},
		get: function() {
			return test1Holder;
		}
	});
	Object.setPrototypeOf(s, proto);
	assert.sameValue(s.Field, 0, "s.Field");
	s.Field = 2;
	assert.sameValue(s.Field, 2, "s.Field");
	assert.sameValue(s.test, 42, "s.test");
	assert.throws(TypeError, function() {
		Object.defineProperty(s, "test", {value: 43});
	});
	test1Holder = 1;
	assert.sameValue(s.test1, 1, "s.test1");
	s.test1 = 2;
	assert.sameValue(test1Holder, 2, "test1Holder");
	})();
	`, _undefined, t)
}

func TestGoReflectSymbols(t *testing.T) {
	type S struct {
		Field int
	}
	var s S
	vm := New()
	vm.Set("s", &s)
	_, err := vm.RunString(`
	'use strict';
	var sym = Symbol(66);
	s[sym] = "Test";
	if (s[sym] !== "Test") {
		throw new Error("s[sym]=" + s[sym]);
	}
	`)
	if err != nil {
		t.Fatal(err)
	}
}

func TestGoReflectSymbolEqualityQuirk(t *testing.T) {
	type Field struct {
	}
	type S struct {
		Field *Field
	}
	var s = S{
		Field: &Field{},
	}
	vm := New()
	vm.Set("s", &s)
	res, err := vm.RunString(`
	var sym = Symbol(66);
	var field1 = s.Field;
	field1[sym] = true;
	var field2 = s.Field;
	// Because a wrapper is created every time the property is accessed
	// field1 and field2 will be different instances of the wrapper.
	// Symbol properties only exist in the wrapper, they cannot be placed into the original Go value,
	// hence the following:
	field1 === field2 && field1[sym] === true && field2[sym] === undefined;
	`)
	if err != nil {
		t.Fatal(err)
	}
	if res != valueTrue {
		t.Fatal(res)
	}
}

func TestGoObj__Proto__(t *testing.T) {
	type S struct {
		Field int
	}
	vm := New()
	vm.Set("s", S{})
	vm.Set("m", map[string]interface{}{})
	vm.Set("mr", map[int]string{})
	vm.Set("a", []interface{}{})
	vm.Set("ar", []string{})
	_, err := vm.RunString(`
	function f(s, expectedCtor, prefix) {
		if (s.__proto__ !== expectedCtor.prototype) {
			throw new Error(prefix + ": __proto__: " + s.__proto__);
		}
		s.__proto__ = null;
		if (s.__proto__ !== undefined) { // as there is no longer a prototype, there is no longer the __proto__ property
			throw new Error(prefix + ": __proto__ is not undefined: " + s.__proto__);
		}
		var proto = Object.getPrototypeOf(s);
		if (proto !== null) {
			throw new Error(prefix + ": proto is not null: " + proto);
		}
	}
	f(s, Object, "struct");
	f(m, Object, "simple map");
	f(mr, Object, "reflect map");
	f(a, Array, "slice");
	f(ar, Array, "reflect slice");
	`)
	if err != nil {
		t.Fatal(err)
	}
}

func TestGoReflectUnicodeProps(t *testing.T) {
	type S struct {
		Тест string
	}
	vm := New()
	var s S
	vm.Set("s", &s)
	_, err := vm.RunString(`
	if (!s.hasOwnProperty("Тест")) {
		throw new Error("hasOwnProperty");
	}
	`)
	if err != nil {
		t.Fatal(err)
	}
}

func TestGoReflectPreserveType(t *testing.T) {
	vm := New()
	var expect = time.Duration(math.MaxInt64)
	vm.Set(`make`, func() time.Duration {
		return expect
	})
	vm.Set(`handle`, func(d time.Duration) {
		if d.String() != expect.String() {
			t.Fatal(`expect`, expect, `, but get`, d)
		}
	})
	_, e := vm.RunString(`
	var d=make()
	handle(d)
	`)
	if e != nil {
		t.Fatal(e)
	}
}

func TestGoReflectCopyOnWrite(t *testing.T) {
	type Inner struct {
		Field int
	}
	type S struct {
		I Inner
	}
	var s S
	s.I.Field = 1

	vm := New()
	vm.Set("s", &s)
	_, err := vm.RunString(`
		if (s.I.Field !== 1) {
			throw new Error("s.I.Field: " + s.I.Field);
		}

		let tmp = s.I; // tmp becomes a reference to s.I
		if (tmp.Field !== 1) {
			throw new Error("tmp.Field: " + tmp.Field);
		}

		s.I.Field = 2;
		if (s.I.Field !== 2) {
			throw new Error("s.I.Field (1): " + s.I.Field);
		}
		if (tmp.Field !== 2) {
			throw new Error("tmp.Field (1): " + tmp.Field);
		}

		s.I = {Field: 3}; // at this point tmp is changed to a copy
		if (s.I.Field !== 3) {
			throw new Error("s.I.Field (2): " + s.I.Field);
		}
		if (tmp.Field !== 2) {
			throw new Error("tmp.Field (2): " + tmp.Field);
		}
	`)

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

func TestReflectSetReflectValue(t *testing.T) {
	o := []testGoReflectMethod_O{{}}
	vm := New()
	vm.Set("o", o)
	_, err := vm.RunString(`
		const t = o[0];
		t.Set("a");
		o[0] = {};
		o[0].Set("b");
		if (t.Get() !== "a") {
			throw new Error();
		}
	`)

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

func TestReflectOverwriteReflectMap(t *testing.T) {
	vm := New()
	type S struct {
		M map[int]interface{}
	}
	var s S
	s.M = map[int]interface{}{
		0: true,
	}
	vm.Set("s", &s)
	_, err := vm.RunString(`
	s.M = {1: false};
	`)
	if err != nil {
		t.Fatal(err)
	}
	if _, exists := s.M[0]; exists {
		t.Fatal(s)
	}
}

type testBoolS bool

func (testBoolS) String() string {
	return "B"
}

type testIntS int

func (testIntS) String() string {
	return "I"
}

type testStringS string

func (testStringS) String() string {
	return "S"
}

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

	f := func(expr string, expected Value, t *testing.T) {
		v, err := vm.RunString(expr)
		if err != nil {
			t.Fatal(err)
		}
		if IsNaN(expected) {
			if IsNaN(v) {
				return
			}
		} else {
			if v.StrictEquals(expected) {
				return
			}
		}
		t.Fatalf("%s: expected: %v, actual: %v", expr, expected, v)
	}

	t.Run("Not Stringers", func(t *testing.T) {
		type Bool bool
		var b Bool = true

		t.Run("Bool", func(t *testing.T) {
			vm.Set("b", b)
			f("+b", intToValue(1), t)
			f("`${b}`", asciiString("true"), t)
			f("b.toString()", asciiString("true"), t)
			f("b.valueOf()", valueTrue, t)
		})

		t.Run("*Bool", func(t *testing.T) {
			vm.Set("b", &b)
			f("+b", intToValue(1), t)
			f("`${b}`", asciiString("true"), t)
			f("b.toString()", asciiString("true"), t)
			f("b.valueOf()", valueTrue, t)
		})

		type Int int
		var i Int = 1

		t.Run("Int", func(t *testing.T) {
			vm.Set("i", i)
			f("+i", intToValue(1), t)
			f("`${i}`", asciiString("1"), t)
			f("i.toString()", asciiString("1"), t)
			f("i.valueOf()", intToValue(1), t)
		})

		t.Run("*Int", func(t *testing.T) {
			vm.Set("i", &i)
			f("+i", intToValue(1), t)
			f("`${i}`", asciiString("1"), t)
			f("i.toString()", asciiString("1"), t)
			f("i.valueOf()", intToValue(1), t)
		})

		type Uint uint
		var ui Uint = 1

		t.Run("Uint", func(t *testing.T) {
			vm.Set("ui", ui)
			f("+ui", intToValue(1), t)
			f("`${ui}`", asciiString("1"), t)
			f("ui.toString()", asciiString("1"), t)
			f("ui.valueOf()", intToValue(1), t)
		})

		t.Run("*Uint", func(t *testing.T) {
			vm.Set("ui", &i)
			f("+ui", intToValue(1), t)
			f("`${ui}`", asciiString("1"), t)
			f("ui.toString()", asciiString("1"), t)
			f("ui.valueOf()", intToValue(1), t)
		})

		type Float float64
		var fl Float = 1.1

		t.Run("Float", func(t *testing.T) {
			vm.Set("fl", fl)
			f("+fl", floatToValue(1.1), t)
			f("`${fl}`", asciiString("1.1"), t)
			f("fl.toString()", asciiString("1.1"), t)
			f("fl.valueOf()", floatToValue(1.1), t)
		})

		t.Run("*Float", func(t *testing.T) {
			vm.Set("fl", &fl)
			f("+fl", floatToValue(1.1), t)
			f("`${fl}`", asciiString("1.1"), t)
			f("fl.toString()", asciiString("1.1"), t)
			f("fl.valueOf()", floatToValue(1.1), t)
		})

		fl = Float(math.Inf(1))
		t.Run("FloatInf", func(t *testing.T) {
			vm.Set("fl", fl)
			f("+fl", _positiveInf, t)
			f("fl.toString()", asciiString("Infinity"), t)
		})

		type Empty struct{}

		var e Empty
		t.Run("Empty", func(t *testing.T) {
			vm.Set("e", &e)
			f("+e", _NaN, t)
			f("`${e}`", asciiString("[object Object]"), t)
			f("e.toString()", asciiString("[object Object]"), t)
			f("e.valueOf()", vm.ToValue(&e), t)
		})
	})

	t.Run("Stringers", func(t *testing.T) {
		var b testBoolS = true
		t.Run("Bool", func(t *testing.T) {
			vm.Set("b", b)
			f("`${b}`", asciiString("B"), t)
			f("b.toString()", asciiString("B"), t)
			f("b.valueOf()", valueTrue, t)
			f("+b", intToValue(1), t)
		})

		t.Run("*Bool", func(t *testing.T) {
			vm.Set("b", &b)
			f("`${b}`", asciiString("B"), t)
			f("b.toString()", asciiString("B"), t)
			f("b.valueOf()", valueTrue, t)
			f("+b", intToValue(1), t)
		})

		var i testIntS = 1
		t.Run("Int", func(t *testing.T) {
			vm.Set("i", i)
			f("`${i}`", asciiString("I"), t)
			f("i.toString()", asciiString("I"), t)
			f("i.valueOf()", intToValue(1), t)
			f("+i", intToValue(1), t)
		})

		t.Run("*Int", func(t *testing.T) {
			vm.Set("i", &i)
			f("`${i}`", asciiString("I"), t)
			f("i.toString()", asciiString("I"), t)
			f("i.valueOf()", intToValue(1), t)
			f("+i", intToValue(1), t)
		})

		var s testStringS
		t.Run("String", func(t *testing.T) {
			vm.Set("s", s)
			f("`${s}`", asciiString("S"), t)
			f("s.toString()", asciiString("S"), t)
			f("s.valueOf()", asciiString("S"), t)
			f("+s", _NaN, t)
		})

		t.Run("*String", func(t *testing.T) {
			vm.Set("s", &s)
			f("`${s}`", asciiString("S"), t)
			f("s.toString()", asciiString("S"), t)
			f("s.valueOf()", asciiString("S"), t)
			f("+s", _NaN, t)
		})
	})
}

type testGoReflectFuncRt struct {
}

func (*testGoReflectFuncRt) M(call FunctionCall, r *Runtime) Value {
	if r == nil {
		panic(typeError("Runtime is nil"))
	}
	return call.Argument(0)
}

func (*testGoReflectFuncRt) C(call ConstructorCall, r *Runtime) *Object {
	if r == nil {
		panic(typeError("Runtime is nil in constructor"))
	}
	call.This.Set("r", call.Argument(0))
	return nil
}

func TestGoReflectFuncWithRuntime(t *testing.T) {
	vm := New()
	var s testGoReflectFuncRt
	vm.Set("s", &s)
	res, err := vm.RunString("s.M(true)")
	if err != nil {
		t.Fatal(err)
	}
	if res != valueTrue {
		t.Fatal(res)
	}

	res, err = vm.RunString("new s.C(true).r")
	if err != nil {
		t.Fatal(err)
	}
	if res != valueTrue {
		t.Fatal(res)
	}
}

func TestGoReflectDefaultToString(t *testing.T) {
	var s testStringS
	vm := New()
	v := vm.ToValue(s).(*Object)
	v.Delete("toString")
	v.Delete("valueOf")
	vm.Set("s", v)
	_, err := vm.RunString(`
		class S {
			toString() {
				return "X";
			}
		}

		if (s.toString() !== "S") {
			throw new Error(s.toString());
		}
		if (("" + s) !== "S") {
			throw new Error("" + s);
		}

		Object.setPrototypeOf(s, S.prototype);
		if (s.toString() !== "X") {
			throw new Error(s.toString());
		}
		if (("" + s) !== "X") {
			throw new Error("" + s);
		}
	`)
	if err != nil {
		t.Fatal(err)
	}
}

func TestGoReflectUnexportedEmbedStruct(t *testing.T) {
	type privateEmbed struct {
		A string
	}
	type PublicEmbed struct {
		B string
	}
	type privateNested struct {
		C string
	}
	type PublicNested struct {
		D string
	}
	type Foo struct {
		privateEmbed
		PublicEmbed

		privateNested privateNested
		PublicNested  PublicNested

		e string
		F string
	}

	vm := New()
	vm.Set("foo", Foo{
		privateEmbed:  privateEmbed{A: "testA"},
		PublicEmbed:   PublicEmbed{B: "testB"},
		privateNested: privateNested{C: "testC"},
		PublicNested:  PublicNested{D: "testD"},
		e:             "testE",
		F:             "testF",
	})

	scenarios := []struct {
		expr     string
		expected string
	}{
		{"foo.privateEmbed", "undefined"},
		{"foo.A", "testA"},
		// ---
		{"foo.PublicEmbed", "[object Object]"},
		{"foo.B", "testB"},
		{"foo.PublicEmbed.B", "testB"},
		// ---
		{"foo.privateNested", "undefined"},
		{"foo.C", "undefined"},
		// ---
		{"foo.PublicNested", "[object Object]"},
		{"foo.D", "undefined"},
		{"foo.PublicNested.D", "testD"},
		// ---
		{"foo.e", "undefined"},
		{"foo.F", "testF"},
	}

	for _, s := range scenarios {
		t.Run(s.expr, func(t *testing.T) {
			v, err := vm.RunString(s.expr)
			if err != nil {
				t.Fatal(err)
			}

			vStr := v.String()

			if vStr != s.expected {
				t.Fatalf("Expected %q, got %q", s.expected, vStr)
			}
		})
	}
}

func TestNestedSliceAddr(t *testing.T) {
	type document struct {
		Items []any
	}

	var d document
	runtime := New()
	runtime.Set("d", &d)

	_, err := runtime.RunString(`
	d.Items.push("Hello");
	`)
	if err != nil {
		t.Fatal(err)
	}
	if len(d.Items) != 1 || d.Items[0] != "Hello" {
		t.Fatal(d.Items)
	}
}
