package compiler_test

import (
	"math/big"
	"testing"

	"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
)

var structTestCases = []testCase{
	{
		"struct field assign",
		`
		package foo
		func Main() int {
			t := token {
				x: 2,
				y: 4,
			}

			age := t.x
			return age
		}

		type token struct {
			x int 
			y int
		}
		`,
		big.NewInt(2),
	},
	{
		"struct field from func result",
		`
		package foo
		type S struct { x int }
		func fn() int { return 2 }
		func Main() int {
			t := S{x: fn()}
			return t.x
		}
		`,
		big.NewInt(2),
	},
	{
		"struct field return",
		`
		package foo
		type token struct {
			x int
			y int
		}

		func Main() int {
			t := token {
				x: 2,
				y: 4,
			}

			return t.x
		}
		`,
		big.NewInt(2),
	},
	{
		"struct field assign",
		`
		package foo
		type token struct {
			x int
			y int
		}

		func Main() int {
			t := token {
				x: 2,
				y: 4,
			}
			t.x = 10
			return t.x
		}
		`,
		big.NewInt(10),
	},
	{
		"complex struct",
		`
		package foo
		type token struct {
			x int
			y int
		}

		func Main() int {
			x := 10

			t := token {
				x: 2,
				y: 4,
			}

			y := x + t.x

			return y
		}
		`,
		big.NewInt(12),
	},
	{
		"initialize struct field from variable",
		`
		package foo
		type token struct {
			x int
			y int
		}

		func Main() int {
			x := 10
			t := token {
				x: x,
				y: 4,
			}
			y := t.x + t.y
			return y
		}`,
		big.NewInt(14),
	},
	{
		"assign a variable to a struct field",
		`
		package foo
		type token struct {
			x int
			y int
		}

		func Main() int {
			ten := 10
			t := token {
				x: 2,
				y: 4,
			}
			t.x = ten
			y := t.y + t.x
			return y
		}`,
		big.NewInt(14),
	},
	{
		"increase struct field with +=",
		`package foo
		type token struct { x int }
		func Main() int {
		t := token{x: 2}
		t.x += 3
		return t.x
		}`,
		big.NewInt(5),
	},
	{
		"assign a struct field to a struct field",
		`
		package foo
		type token struct {
			x int
			y int
		}

		func Main() int {
			t1 := token {
				x: 2,
				y: 4,
			}
			t2 := token {
				x: 3,
				y: 5,
			}
			t1.x = t2.y
			y := t1.x + t2.x
			return y
		}`,
		big.NewInt(8),
	},
	{
		"initialize same struct twice",
		`
		package foo
		type token struct {
			x int
			y int
		}

		func Main() int {
			t1 := token {
				x: 2,
				y: 4,
			}
			t2 := token {
				x: 2,
				y: 4,
			}
			return t1.x + t2.y
		}
		`,
		big.NewInt(6),
	},
	{
		"struct methods",
		`
		package foo
		type token struct {
			x int
		}

		func(t token) getInteger() int {
			return t.x
		}

		func Main() int {
			t := token {
				x: 4, 
			}
			someInt := t.getInteger()
			return someInt
		}
		`,
		big.NewInt(4),
	},
	{
		"struct methods with arguments",
		`
		package foo
		type token struct {
			x int
		}

		// Also tests if x conflicts with t.x
		func(t token) addIntegers(x int, y int) int {
			return t.x + x + y
		}

		func Main() int {
			t := token {
				x: 4, 
			}
			someInt := t.addIntegers(2, 4)
			return someInt
		}
		`,
		big.NewInt(10),
	},
	{
		"initialize struct partially",
		`
		package foo
		type token struct {
			x int
			y int
			z string
			b bool
		}

		func Main() int {
			t := token {
				x: 4,
			}
			return t.y
		}
		`,
		big.NewInt(0),
	},
	{
		"test return struct from func",
		`
		package foo
		type token struct {
			x int
			y int
			z string
			b bool
		}

		func newToken() token {
			return token{
				x: 1,
				y: 2, 
				z: "hello",
				b: false,
			}
		}

		func Main() token {
			return newToken()
		}
		`,
		[]stackitem.Item{
			stackitem.NewBigInteger(big.NewInt(1)),
			stackitem.NewBigInteger(big.NewInt(2)),
			stackitem.NewByteArray([]byte("hello")),
			stackitem.NewBool(false),
		},
	},
	{
		"pass struct as argument",
		`
		package foo

		type Bar struct {
			amount int
		}

		func addToAmount(x int, bar Bar) int {
			bar.amount = bar.amount + x
			return bar.amount
		}

		func Main() int {
			b := Bar{
				amount: 10,
			}

			x := addToAmount(4, b)
			return x 
		}
		`,
		big.NewInt(14),
	},
	{
		"declare struct literal",
		`package foo
		func Main() int {
			var x struct {
				a int
			}
			x.a = 2
			return x.a
		}`,
		big.NewInt(2),
	},
	{
		"declare struct type",
		`package foo
		type withA struct {
			a int
		}
		func Main() int {
			var x withA
			x.a = 2
			return x.a
		}`,
		big.NewInt(2),
	},
	{
		"nested selectors (simple read)",
		`package foo
		type S1 struct { x, y S2 }
		type S2 struct { a, b int }
		func Main() int {
			var s1 S1
			var s2 S2
			s2.a = 3
			s1.y = s2
			return s1.y.a
		}`,
		big.NewInt(3),
	},
	{
		"nested selectors (simple write)",
		`package foo
		type S1 struct { x S2 }
		type S2 struct { a int }
		func Main() int {
			s1 := S1{
				x: S2 {
					a: 3,
				},
			}
			s1.x.a = 11
			return s1.x.a
		}`,
		big.NewInt(11),
	},
	{
		"complex struct default value",
		`package foo
		type S1 struct { x S2 }
		type S2 struct { y S3 }
		type S3 struct { a int }
		func Main() int {
			var s1 S1
			s1.x.y.a = 11
			return s1.x.y.a
		}`,
		big.NewInt(11),
	},
	{
		"nested selectors (complex write)",
		`package foo
		type S1 struct { x S2 }
		type S2 struct { y, z S3 }
		type S3 struct { a int }
		func Main() int {
			var s1 S1
			s1.x.y.a, s1.x.z.a = 11, 31
			return s1.x.y.a + s1.x.z.a
		}`,
		big.NewInt(42),
	},
	{
		"omit field names",
		`package foo
		type pair struct { a, b int }
		func Main() int {
			p := pair{1, 2}
			x := p.a * 10
			return x + p.b
		}`,
		big.NewInt(12),
	},
	{
		"uninitialized struct fields",
		`package foo
               type Foo struct {
                       i int
                       m map[string]int
                       b []byte
                       a []int
                       s struct { ii int }
               }
               func NewFoo() Foo { return Foo{} }
               func Main() int {
                       foo := NewFoo()
                       if foo.i != 0 { return 1 }
                       if len(foo.m) != 0 { return 1 }
                       if len(foo.b) != 0 { return 1 }
                       if len(foo.a) != 0 { return 1 }
                       s := foo.s
                       if s.ii != 0 { return 1 }
                       return 2
               }`,
		big.NewInt(2),
	},
}

func TestStructs(t *testing.T) {
	runTestCases(t, structTestCases)
}