package compiler_test

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

	"github.com/nspcc-dev/neo-go/pkg/compiler"
	"github.com/nspcc-dev/neo-go/pkg/vm"
	"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
	"github.com/stretchr/testify/require"
)

var sliceTestCases = []testCase{
	{
		"constant index",
		`func F%d() int {
			a := []int{0,0}
			a[1] = 42
			return a[1]+0
		}
		`,
		big.NewInt(42),
	},
	{
		"variable index",
		`func F%d() int {
			a := []int{0,0}
			i := 1
			a[i] = 42
			return a[1]+0
		}
		`,
		big.NewInt(42),
	},
	{
		"increase slice element with +=",
		`func F%d() int {
			a := []int{1, 2, 3}
			a[1] += 40
			return a[1]
		}
		`,
		big.NewInt(42),
	},
	{
		"complex test",
		`func F%d() int {
			a := []int{1,2,3}
			x := a[0]
			a[x] = a[x] + 4
			a[x] = a[x] + a[2]
			return a[1]
		}
		`,
		big.NewInt(9),
	},
	{
		"slice literals with variables",
		`func F%d() int {
			elem := 7
			a := []int{6, elem, 8}
			return a[1]
		}
		`,
		big.NewInt(7),
	},
	{
		"slice literals with expressions",
		`func F%d() int {
			elem := []int{3, 7}
			a := []int{6, elem[1]*2+1, 24}
			return a[1]
		}
		`,
		big.NewInt(15),
	},
	{
		"sub-slice with literal bounds",
		`func F%d() []byte {
			a := []byte{0, 1, 2, 3}
			b := a[1:3]
			return b
		}
		`,
		[]byte{1, 2},
	},
	{
		"sub-slice with constant bounds",
		`const x = 1
		const y = 3
		func F%d() []byte {
			a := []byte{0, 1, 2, 3}
			b := a[x:y]
			return b
		}
		`,
		[]byte{1, 2},
	},
	{
		"sub-slice with variable bounds",
		`func F%d() []byte {
			a := []byte{0, 1, 2, 3}
			x := 1
			y := 3
			b := a[x:y]
			return b
		}
		`,
		[]byte{1, 2},
	},
	{
		"sub-slice with no lower bound",
		`func F%d() []byte {
			a := []byte{0, 1, 2, 3}
			b := a[:3]
			return b
		}
		`,
		[]byte{0, 1, 2},
	},
	{
		"sub-slice with no upper bound",
		`func F%d() []byte {
			a := []byte{0, 1, 2, 3}
			b := a[2:]
			return b
		}
		`,
		[]byte{2, 3},
	},
	{
		"declare byte slice",
		`func F%d() []byte {
			var a []byte
			a = append(a, 1)
			a = append(a, 2)
			return a
		}
		`,
		[]byte{1, 2},
	},
	{
		"append multiple bytes to a slice",
		`func F%d() []byte {
			var a []byte
			a = append(a, 1, 2)
			return a
		}
		`,
		[]byte{1, 2},
	},
	{
		"append multiple ints to a slice",
		`func F%d() []int {
			var a []int
			a = append(a, 1, 2, 3)
			a = append(a, 4, 5)
			return a
		}
		`,
		[]stackitem.Item{
			stackitem.NewBigInteger(big.NewInt(1)),
			stackitem.NewBigInteger(big.NewInt(2)),
			stackitem.NewBigInteger(big.NewInt(3)),
			stackitem.NewBigInteger(big.NewInt(4)),
			stackitem.NewBigInteger(big.NewInt(5)),
		},
	},
	{
		"int slice, append slice",
		`	func getByte() byte { return 0x80 }
			func F%d() []int {
				x := []int{1}
				y := []int{2, 3}
				x = append(x, y...)
				x = append(x, y...)
				return x
			}
		`,
		[]stackitem.Item{
			stackitem.Make(1),
			stackitem.Make(2), stackitem.Make(3),
			stackitem.Make(2), stackitem.Make(3),
		},
	},
	{
		"declare compound slice",
		`func F%d() []string {
			var a []string
			a = append(a, "a")
			a = append(a, "b")
			return a
		}
		`,
		[]stackitem.Item{
			stackitem.NewByteArray([]byte("a")),
			stackitem.NewByteArray([]byte("b")),
		},
	},
	{
		"declare compound slice alias",
		`type strs []string
		func F%d() []string {
			var a strs
			a = append(a, "a")
			a = append(a, "b")
			return a
		}
		`,
		[]stackitem.Item{
			stackitem.NewByteArray([]byte("a")),
			stackitem.NewByteArray([]byte("b")),
		},
	},
	{
		"byte-slice assignment",
		`func F%d() []byte {
			a := []byte{0, 1, 2}
			a[1] = 42
			return a
		}
		`,
		[]byte{0, 42, 2},
	},
	{
		"byte-slice assignment after string conversion",
		`func F%d() []byte {
			a := "abc"
			b := []byte(a)
			b[1] = 42
			return []byte(a)
		}
		`,
		[]byte{0x61, 0x62, 0x63},
	},
	{
		"declare and append byte-slice",
		`func F%d() []byte {
			var a []byte
			a = append(a, 1)
			a = append(a, 2)
			return a
		}
		`,
		[]byte{1, 2},
	},
	{
		"nested slice assignment",
		`func F%d() int {
			a := [][]int{[]int{1, 2}, []int{3, 4}}
			a[1][0] = 42
			return a[1][0]
		}
		`,
		big.NewInt(42),
	},
	{
		"nested slice omitted type (slice)",
		`func F%d() int {
			a := [][]int{{1, 2}, {3, 4}}
			a[1][0] = 42
			return a[1][0]
		}
		`,
		big.NewInt(42),
	},
	{
		"nested slice omitted type (struct)",
		`type pairA struct { a, b int }
		func F%d() int {
			a := []pairA{{a: 1, b: 2}, {a: 3, b: 4}}
			a[1].a = 42
			return a[1].a
		}
		`,
		big.NewInt(42),
	},
	{
		"defaults to nil for byte slice",
		`func F%d() int {
			var a []byte
			if a != nil { return 1}
			return 2
		}
		`,
		big.NewInt(2),
	},
	{
		"defaults to nil for int slice",
		`func F%d() int {
			var a []int
			if a != nil { return 1}
			return 2
		}
		`,
		big.NewInt(2),
	},
	{
		"defaults to nil for struct slice",
		`type pairB struct { a, b int }
		func F%d() int {
			var a []pairB
			if a != nil { return 1}
			return 2
		}
		`,
		big.NewInt(2),
	},
	{
		"literal byte-slice with variable values",
		`const sym1 = 's'
		func F%d() []byte {
			sym2 := byte('t')
			sym4 := byte('i')
			return []byte{sym1, sym2, 'r', sym4, 'n', 'g'}
		}
		`,
		[]byte("string"),
	},
	{
		"literal slice with function call",
		`func fn() byte { return 't' }
		func F%d() []byte {
			return []byte{'s', fn(), 'r'}
		}
		`,
		[]byte("str"),
	},
}

