package otto

import (
	"bytes"
	"encoding/json"
	"fmt"
	"math"
	"reflect"
	"strings"
	"testing"

	"github.com/stretchr/testify/require"
)

type abcStruct struct {
	Jkl interface{}
	Pqr map[string]int8
	Ghi string
	Mno _mnoStruct
	Def int
	Abc bool
}

func (abc abcStruct) String() string {
	return abc.Ghi
}

func (abc *abcStruct) FuncPointer() string {
	return "abc"
}

func (abc abcStruct) Func() {
}

func (abc abcStruct) FuncReturn1() string {
	return "abc"
}

func (abc abcStruct) FuncReturn2() (string, error) {
	return "def", nil
}

func (abc abcStruct) Func1Return1(a string) string {
	return a
}

func (abc abcStruct) Func2Return1(x, y string) string {
	return x + y
}

func (abc abcStruct) FuncEllipsis(xyz ...string) int {
	return len(xyz)
}

func (abc abcStruct) FuncReturnStruct() _mnoStruct {
	return _mnoStruct{}
}

func (abc abcStruct) Func1Int(i int) int {
	return i + 1
}

func (abc abcStruct) Func1Int8(i int8) int8 {
	return i + 1
}

func (abc abcStruct) Func1Int16(i int16) int16 {
	return i + 1
}

func (abc abcStruct) Func1Int32(i int32) int32 {
	return i + 1
}

func (abc abcStruct) Func1Int64(i int64) int64 {
	return i + 1
}

func (abc abcStruct) Func1Uint(i uint) uint {
	return i + 1
}

func (abc abcStruct) Func1Uint8(i uint8) uint8 {
	return i + 1
}

func (abc abcStruct) Func1Uint16(i uint16) uint16 {
	return i + 1
}

func (abc abcStruct) Func1Uint32(i uint32) uint32 {
	return i + 1
}

func (abc abcStruct) Func1Uint64(i uint64) uint64 {
	return i + 1
}

func (abc abcStruct) Func2Int(i, j int) int {
	return i + j
}

func (abc abcStruct) Func2StringInt(s string, i int) string {
	return fmt.Sprintf("%v:%v", s, i)
}

func (abc abcStruct) Func1IntVariadic(a ...int) int {
	t := 0
	for _, i := range a {
		t += i
	}
	return t
}

func (abc abcStruct) Func2IntVariadic(s string, a ...int) string {
	t := 0
	for _, i := range a {
		t += i
	}
	return fmt.Sprintf("%v:%v", s, t)
}

func (abc abcStruct) Func2IntArrayVariadic(s string, a ...[]int) string {
	t := 0
	for _, i := range a {
		for _, j := range i {
			t += j
		}
	}
	return fmt.Sprintf("%v:%v", s, t)
}

type _mnoStruct struct {
	Ghi string
}

func (mno _mnoStruct) Func() string {
	return "mno"
}

func TestReflect(t *testing.T) {
	tt(t, func() {
		// Testing dbgf
		// These should panic
		str := "test"
		require.Panics(t, func() {
			_, err := toValue("Xyzzy").toReflectValue(reflect.ValueOf(&str).Type())
			require.NoError(t, err)
		})
		require.Panics(t, func() {
			_, err := stringToReflectValue("Xyzzy", reflect.Ptr)
			require.NoError(t, err)
		})
	})
}

