package vm

import (
	"math/big"
	"testing"

	"github.com/nspcc-dev/neo-go/pkg/smartcontract"
	"github.com/stretchr/testify/assert"
)

var makeStackItemTestCases = []struct {
	input  interface{}
	result StackItem
}{
	{
		input:  int64(3),
		result: &BigIntegerItem{value: big.NewInt(3)},
	},
	{
		input:  int16(3),
		result: &BigIntegerItem{value: big.NewInt(3)},
	},
	{
		input:  3,
		result: &BigIntegerItem{value: big.NewInt(3)},
	},
	{
		input:  uint8(3),
		result: &BigIntegerItem{value: big.NewInt(3)},
	},
	{
		input:  uint16(3),
		result: &BigIntegerItem{value: big.NewInt(3)},
	},
	{
		input:  uint32(3),
		result: &BigIntegerItem{value: big.NewInt(3)},
	},
	{
		input:  uint64(3),
		result: &BigIntegerItem{value: big.NewInt(3)},
	},
	{
		input:  big.NewInt(3),
		result: &BigIntegerItem{value: big.NewInt(3)},
	},
	{
		input:  []byte{1, 2, 3, 4},
		result: &ByteArrayItem{value: []byte{1, 2, 3, 4}},
	},
	{
		input:  []byte{},
		result: &ByteArrayItem{value: []byte{}},
	},
	{
		input:  "bla",
		result: &ByteArrayItem{value: []byte("bla")},
	},
	{
		input:  "",
		result: &ByteArrayItem{value: []byte{}},
	},
	{
		input:  true,
		result: &BoolItem{value: true},
	},
	{
		input:  false,
		result: &BoolItem{value: false},
	},
	{
		input:  []StackItem{&BigIntegerItem{value: big.NewInt(3)}, &ByteArrayItem{value: []byte{1, 2, 3}}},
		result: &ArrayItem{value: []StackItem{&BigIntegerItem{value: big.NewInt(3)}, &ByteArrayItem{value: []byte{1, 2, 3}}}},
	},
	{
		input:  []int{1, 2, 3},
		result: &ArrayItem{value: []StackItem{&BigIntegerItem{value: big.NewInt(1)}, &BigIntegerItem{value: big.NewInt(2)}, &BigIntegerItem{value: big.NewInt(3)}}},
	},
}

var makeStackItemErrorCases = []struct {
	input interface{}
}{
	{
		input: nil,
	},
}

func TestMakeStackItem(t *testing.T) {
	for _, testCase := range makeStackItemTestCases {
		assert.Equal(t, testCase.result, makeStackItem(testCase.input))
	}
	for _, errorCase := range makeStackItemErrorCases {
		assert.Panics(t, func() { makeStackItem(errorCase.input) })
	}
}

var stringerTestCases = []struct {
	input  StackItem
	result string
}{
	{
		input:  NewStructItem([]StackItem{}),
		result: "Struct",
	},
	{
		input:  NewBigIntegerItem(big.NewInt(3)),
		result: "BigInteger",
	},
	{
		input:  NewBoolItem(true),
		result: "Boolean",
	},
	{
		input:  NewByteArrayItem([]byte{}),
		result: "ByteArray",
	},
	{
		input:  NewArrayItem([]StackItem{}),
		result: "Array",
	},
	{
		input:  NewMapItem(),
		result: "Map",
	},
	{
		input:  NewInteropItem(nil),
		result: "InteropItem",
	},
}

func TestStringer(t *testing.T) {
	for _, testCase := range stringerTestCases {
		assert.Equal(t, testCase.result, testCase.input.String())
	}
}