func TestSliceOperations(t *testing.T) {
	srcBuilder := bytes.NewBuffer([]byte("package testcase\n"))
	for i, tc := range sliceTestCases {
		srcBuilder.WriteString(fmt.Sprintf(tc.src, i))
	}

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

	for i, tc := range sliceTestCases {
		t.Run(tc.name, func(t *testing.T) {
			v := vm.New()
			invokeMethod(t, fmt.Sprintf("F%d", i), ne.Script, v, di)
			runAndCheck(t, v, tc.result)
		})
	}
}

func TestByteSlices(t *testing.T) {
	testCases := []testCase{
		{
			"append bytes bigger than 0x79",
			`package foo
			func Main() []byte {
				var z []byte
				z = append(z, 0x78, 0x79, 0x80, 0x81, 0xFF)
				return z
			}`,
			[]byte{0x78, 0x79, 0x80, 0x81, 0xFF},
		},
		{
			"append bytes bigger than 0x79, not nil",
			`package foo
			func Main() []byte {
				z := []byte{0x78}
				z = append(z, 0x79, 0x80, 0x81, 0xFF)
				return z
			}`,
			[]byte{0x78, 0x79, 0x80, 0x81, 0xFF},
		},
		{
			"append bytes bigger than 0x79, function return",
			`package foo
			func getByte() byte { return 0x80 }
			func Main() []byte {
				var z []byte
				z = append(z, 0x78, 0x79, getByte(), 0x81, 0xFF)
				return z
			}`,
			[]byte{0x78, 0x79, 0x80, 0x81, 0xFF},
		},
		{
			"append ints bigger than 0x79, function return byte",
			`package foo
			func getByte() byte { return 0x80 }
			func Main() []int {
				var z []int
				z = append(z, 0x78, int(getByte()))
				return z
			}`,
			[]stackitem.Item{stackitem.Make(0x78), stackitem.Make(0x80)},
		},
		{
			"slice literal, bytes bigger than 0x79, function return",
			`package foo
			func getByte() byte { return 0x80 }
			func Main() []byte {
				z := []byte{0x79, getByte(), 0x81}
				return z
			}`,
			[]byte{0x79, 0x80, 0x81},
		},
		{
			"compare bytes as integers",
			`package foo
				func getByte1() byte { return 0x79 }
				func getByte2() byte { return 0x80 }
				func Main() bool {
					return getByte1() < getByte2()
				}`,
			true,
		},
	}
	runTestCases(t, testCases)
}