func Test_reflectStruct(t *testing.T) {
	tt(t, func() {
		test, vm := test()

		// _abcStruct
		{
			abc := &abcStruct{}
			vm.Set("abc", abc)

			test(`
                [ abc.Abc, abc.Ghi ];
            `, "false,")

			abc.Abc = true
			abc.Ghi = "Nothing happens."

			test(`
                [ abc.Abc, abc.Ghi ];
            `, "true,Nothing happens.")

			*abc = abcStruct{}

			test(`
                [ abc.Abc, abc.Ghi ];
            `, "false,")

			abc.Abc = true
			abc.Ghi = "Xyzzy"
			vm.Set("abc", abc)

			test(`
                [ abc.Abc, abc.Ghi ];
            `, "true,Xyzzy")

			is(abc.Abc, true)
			test(`
                abc.Abc = false;
                abc.Def = 451;
                abc.Ghi = "Nothing happens.";
                abc.abc = "Something happens.";
                [ abc.Def, abc.abc ];
            `, "451,Something happens.")
			is(abc.Abc, false)
			is(abc.Def, 451)
			is(abc.Ghi, "Nothing happens.")

			test(`
                delete abc.Def;
                delete abc.abc;
                [ abc.Def, abc.abc ];
            `, "451,")
			is(abc.Def, 451)

			test(`
                abc.FuncPointer();
            `, "abc")

			test(`
                abc.Func();
            `, "undefined")

			test(`
                abc.FuncReturn1();
            `, "abc")

			test(`
                abc.Func1Return1("abc");
            `, "abc")

			test(`
                abc.Func2Return1("abc", "def");
            `, "abcdef")

			test(`
                abc.FuncEllipsis("abc", "def", "ghi");
            `, 3)

			test(`
                ret = abc.FuncReturn2();
                if (ret && ret.length && ret.length == 2 && ret[0] == "def" && ret[1] === undefined) {
                        true;
                } else {
                       false;
                }
            `, true)

			test(`
                abc.FuncReturnStruct();
            `, "[object Object]")

			test(`
                abc.FuncReturnStruct().Func();
            `, "mno")

			test(`
                abc.Func1Int(1);
            `, 2)

			test(`
                abc.Func1Int(0x01 & 0x01);
            `, 2)

			test(`raise:
                abc.Func1Int(1.1);
            `, "RangeError: converting float64 to int would cause loss of precision")

			test(`
		var v = 1;
                abc.Func1Int(v + 1);
            `, 3)

			test(`
                abc.Func2Int(1, 2);
            `, 3)

			test(`
                abc.Func1Int8(1);
            `, 2)

			test(`
                abc.Func1Int16(1);
            `, 2)

			test(`
                abc.Func1Int32(1);
            `, 2)

			test(`
                abc.Func1Int64(1);
            `, 2)

			test(`
                abc.Func1Uint(1);
            `, 2)

			test(`
                abc.Func1Uint8(1);
            `, 2)

			test(`
                abc.Func1Uint16(1);
            `, 2)

			test(`
                abc.Func1Uint32(1);
            `, 2)

			test(`
                abc.Func1Uint64(1);
            `, 2)

			test(`
                abc.Func2StringInt("test", 1);
            `, "test:1")

			test(`
                abc.Func1IntVariadic(1, 2);
            `, 3)

			test(`
                abc.Func2IntVariadic("test", 1, 2);
            `, "test:3")

			test(`
                abc.Func2IntVariadic("test", [1, 2]);
            `, "test:3")

			test(`
                abc.Func2IntArrayVariadic("test", [1, 2]);
            `, "test:3")

			test(`
                abc.Func2IntArrayVariadic("test", [1, 2], [3, 4]);
            `, "test:10")

			test(`
                abc.Func2IntArrayVariadic("test", [[1, 2], [3, 4]]);
            `, "test:10")
		}
	})
}

func Test_reflectMap(t *testing.T) {
	tt(t, func() {
		test, vm := test()

		// map[string]string
		{
			abc := map[string]string{
				"Xyzzy": "Nothing happens.",
				"def":   "1",
			}
			vm.Set("abc", abc)

			test(`
                abc.xyz = "pqr";
                [ abc.Xyzzy, abc.def, abc.ghi ];
            `, "Nothing happens.,1,")

			is(abc["xyz"], "pqr")
		}

		// map[string]float64
		{
			abc := map[string]float64{
				"Xyzzy": math.Pi,
				"def":   1,
			}
			vm.Set("abc", abc)

			test(`
                abc.xyz = "pqr";
                abc.jkl = 10;
                [ abc.Xyzzy, abc.def, abc.ghi ];
            `, "3.141592653589793,1,")

			is(abc["xyz"], math.NaN())
			is(abc["jkl"], float64(10))
		}

		// map[string]int32
		{
			abc := map[string]int32{
				"Xyzzy": 3,
				"def":   1,
			}
			vm.Set("abc", abc)

			test(`
                abc.xyz = "pqr";
                abc.jkl = 10;
                [ abc.Xyzzy, abc.def, abc.ghi ];
            `, "3,1,")

			is(abc["xyz"], 0)
			is(abc["jkl"], int32(10))

			test(`
                delete abc["Xyzzy"];
            `)

			_, exists := abc["Xyzzy"]
			is(exists, false)
			is(abc["Xyzzy"], 0)
		}

		// map[int32]string
		{
			abc := map[int32]string{
				0: "abc",
				1: "def",
			}
			vm.Set("abc", abc)

			test(`
                abc[2] = "pqr";
                //abc.jkl = 10;
                abc[3] = 10;
                [ abc[0], abc[1], abc[2], abc[3] ]
            `, "abc,def,pqr,10")

			is(abc[2], "pqr")
			is(abc[3], "10")

			test(`
                delete abc[2];
            `)

			_, exists := abc[2]
			is(exists, false)
		}
	})
}