var equalsTestCases = map[string][]struct {
	item1  StackItem
	item2  StackItem
	result bool
}{
	"struct": {
		{
			item1:  NewStructItem(nil),
			item2:  nil,
			result: false,
		},
		{
			item1:  NewStructItem(nil),
			item2:  NewBigIntegerItem(big.NewInt(1)),
			result: false,
		},
		{
			item1:  NewStructItem(nil),
			item2:  NewStructItem([]StackItem{NewBigIntegerItem(big.NewInt(1))}),
			result: false,
		},
		{
			item1:  NewStructItem([]StackItem{NewBigIntegerItem(big.NewInt(1))}),
			item2:  NewStructItem([]StackItem{NewBigIntegerItem(big.NewInt(2))}),
			result: false,
		},
		{
			item1:  NewStructItem([]StackItem{NewBigIntegerItem(big.NewInt(1))}),
			item2:  NewStructItem([]StackItem{NewBigIntegerItem(big.NewInt(1))}),
			result: true,
		},
	},
	"bigint": {
		{
			item1:  NewBigIntegerItem(big.NewInt(2)),
			item2:  nil,
			result: false,
		},
		{
			item1:  NewBigIntegerItem(big.NewInt(2)),
			item2:  NewBigIntegerItem(big.NewInt(2)),
			result: true,
		},
		{
			item1:  NewBigIntegerItem(big.NewInt(2)),
			item2:  NewBoolItem(false),
			result: false,
		},
		{
			item1:  NewBigIntegerItem(big.NewInt(0)),
			item2:  NewBoolItem(false),
			result: false,
		},
		{
			item1:  NewBigIntegerItem(big.NewInt(2)),
			item2:  makeStackItem(int32(2)),
			result: true,
		},
	},
	"bool": {
		{
			item1:  NewBoolItem(true),
			item2:  nil,
			result: false,
		},
		{
			item1:  NewBoolItem(true),
			item2:  NewBoolItem(true),
			result: true,
		},
		{
			item1:  NewBoolItem(true),
			item2:  NewBigIntegerItem(big.NewInt(1)),
			result: true,
		},
		{
			item1:  NewBoolItem(true),
			item2:  NewBoolItem(false),
			result: false,
		},
		{
			item1:  NewBoolItem(true),
			item2:  makeStackItem(true),
			result: true,
		},
	},
	"bytearray": {
		{
			item1:  NewByteArrayItem(nil),
			item2:  nil,
			result: false,
		},
		{
			item1:  NewByteArrayItem([]byte{1, 2, 3}),
			item2:  NewByteArrayItem([]byte{1, 2, 3}),
			result: true,
		},
		{
			item1:  NewByteArrayItem([]byte{1}),
			item2:  NewBigIntegerItem(big.NewInt(1)),
			result: true,
		},
		{
			item1:  NewByteArrayItem([]byte{1, 2, 3}),
			item2:  NewByteArrayItem([]byte{1, 2, 4}),
			result: false,
		},
		{
			item1:  NewByteArrayItem([]byte{1, 2, 3}),
			item2:  makeStackItem([]byte{1, 2, 3}),
			result: true,
		},
	},
	"array": {
		{
			item1:  NewArrayItem(nil),
			item2:  nil,
			result: false,
		},
		{
			item1:  NewArrayItem([]StackItem{&BigIntegerItem{big.NewInt(1)}, &BigIntegerItem{big.NewInt(2)}, &BigIntegerItem{big.NewInt(3)}}),
			item2:  NewArrayItem([]StackItem{&BigIntegerItem{big.NewInt(1)}, &BigIntegerItem{big.NewInt(2)}, &BigIntegerItem{big.NewInt(3)}}),
			result: false,
		},
		{
			item1:  NewArrayItem([]StackItem{&BigIntegerItem{big.NewInt(1)}}),
			item2:  NewBigIntegerItem(big.NewInt(1)),
			result: false,
		},
		{
			item1:  NewArrayItem([]StackItem{&BigIntegerItem{big.NewInt(1)}, &BigIntegerItem{big.NewInt(2)}, &BigIntegerItem{big.NewInt(3)}}),
			item2:  NewArrayItem([]StackItem{&BigIntegerItem{big.NewInt(1)}, &BigIntegerItem{big.NewInt(2)}, &BigIntegerItem{big.NewInt(4)}}),
			result: false,
		},
	},
	"map": {
		{
			item1:  NewMapItem(),
			item2:  nil,
			result: false,
		},
		{
			item1:  NewMapItem(),
			item2:  NewMapItem(),
			result: false,
		},
		{
			item1:  &MapItem{value: []MapElement{{NewByteArrayItem([]byte("first")), NewBigIntegerItem(big.NewInt(1))}, {NewBoolItem(true), NewByteArrayItem([]byte{2})}}},
			item2:  &MapItem{value: []MapElement{{NewByteArrayItem([]byte("first")), NewBigIntegerItem(big.NewInt(1))}, {NewBoolItem(true), NewByteArrayItem([]byte{2})}}},
			result: false,
		},
		{
			item1:  &MapItem{value: []MapElement{{NewByteArrayItem([]byte("first")), NewBigIntegerItem(big.NewInt(1))}, {NewBoolItem(true), NewByteArrayItem([]byte{2})}}},
			item2:  &MapItem{value: []MapElement{{NewByteArrayItem([]byte("first")), NewBigIntegerItem(big.NewInt(1))}, {NewBoolItem(true), NewByteArrayItem([]byte{3})}}},
			result: false,
		},
	},
	"interop": {
		{
			item1:  NewInteropItem(nil),
			item2:  nil,
			result: false,
		},
		{
			item1:  NewInteropItem(nil),
			item2:  NewInteropItem(nil),
			result: true,
		},
		{
			item1:  NewInteropItem(2),
			item2:  NewInteropItem(3),
			result: false,
		},
		{
			item1:  NewInteropItem(3),
			item2:  NewInteropItem(3),
			result: true,
		},
	},
}

