package compiler_test

import (
	"bytes"
	"encoding/binary"
	"fmt"
	"math/big"
	"strings"
	"testing"

	"git.marketally.com/tutus-one/tutus-chain/pkg/compiler"
	"git.marketally.com/tutus-one/tutus-chain/pkg/smartcontract/trigger"
	"git.marketally.com/tutus-one/tutus-chain/pkg/vm"
	"git.marketally.com/tutus-one/tutus-chain/pkg/vm/stackitem"
	"github.com/stretchr/testify/require"
)

type convertTestCase struct {
	returnType string
	argValue   string
	result     any
}

func getFunctionName(typ string) string {
	switch typ {
	case "bool":
		return "Bool"
	case "[]byte":
		return "Bytes"
	case "int":
		return "Integer"
	}
	panic("invalid type")
}

func TestConvert(t *testing.T) {
	srcTmpl := `func F%d() %s {
		arg := %s
		return convert.To%s(arg)
	}
	`

	convertTestCases := []convertTestCase{
		{"bool", "true", true},
		{"bool", "false", false},
		{"bool", "12", true},
		{"bool", "0", false},
		{"bool", "[]byte{0, 1, 0}", true},
		{"bool", "[]byte{0}", true},
		{"bool", `""`, false},
		{"int", "true", big.NewInt(1)},
		{"int", "false", big.NewInt(0)},
		{"int", "12", big.NewInt(12)},
		{"int", "0", big.NewInt(0)},
		{"int", "[]byte{0, 1, 0}", big.NewInt(256)},
		{"int", "[]byte{0}", big.NewInt(0)},
		{"[]byte", "true", []byte{1}},
		{"[]byte", "false", []byte{0}},
		{"[]byte", "12", []byte{0x0C}},
		{"[]byte", "0", []byte{}},
		{"[]byte", "[]byte{0, 1, 0}", []byte{0, 1, 0}},
	}

	srcBuilder := bytes.NewBuffer([]byte(`package testcase
		import "git.marketally.com/tutus-one/tutus-chain/pkg/interop/convert"
	`))
	for i, tc := range convertTestCases {
		name := getFunctionName(tc.returnType)
		_, err := fmt.Fprintf(srcBuilder, srcTmpl, i, tc.returnType, tc.argValue, name)
		require.NoError(t, err)
	}

	ne, di, err := compiler.CompileWithOptions("file.go", strings.NewReader(srcBuilder.String()), nil)
	require.NoError(t, err)

	for i, tc := range convertTestCases {
		v := vm.New()
		t.Run(tc.argValue+getFunctionName(tc.returnType), func(t *testing.T) {
			v.Reset(trigger.Application)
			invokeMethod(t, fmt.Sprintf("F%d", i), ne.Script, v, di)
			runAndCheck(t, v, tc.result)
		})
	}
}

func TestTypeAssertion(t *testing.T) {
	t.Run("inside return statement", func(t *testing.T) {
		src := `package foo
				func Main() int {
					a := []byte{1}
					var u any
					u = a
					return u.(int)
				}`
		eval(t, src, big.NewInt(1))
	})
	t.Run("inside general declaration", func(t *testing.T) {
		src := `package foo
				func Main() int {
					a := []byte{1}
					var u any
					u = a
					var ret = u.(int)
					return ret
				}`
		eval(t, src, big.NewInt(1))
	})
	t.Run("inside assignment statement", func(t *testing.T) {
		src := `package foo
				func Main() int {
					a := []byte{1}
					var u any
					u = a
					var ret int
					ret = u.(int)
					return ret
				}`
		eval(t, src, big.NewInt(1))
	})
	t.Run("inside definition statement", func(t *testing.T) {
		src := `package foo
				func Main() int {
					a := []byte{1}
					var u any
					u = a
					ret := u.(int)
					return ret
				}`
		eval(t, src, big.NewInt(1))
	})
}

func TestTypeAssertionWithOK(t *testing.T) {
	t.Run("inside general declaration", func(t *testing.T) {
		src := `package foo
				func Main() bool {
					a := 1
					var u any
					u = a
					var _, ok = u.(int)	// *ast.GenDecl
					return ok
				}`
		_, _, err := compiler.CompileWithOptions("foo.go", strings.NewReader(src), nil)
		require.Error(t, err)
		require.ErrorIs(t, err, compiler.ErrUnsupportedTypeAssertion)
	})
	t.Run("inside assignment statement", func(t *testing.T) {
		src := `package foo
				func Main() bool {
					a := 1
					var u any
					u = a
					var ok bool
					_, ok = u.(int)	// *ast.AssignStmt
					return ok
				}`
		_, _, err := compiler.CompileWithOptions("foo.go", strings.NewReader(src), nil)
		require.Error(t, err)
		require.ErrorIs(t, err, compiler.ErrUnsupportedTypeAssertion)
	})
	t.Run("inside definition statement", func(t *testing.T) {
		src := `package foo
				func Main() bool {
					a := 1
					var u any
					u = a
					_, ok := u.(int)	// *ast.AssignStmt
					return ok
				}`
		_, _, err := compiler.CompileWithOptions("foo.go", strings.NewReader(src), nil)
		require.Error(t, err)
		require.ErrorIs(t, err, compiler.ErrUnsupportedTypeAssertion)
	})
}