func TestSubsliceCompound(t *testing.T) {
	src := `package foo
	func Main() []int {
		a := []int{0, 1, 2, 3}
		b := a[1:3]
		return b
	}`
	_, err := compiler.Compile("", strings.NewReader(src))
	require.Error(t, err)
}

func TestSubsliceFromStructField(t *testing.T) {
	src := `package foo
	type pair struct { key, value []byte }
	func Main() []byte {
		p := pair{ []byte{1}, []byte{4, 8, 15, 16, 23, 42} }
		return p.value[2:4]
	}`
	eval(t, src, []byte{15, 16})
}

func TestRemove(t *testing.T) {
	t.Run("Valid", func(t *testing.T) {
		src := `package foo
		import "github.com/nspcc-dev/neo-go/pkg/interop/util"
		func Main() int {
			a := []int{11, 22, 33}
			util.Remove(a, 1)
			return len(a) + a[0] + a[1]
		}`
		eval(t, src, big.NewInt(46))
	})
	t.Run("ByteSlice", func(t *testing.T) {
		// This test checks that `Remove` has correct arguments.
		// After `Remove` became an opcode it is harder to do such checks.
		// Skip the test for now.
		t.Skip()
		src := `package foo
		import "github.com/nspcc-dev/neo-go/pkg/interop/util"
		func Main() int {
			a := []byte{11, 22, 33}
			util.Remove(a, 1)
			return len(a)
		}`
		_, err := compiler.Compile("", strings.NewReader(src))
		require.Error(t, err)
	})
}

func TestJumps(t *testing.T) {
	src := `
	package foo
	func Main() []byte {
		buf := []byte{0x62, 0x01, 0x00}
		return buf
	}
	`
	eval(t, src, []byte{0x62, 0x01, 0x00})
}

func TestMake(t *testing.T) {
	t.Run("Map", func(t *testing.T) {
		src := `package foo
		func Main() int {
			a := make(map[int]int)
			a[1] = 10
			a[2] = 20
			return a[1]
		}`
		eval(t, src, big.NewInt(10))
	})
	t.Run("IntSlice", func(t *testing.T) {
		src := `package foo
		func Main() int {
			a := make([]int, 10)
			return len(a) + a[0]
		}`
		eval(t, src, big.NewInt(10))
	})
	t.Run("ByteSlice", func(t *testing.T) {
		src := `package foo
		func Main() int {
			a := make([]byte, 10)
			return len(a) + int(a[0])
		}`
		eval(t, src, big.NewInt(10))
	})
	t.Run("CapacityError", func(t *testing.T) {
		src := `package foo
		func Main() int {
			a := make([]int, 1, 2)
			return a[0]
		}`
		_, err := compiler.Compile("foo.go", strings.NewReader(src))
		require.Error(t, err)
	})
}

func TestCopy(t *testing.T) {
	t.Run("Invalid", func(t *testing.T) {
		src := `package foo
		func Main() []int {
			src := []int{3, 2, 1}
			dst := make([]int, 2)
			copy(dst, src)
			return dst
		}`
		_, err := compiler.Compile("foo.go", strings.NewReader(src))
		require.Error(t, err)
	})
	t.Run("Simple", func(t *testing.T) {
		src := `package foo
		func Main() []byte {
			src := []byte{3, 2, 1}
			dst := make([]byte, 2)
			copy(dst, src)
			return dst
		}`
		eval(t, src, []byte{3, 2})
	})
	t.Run("LowSrcIndex", func(t *testing.T) {
		src := `package foo
		func Main() []byte {
			src := []byte{3, 2, 1}
			dst := make([]byte, 2)
			copy(dst, src[1:])
			return dst
		}`
		eval(t, src, []byte{2, 1})
	})
	t.Run("LowDstIndex", func(t *testing.T) {
		src := `package foo
		func Main() []byte {
			src := []byte{3, 2, 1}
			dst := make([]byte, 2)
			copy(dst[1:], src[1:])
			return dst
		}`
		eval(t, src, []byte{0, 2})
	})
	t.Run("BothIndices", func(t *testing.T) {
		src := `package foo
		func Main() []byte {
			src := []byte{4, 3, 2, 1}
			dst := make([]byte, 4)
			copy(dst[1:], src[1:3])
			return dst
		}`
		eval(t, src, []byte{0, 3, 2, 0})
	})
	t.Run("EmptySliceExpr", func(t *testing.T) {
		src := `package foo
		func Main() []byte {
			src := []byte{3, 2, 1}
			dst := make([]byte, 2)
			copy(dst[1:], src[:])
			return dst
		}`
		eval(t, src, []byte{0, 3})
	})
	t.Run("AssignToVariable", func(t *testing.T) {
		src := `package foo
		func Main() int {
			src := []byte{3, 2, 1}
			dst := make([]byte, 2)
			n := copy(dst, src)
			return n
		}`
		eval(t, src, big.NewInt(2))
	})
}