func Test_reflectMapIterateKeys(t *testing.T) {
	tt(t, func() {
		test, vm := test()

		// map[string]interface{}
		{
			abc := map[string]interface{}{
				"Xyzzy": "Nothing happens.",
				"def":   1,
			}
			vm.Set("abc", abc)
			test(`
                var keys = [];
                for (var key in abc) {
                  keys.push(key);
                }
                keys.sort();
                keys;
            `, "Xyzzy,def")
		}

		// map[uint]interface{}
		{
			abc := map[uint]interface{}{
				456: "Nothing happens.",
				123: 1,
			}
			vm.Set("abc", abc)
			test(`
                var keys = [];
                for (var key in abc) {
                  keys.push(key);
                }
                keys.sort();
                keys;
            `, "123,456")
		}

		// map[byte]interface{}
		{
			abc := map[byte]interface{}{
				10: "Nothing happens.",
				20: 1,
			}
			vm.Set("abc", abc)
			test(`
                for (var key in abc) {
                  abc[key] = "123";
                }
            `)
			is(abc[10], "123")
			is(abc[20], "123")
		}
	})
}

func Test_reflectSlice(t *testing.T) {
	tt(t, func() {
		test, vm := test()

		// []bool
		{
			abc := []bool{
				false,
				true,
				true,
				false,
			}
			vm.Set("abc", abc)

			test(`
                abc;
            `, "false,true,true,false")

			test(`
                abc[0] = true;
                abc[abc.length-1] = true;
                delete abc[2];
                abc;
            `, "true,true,false,true")

			is(abc, []bool{true, true, false, true})
			is(abc[len(abc)-1], true)
		}

		// []int32
		{
			abc := make([]int32, 4)
			vm.Set("abc", abc)

			test(`
                abc;
            `, "0,0,0,0")

			test(`raise:
                abc[0] = "42";
                abc[1] = 4.2;
                abc[2] = 3.14;
                abc;
            `, "RangeError: 4.2 to reflect.Kind: int32")

			is(abc, []int32{42, 0, 0, 0})

			test(`
                delete abc[1];
                delete abc[2];
            `)
			is(abc[1], 0)
			is(abc[2], 0)
		}
	})
}

func Test_reflectArray(t *testing.T) {
	tt(t, func() {
		test, vm := test()

		// []bool
		{
			abc := [4]bool{
				false,
				true,
				true,
				false,
			}
			vm.Set("abc", abc)

			test(`
                abc;
            `, "false,true,true,false")
			// Unaddressable array

			test(`
                abc[0] = true;
                abc[abc.length-1] = true;
                abc;
            `, "false,true,true,false")
			// Again, unaddressable array

			is(abc, [4]bool{false, true, true, false})
			is(abc[len(abc)-1], false)
			// ...
		}
		// []int32
		{
			abc := make([]int32, 4)
			vm.Set("abc", abc)

			test(`
                abc;
            `, "0,0,0,0")

			test(`raise:
                abc[0] = "42";
                abc[1] = 4.2;
                abc[2] = 3.14;
                abc;
            `, "RangeError: 4.2 to reflect.Kind: int32")

			is(abc, []int32{42, 0, 0, 0})
		}

		// []bool
		{
			abc := [4]bool{
				false,
				true,
				true,
				false,
			}
			vm.Set("abc", &abc)

			test(`
                abc;
            `, "false,true,true,false")

			test(`
                abc[0] = true;
                abc[abc.length-1] = true;
                delete abc[2];
                abc;
            `, "true,true,false,true")

			is(abc, [4]bool{true, true, false, true})
			is(abc[len(abc)-1], true)
		}

		// no common type
		{
			test(`
                 abc = [1, 2.2, "str"];
                 abc;
             `, "1,2.2,str")
			val, err := vm.Get("abc")
			is(err, nil)
			abc, err := val.Export()
			is(err, nil)
			is(abc, []interface{}{int64(1), 2.2, "str"})
		}

		// common type int
		{
			test(`
                 abc = [1, 2, 3];
                 abc;
             `, "1,2,3")
			val, err := vm.Get("abc")
			is(err, nil)
			abc, err := val.Export()
			is(err, nil)
			is(abc, []int64{1, 2, 3})
		}

		// common type string
		{
			test(`
                 abc = ["str1", "str2", "str3"];
                 abc;
             `, "str1,str2,str3")

			val, err := vm.Get("abc")
			is(err, nil)
			abc, err := val.Export()
			is(err, nil)
			is(abc, []string{"str1", "str2", "str3"})
		}

		// issue #269
		{
			called := false
			vm.Set("blah", func(c FunctionCall) Value {
				v, err := c.Argument(0).Export()
				is(err, nil)
				is(v, []int64{3})
				called = true
				return UndefinedValue()
			})
			is(called, false)
			test(`var x = 3; blah([x])`)
			is(called, true)
		}
	})
}