func TestTypeConversion(t *testing.T) {
	src := `package foo
	type myInt int
	func Main() int32 {
		var a int32 = 41
		b := myInt(a)
		incMy := func(x myInt) myInt { return x + 1 }
		c := incMy(b)
		return int32(c)
	}`

	eval(t, src, big.NewInt(42))
}

func TestSelectorTypeConversion(t *testing.T) {
	src := `package foo
	import "git.marketally.com/tutus-one/tutus-chain/pkg/compiler/testdata/types"
	import "git.marketally.com/tutus-one/tutus-chain/pkg/interop/util"
	import "git.marketally.com/tutus-one/tutus-chain/pkg/interop"
	func Main() int {
		var a int
		if util.Equals(types.Buffer(nil), nil) {
			a += 1
		}

	    // Buffer != ByteArray
		if util.Equals(types.Buffer("\x12"), "\x12") {
			a += 10
		}

		tmp := []byte{0x23}
		if util.Equals(types.ByteString(tmp), "\x23") {
			a += 100
		}

		addr := "aaaaaaaaaaaaaaaaaaaa"
		buf := []byte(addr)
		if util.Equals(interop.Hash160(addr), interop.Hash160(buf)) {
			a += 1000
		}
		return a
	}`
	eval(t, src, big.NewInt(1101))
}

func TestTypeConversionString(t *testing.T) {
	src := `package foo
	type mystr string
	func Main() mystr {
		b := []byte{'l', 'a', 'm', 'a', 'o'}
		s := mystr(b)
		b[0] = 'u'
		return s
	}`
	eval(t, src, []byte("lamao"))
}

func TestInterfaceTypeConversion(t *testing.T) {
	src := `package foo
	func Main() int {
		a := 1
		b := any(a).(int)
		return b
	}`
	eval(t, src, big.NewInt(1))
}

func TestConvert_Uint(t *testing.T) {
	nums := []uint64{
		0,
		1,
		127,
		128,
		255,
		256,
		1024,
		1<<16 - 1,
		1 << 16,
		1<<32 - 1,
		1 << 32,
		1<<64 - 1,
	}

	var (
		countUint64 = len(nums)
		countUint32 = len(nums) - 2
		countUint16 = len(nums) - 4
		countUint8  = 5
	)

	testCases := []struct {
		nums       []uint64
		offset     int
		typ        string
		helperType string
		endians    map[string]func([]byte, uint64)
	}{
		{
			nums:       nums[:countUint64],
			typ:        "uint64",
			helperType: "Uint64",
			endians: map[string]func([]byte, uint64){
				"LE": binary.LittleEndian.PutUint64,
				"BE": binary.BigEndian.PutUint64,
			},
		},
		{
			nums:       nums[:countUint32],
			offset:     4,
			typ:        "uint32",
			helperType: "Uint32",
			endians: map[string]func([]byte, uint64){
				"LE": binary.LittleEndian.PutUint64,
				"BE": binary.BigEndian.PutUint64,
			},
		},
		{
			nums:       nums[:countUint16],
			offset:     6,
			typ:        "uint16",
			helperType: "Uint16",
			endians: map[string]func([]byte, uint64){
				"LE": binary.LittleEndian.PutUint64,
				"BE": binary.BigEndian.PutUint64,
			},
		},
		{
			nums:       nums[:countUint8],
			offset:     7,
			typ:        "uint8",
			helperType: "Uint8",
			endians: map[string]func([]byte, uint64){
				"": binary.LittleEndian.PutUint64,
			},
		},
	}

	srcToBytesTmpl := `package foo
		import "git.marketally.com/tutus-one/tutus-chain/pkg/interop/convert"
	
		func Main(args []any) []byte {
			return convert.%sToBytes%s(args[0].(%s))
		}
	`

	srcFromBytesTmpl := `package foo
		import "git.marketally.com/tutus-one/tutus-chain/pkg/interop/convert"
	
		func Main(args []any) %s {
			return convert.Bytes%sTo%s(args[0].([]byte))
		}
	`

	srcCompatibilityCheckTmpl := `package foo
		import "git.marketally.com/tutus-one/tutus-chain/pkg/interop/convert"
		func Main(args []any) %s {
			data := convert.%sToBytes%s(args[0].(%s))
			return convert.Bytes%sTo%s(data)
		}
	`

	for _, tc := range testCases {
		for endian, convertFunc := range tc.endians {
			srcToBytes := fmt.Sprintf(srcToBytesTmpl, tc.helperType, endian, tc.typ)
			srcFromBytes := fmt.Sprintf(srcFromBytesTmpl, tc.typ, endian, tc.helperType)
			srcCompatibilityCheck := fmt.Sprintf(srcCompatibilityCheckTmpl,
				tc.typ, tc.helperType, endian, tc.typ, endian, tc.helperType,
			)

			buf := make([]byte, 8)
			data := buf[:8-tc.offset]
			if endian == "BE" {
				data = buf[tc.offset:]
			}

			for _, num := range tc.nums {
				convertFunc(buf, num)
				evalWithArgs(t, srcToBytes, nil, []stackitem.Item{stackitem.Make(num)}, data)
				evalWithArgs(t, srcFromBytes, nil, []stackitem.Item{stackitem.Make(data)},
					new(big.Int).SetUint64(num),
				)
				evalWithArgs(t, srcCompatibilityCheck, nil, []stackitem.Item{stackitem.Make(num)},
					new(big.Int).SetUint64(num),
				)
			}
		}
	}
}