func TestEquals(t *testing.T) {
	for name, testBatch := range equalsTestCases {
		for _, testCase := range testBatch {
			t.Run(name, func(t *testing.T) {
				assert.Equal(t, testCase.result, testCase.item1.Equals(testCase.item2))
				// Reference equals
				assert.Equal(t, true, testCase.item1.Equals(testCase.item1))
			})
		}
	}
}

var marshalJSONTestCases = []struct {
	input  StackItem
	result []byte
}{
	{
		input:  NewBigIntegerItem(big.NewInt(2)),
		result: []byte(`2`),
	},
	{
		input:  NewBoolItem(true),
		result: []byte(`true`),
	},
	{
		input:  NewByteArrayItem([]byte{1, 2, 3}),
		result: []byte(`"010203"`),
	},
	{
		input:  &ArrayItem{value: []StackItem{&BigIntegerItem{value: big.NewInt(3)}, &ByteArrayItem{value: []byte{1, 2, 3}}}},
		result: []byte(`[3,"010203"]`),
	},
	{
		input:  &InteropItem{value: 3},
		result: []byte(`3`),
	},
}

func TestMarshalJSON(t *testing.T) {
	var (
		actual []byte
		err    error
	)
	for _, testCase := range marshalJSONTestCases {
		switch testCase.input.(type) {
		case *BigIntegerItem:
			actual, err = testCase.input.(*BigIntegerItem).MarshalJSON()
		case *BoolItem:
			actual, err = testCase.input.(*BoolItem).MarshalJSON()
		case *ByteArrayItem:
			actual, err = testCase.input.(*ByteArrayItem).MarshalJSON()
		case *ArrayItem:
			actual, err = testCase.input.(*ArrayItem).MarshalJSON()
		case *InteropItem:
			actual, err = testCase.input.(*InteropItem).MarshalJSON()
		default:
			continue
		}

		assert.NoError(t, err)
		assert.Equal(t, testCase.result, actual)
	}
}

var toContractParameterTestCases = []struct {
	input  StackItem
	result smartcontract.Parameter
}{
	{
		input: NewStructItem([]StackItem{
			NewBigIntegerItem(big.NewInt(1)),
			NewBoolItem(true),
		}),
		result: smartcontract.Parameter{Type: smartcontract.ArrayType, Value: []smartcontract.Parameter{
			{Type: smartcontract.IntegerType, Value: int64(1)},
			{Type: smartcontract.BoolType, Value: true},
		}},
	},
	{
		input:  NewBoolItem(false),
		result: smartcontract.Parameter{Type: smartcontract.BoolType, Value: false},
	},
	{
		input:  NewByteArrayItem([]byte{0x01, 0x02, 0x03}),
		result: smartcontract.Parameter{Type: smartcontract.ByteArrayType, Value: []byte{0x01, 0x02, 0x03}},
	},
	{
		input: NewArrayItem([]StackItem{NewBigIntegerItem(big.NewInt(2)), NewBoolItem(true)}),
		result: smartcontract.Parameter{Type: smartcontract.ArrayType, Value: []smartcontract.Parameter{
			{Type: smartcontract.IntegerType, Value: int64(2)},
			{Type: smartcontract.BoolType, Value: true},
		}},
	},
	{
		input:  NewInteropItem(nil),
		result: smartcontract.Parameter{Type: smartcontract.InteropInterfaceType, Value: nil},
	},
	{
		input: &MapItem{value: []MapElement{
			{NewBigIntegerItem(big.NewInt(1)), NewBoolItem(true)},
			{NewByteArrayItem([]byte("qwerty")), NewBigIntegerItem(big.NewInt(3))},
			{NewBoolItem(true), NewBoolItem(false)},
		}},
		result: smartcontract.Parameter{
			Type: smartcontract.MapType,
			Value: []smartcontract.ParameterPair{
				{
					Key:   smartcontract.Parameter{Type: smartcontract.IntegerType, Value: int64(1)},
					Value: smartcontract.Parameter{Type: smartcontract.BoolType, Value: true},
				}, {
					Key:   smartcontract.Parameter{Type: smartcontract.ByteArrayType, Value: []byte("qwerty")},
					Value: smartcontract.Parameter{Type: smartcontract.IntegerType, Value: int64(3)},
				}, {

					Key:   smartcontract.Parameter{Type: smartcontract.BoolType, Value: true},
					Value: smartcontract.Parameter{Type: smartcontract.BoolType, Value: false},
				},
			},
		},
	},
}

func TestToContractParameter(t *testing.T) {
	for _, tc := range toContractParameterTestCases {
		seen := make(map[StackItem]bool)
		res := tc.input.ToContractParameter(seen)
		assert.Equal(t, res, tc.result)
	}
}