func Test_reflectArray_concat(t *testing.T) {
	tt(t, func() {
		test, vm := test()

		vm.Set("ghi", []string{"jkl", "mno"})
		vm.Set("pqr", []interface{}{"jkl", 42, 3.14159, true})
		test(`
            var def = {
                "abc": ["abc"],
                "xyz": ["xyz"]
            };
            xyz = pqr.concat(ghi, def.abc, def, def.xyz);
            [ xyz, xyz.length ];
        `, "jkl,42,3.14159,true,jkl,mno,abc,[object Object],xyz,9")
	})
}

func Test_reflectMapInterface(t *testing.T) {
	tt(t, func() {
		test, vm := test()

		{
			abc := map[string]interface{}{
				"Xyzzy": "Nothing happens.",
				"def":   "1",
				"jkl":   "jkl",
			}
			vm.Set("abc", abc)
			vm.Set("mno", &abcStruct{})

			test(`
                abc.xyz = "pqr";
                abc.ghi = {};
                abc.jkl = 3.14159;
                abc.mno = mno;
                mno.Abc = true;
                mno.Ghi = "Something happens.";
                [ abc.Xyzzy, abc.def, abc.ghi, abc.mno ];
            `, "Nothing happens.,1,[object Object],[object Object]")

			is(abc["xyz"], "pqr")
			is(abc["ghi"], map[string]interface{}{})
			is(abc["jkl"], float64(3.14159))
			mno, valid := abc["mno"].(*abcStruct)
			is(valid, true)
			is(mno.Abc, true)
			is(mno.Ghi, "Something happens.")
		}
	})
}

func TestPassthrough(t *testing.T) {
	tt(t, func() {
		test, vm := test()

		{
			abc := &abcStruct{
				Mno: _mnoStruct{
					Ghi: "<Mno.Ghi>",
				},
			}
			vm.Set("abc", abc)

			test(`
                abc.Mno.Ghi;
            `, "<Mno.Ghi>")

			vm.Set("pqr", map[string]int8{
				"xyzzy":            0,
				"Nothing happens.": 1,
			})

			test(`
                abc.Ghi = "abc";
                abc.Pqr = pqr;
                abc.Pqr["Nothing happens."];
            `, 1)

			mno := _mnoStruct{
				Ghi: "<mno.Ghi>",
			}
			vm.Set("mno", mno)

			test(`
                abc.Mno = mno;
                abc.Mno.Ghi;
            `, "<mno.Ghi>")
		}
	})
}

type TestDynamicFunctionReturningInterfaceMyStruct1 struct{} //nolint:errname

func (m *TestDynamicFunctionReturningInterfaceMyStruct1) Error() string { return "MyStruct1" }

type TestDynamicFunctionReturningInterfaceMyStruct2 struct{} //nolint:errname

func (m *TestDynamicFunctionReturningInterfaceMyStruct2) Error() string { return "MyStruct2" }

