package compiler_test

import (
	"fmt"
	"math/big"
	"strings"
	"testing"

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

func TestMaxMin(t *testing.T) {
	tests := []struct {
		name        string
		args        []stackitem.Item
		expected    map[string]*big.Int
		expectedErr string
		srcTemplate string
	}{
		{
			name:        "invalid type named",
			expectedErr: "only integer types are supported by %s",
			srcTemplate: `
				package foo

				type MyInt int

				func Main() {
					_ = %s(MyInt(1))
				}
			`,
		},
		{
			name:        "invalid type string",
			expectedErr: "only integer types are supported by %s, got untyped string at 1",
			srcTemplate: `
				package foo

				func Main() {
					_ = %s("42")
				}
			`,
		},
		{
			name: "single value as func call",
			expected: map[string]*big.Int{
				"max": big.NewInt(42),
				"min": big.NewInt(42),
			},
			srcTemplate: `
				package foo

				func f() int {
					return 42
				}

				func Main() int {
					return %s(f())
				}
			`,
		},
		{
			name: "two values",
			args: []stackitem.Item{stackitem.Make(10), stackitem.Make(7)},
			expected: map[string]*big.Int{
				"max": big.NewInt(10),
				"min": big.NewInt(7),
			},
			srcTemplate: `
				package foo

				func Main(args []any) int {
					return %s(args[0].(int), args[1].(int))
				}
			`,
		},
		{
			name: "three values",
			args: []stackitem.Item{stackitem.Make(5), stackitem.Make(6), stackitem.Make(3)},
			expected: map[string]*big.Int{
				"max": big.NewInt(6),
				"min": big.NewInt(3),
			},
			srcTemplate: `
				package foo

				func Main(args []any) int {
					return %s(args[0].(int), args[1].(int), args[2].(int))
				}
			`,
		},
	}

	for _, op := range []string{"max", "min"} {
		for _, tc := range tests {
			src := fmt.Sprintf(tc.srcTemplate, op)
			if tc.expectedErr != "" {
				_, _, err := compiler.CompileWithOptions("main.go", strings.NewReader(src), nil)
				require.ErrorContains(t, err, fmt.Sprintf(tc.expectedErr, op))
			} else {
				evalWithArgs(t, src, nil, tc.args, tc.expected[op])
			}
		}
	}
}

func TestClear(t *testing.T) {
	tests := []struct {
		name        string
		src         string
		expectedErr string
		expected    any
	}{
		{
			name: "invalid type array",
			src: `package foo
				func Main() [42]int {
					var m = [42]int{}
					clear(m)
					return m
				}`,
			expectedErr: "invalid argument: cannot clear m (variable of type [42]int): " +
				"argument must be (or constrained by) map or slice",
		},
		{
			name: "map",
			src: `package foo
				func Main() map[string]int {
					var m = map[string]int{
						"one": 1,
						"two": 2,
					}
					clear(m)
					return m
				}`,
			expected: []stackitem.MapElement{},
		},
		{
			name: "slice of integers",
			src: `package foo
				func Main() []int {
					var s = []int{1, 2, 3}
					clear(s)
					return s
				}`,
			expected: []stackitem.Item{stackitem.Make(0), stackitem.Make(0), stackitem.Make(0)},
		},
		{
			name: "slice of strings",
			src: `package foo
				func Main() []string {
					var s = []string{"one", "two", "three"}
					clear(s)
					return s
				}`,
			expected: []stackitem.Item{stackitem.Make(""), stackitem.Make(""), stackitem.Make("")},
		},
		{
			name: "slice of structs",
			src: `package foo
				type S2 struct {
					A int
					B string
					C bool
				}
					
				type S1 struct {
					S S2
				}
				
				func Main() []S1 {
					var s = []S1 {
						{
							S: S2 {
								A: 42,
								B: "first",
								C: true,
							},
						},
						{
							S: S2 {
								A: 24,
								B: "second",
								C: true,
							},
						},
					}
					clear(s)
					return s
				}`,
			expected: []stackitem.Item{
				stackitem.NewStruct([]stackitem.Item{
					stackitem.NewStruct([]stackitem.Item{
						stackitem.Make(0),
						stackitem.Make(""),
						stackitem.Make(false),
					}),
				}),
				stackitem.NewStruct([]stackitem.Item{
					stackitem.NewStruct([]stackitem.Item{
						stackitem.Make(0),
						stackitem.Make(""),
						stackitem.Make(false),
					}),
				}),
			},
		},
		{
			name: "slice of slices",
			src: `package foo
				func Main() [][]int {
					var s = [][]int{{1, 2, 3}, {4, 5, 6}, {7, 8}}
					clear(s)
					return s
				}`,
			expected: []stackitem.Item{stackitem.Null{}, stackitem.Null{}, stackitem.Null{}},
		},
		{
			name: "slice of maps",
			src: `package foo
				func Main() []map[string]string {
					var s = []map[string]string{
						{
							"key": "val",
						},
						{
							"key": "val",
						},
					}
					clear(s)
					return s
				}`,
			expected: []stackitem.Item{stackitem.Null{}, stackitem.Null{}},
		},
		{
			name: "empty slice",
			src: `package foo
				func Main() []any {
					var s = []any{}
					clear(s)
					return s
				}`,
			expected: []stackitem.Item{},
		},
		{
			name: "empty map",
			src: `package foo
				func Main() map[any]any {
					var m = map[any]any{}
					clear(m)
					return m
				}`,
			expected: []stackitem.MapElement{},
		},
		{
			name: "slice of simple structs",
			src: `package foo
				type S struct {
					A int
				}
				
				func Main() []S {
					var s = []S {
						{
							A: 1,	
						},
						{
							A: 2,
						},
					}
					clear(s)
					s[0].A = 42
					return s
				}`,
			expected: []stackitem.Item{
				stackitem.NewStruct([]stackitem.Item{
					stackitem.Make(42),
				}),
				stackitem.NewStruct([]stackitem.Item{
					stackitem.Make(0),
				}),
			},
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			if tc.expectedErr != "" {
				_, _, err := compiler.CompileWithOptions("main.go", strings.NewReader(tc.src), nil)
				require.ErrorContains(t, err, tc.expectedErr)
			} else {
				eval(t, tc.src, tc.expected)
			}
		})
	}
}