func TestDynamicFunctionReturningInterface(t *testing.T) {
	tt(t, func() {
		test, vm := test()

		var l []func() error

		vm.Set("r", func(cb func() error) { l = append(l, cb) })
		vm.Set("e1", func() error { return &TestDynamicFunctionReturningInterfaceMyStruct1{} })
		vm.Set("e2", func() error { return &TestDynamicFunctionReturningInterfaceMyStruct2{} })
		vm.Set("e3", func() error { return nil })

		test("r(function() { return e1(); })", UndefinedValue())
		test("r(function() { return e2(); })", UndefinedValue())
		test("r(function() { return e3(); })", UndefinedValue())
		test("r(function() { return null; })", UndefinedValue())

		if l[0]() == nil {
			t.Fail()
		}
		if l[1]() == nil {
			t.Fail()
		}
		if l[2]() != nil {
			t.Fail()
		}
		if l[3]() != nil {
			t.Fail()
		}
	})
}

func TestStructCallParameterConversion(t *testing.T) {
	tt(t, func() {
		test, vm := test()

		type T struct {
			StringValue  string `json:"s"`
			BooleanValue bool   `json:"b"`
			IntegerValue int    `json:"i"`
		}

		var x T

		vm.Set("f", func(t T) bool { return t == x })

		// test set properties

		x = T{"A", true, 1}
		test("f({s: 'A', b: true, i: 1})", true)

		// test zero-value properties

		x = T{"", false, 0}
		test("f({s: '', b: false, i: 0})", true)

		// test missing properties

		x = T{"", true, 1}
		test("f({b: true, i: 1})", true)

		x = T{"A", false, 1}
		test("f({s: 'A', i: 1})", true)

		x = T{"A", true, 0}
		test("f({s: 'A', b: true})", true)

		x = T{"", false, 0}
		test("f({})", true)

		// make sure it fails with extra properties

		x = T{"", false, 0}
		if _, err := vm.Run("f({x: true})"); err == nil {
			t.Fail()
		}
	})
}

type TestTextUnmarshallerCallParameterConversionMyStruct struct{}

func (m *TestTextUnmarshallerCallParameterConversionMyStruct) UnmarshalText(b []byte) error {
	if string(b) == "good" {
		return nil
	}

	return fmt.Errorf("NOT_GOOD: %s", string(b))
}

func TestTextUnmarshallerCallParameterConversion(t *testing.T) {
	tt(t, func() {
		test, vm := test()

		vm.Set("f", func(t TestTextUnmarshallerCallParameterConversionMyStruct) bool { return true })

		// success
		test("f('good')", true)

		// explicit failure, should pass error message up
		if _, err := vm.Run("f('bad')"); err == nil || !strings.Contains(err.Error(), "NOT_GOOD: bad") {
			t.Fail()
		}

		// wrong input
		if _, err := vm.Run("f(null)"); err == nil {
			t.Fail()
		}

		// no input
		if _, err := vm.Run("f()"); err == nil {
			t.Fail()
		}
	})
}

func TestJSONRawMessageCallParameterConversion(t *testing.T) {
	for _, e := range []struct {
		c string
		r string
		e bool
	}{
		{"f({a:1})", `{"a":1}`, false},
		{"f(null)", `null`, false},
		{"f(1)", `1`, false},
		{"f('x')", `"x"`, false},
		{"f([1,2,3])", `[1,2,3]`, false},
		{"f(function(){})", `{}`, false},
		{"f()", `[1,2,3]`, true},
	} {
		t.Run(e.c, func(t *testing.T) {
			vm := New()
			err := vm.Set("f", func(m json.RawMessage) json.RawMessage { return m })
			require.NoError(t, err)
			r, err := vm.Run(e.c)
			if err != nil {
				if !e.e {
					t.Error("err should be nil")
					t.Fail()
				}

				return
			}

			if e.e {
				t.Error("err should not be nil")
				t.Fail()
				return
			}

			v, err := r.Export()
			if err != nil {
				t.Error(err)
				t.Fail()
			}

			m, ok := v.(json.RawMessage)
			if !ok {
				t.Error("result should be json.RawMessage")
				t.Fail()
			}

			if !bytes.Equal(m, json.RawMessage(e.r)) {
				t.Errorf("output is wrong\nexpected: %s\nactual:   %s\n", e.r, m)
				t.Fail()
			}
		})
	}
}
