package vm

import (
	"math/big"
	"testing"

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

func TestPushElement(t *testing.T) {
	elems := makeElements(10)
	s := NewStack("test")
	for _, elem := range elems {
		s.Push(elem)
	}

	assert.Equal(t, len(elems), s.Len())

	for i := range elems {
		assert.Equal(t, elems[len(elems)-1-i], s.Peek(i))
	}
}

func TestStack_PushVal(t *testing.T) {
	type (
		i32      int32
		testByte uint8
	)

	s := NewStack("test")
	require.NotPanics(t, func() { s.PushVal(i32(123)) })
	require.NotPanics(t, func() { s.PushVal(testByte(42)) })
	require.Equal(t, 2, s.Len())
	require.Equal(t, big.NewInt(42), s.Pop().Value())
	require.Equal(t, big.NewInt(123), s.Pop().Value())
}

func TestPopElement(t *testing.T) {
	var (
		s     = NewStack("test")
		elems = makeElements(10)
	)
	for _, elem := range elems {
		s.Push(elem)
	}

	for i := len(elems) - 1; i >= 0; i-- {
		assert.Equal(t, elems[i], s.Pop())
		assert.Equal(t, i, s.Len())
	}
}

func TestPeekElement(t *testing.T) {
	var (
		s     = NewStack("test")
		elems = makeElements(10)
	)
	for _, elem := range elems {
		s.Push(elem)
	}
	for i := len(elems) - 1; i >= 0; i-- {
		assert.Equal(t, elems[i], s.Peek(len(elems)-i-1))
	}
}

func TestRemoveAt(t *testing.T) {
	var (
		s     = NewStack("test")
		elems = makeElements(10)
	)
	for _, elem := range elems {
		s.Push(elem)
	}

	elem := s.RemoveAt(8)
	assert.Equal(t, elems[1], elem)

	// Test if the pointers are moved.
	assert.Equal(t, elems[0], s.Peek(8))
	assert.Equal(t, elems[2], s.Peek(7))
}

func TestPushFromOtherStack(t *testing.T) {
	var (
		s1    = NewStack("test")
		s2    = NewStack("test2")
		elems = makeElements(2)
	)
	for _, elem := range elems {
		s1.Push(elem)
	}
	s2.Push(NewElement(100))
	s2.Push(NewElement(101))

	s1.Push(s2.Pop())
	assert.Equal(t, len(elems)+1, s1.Len())
	assert.Equal(t, 1, s2.Len())
}

func TestDupElement(t *testing.T) {
	s := NewStack("test")
	elemA := NewElement(101)
	s.Push(elemA)

	dupped := s.Dup(0)
	s.Push(dupped)
	assert.Equal(t, 2, s.Len())
	assert.Equal(t, dupped, s.Peek(0))
}

func TestBack(t *testing.T) {
	var (
		s     = NewStack("test")
		elems = makeElements(10)
	)
	for _, elem := range elems {
		s.Push(elem)
	}

	assert.Equal(t, elems[0], s.Back())
}

func TestTop(t *testing.T) {
	var (
		s     = NewStack("test")
		elems = makeElements(10)
	)
	for _, elem := range elems {
		s.Push(elem)
	}

	assert.Equal(t, elems[len(elems)-1], s.Top())
}

func TestRemoveLastElement(t *testing.T) {
	var (
		s     = NewStack("test")
		elems = makeElements(2)
	)
	for _, elem := range elems {
		s.Push(elem)
	}
	elem := s.RemoveAt(1)
	assert.Equal(t, elems[0], elem)
	assert.Equal(t, 1, s.Len())
}

func TestIterAfterRemove(t *testing.T) {
	var (
		s     = NewStack("test")
		elems = makeElements(10)
	)
	for _, elem := range elems {
		s.Push(elem)
	}
	s.RemoveAt(0)

	i := 0
	s.Iter(func(_ Element) {
		i++
	})
	assert.Equal(t, len(elems)-1, i)
}

func TestIteration(t *testing.T) {
	var (
		n     = 10
		s     = NewStack("test")
		elems = makeElements(n)
	)
	for _, elem := range elems {
		s.Push(elem)
	}
	assert.Equal(t, len(elems), s.Len())

	iteratedElems := make([]Element, 0)

	s.Iter(func(elem Element) {
		iteratedElems = append(iteratedElems, elem)
	})

	// Top to bottom order of iteration.
	poppedElems := make([]Element, 0)
	for s.Len() != 0 {
		poppedElems = append(poppedElems, s.Pop())
	}
	assert.Equal(t, poppedElems, iteratedElems)
}

func TestBackIteration(t *testing.T) {
	var (
		n     = 10
		s     = NewStack("test")
		elems = makeElements(n)
	)
	for _, elem := range elems {
		s.Push(elem)
	}
	assert.Equal(t, len(elems), s.Len())

	iteratedElems := make([]Element, 0)

	s.IterBack(func(elem Element) {
		iteratedElems = append(iteratedElems, elem)
	})
	// Bottom to the top order of iteration.
	assert.Equal(t, elems, iteratedElems)
}

func TestPushVal(t *testing.T) {
	s := NewStack("test")

	// integer
	s.PushVal(2)
	elem := s.Pop()
	assert.Equal(t, int64(2), elem.BigInt().Int64())

	// byteArray
	s.PushVal([]byte("foo"))
	elem = s.Pop()
	assert.Equal(t, "foo", string(elem.Bytes()))

	// boolean
	s.PushVal(true)
	elem = s.Pop()
	assert.Equal(t, true, elem.Bool())

	// array
	s.PushVal([]stackitem.Item{stackitem.NewBool(true), stackitem.NewBool(false), stackitem.NewBool(true)})
	elem = s.Pop()
	assert.IsType(t, elem.value, &stackitem.Array{})
}

func TestStack_ToArray(t *testing.T) {
	t.Run("Empty", func(t *testing.T) {
		s := NewStack("test")
		items := s.ToArray()
		require.Equal(t, 0, len(items))
	})
	t.Run("NonEmpty", func(t *testing.T) {
		s := NewStack("test")
		expected := []stackitem.Item{stackitem.Make(1), stackitem.Make(true)}
		for i := range expected {
			s.PushVal(expected[i])
		}
		require.Equal(t, expected, s.ToArray())
	})
}

func TestSwapElemValues(t *testing.T) {
	s := NewStack("test")

	s.PushVal(2)
	s.PushVal(4)

	assert.NoError(t, s.Swap(0, 1))
	assert.Equal(t, int64(2), s.Pop().BigInt().Int64())
	assert.Equal(t, int64(4), s.Pop().BigInt().Int64())

	s.PushVal(1)
	s.PushVal(2)
	s.PushVal(3)
	s.PushVal(4)

	assert.NoError(t, s.Swap(1, 3))
	assert.Equal(t, int64(4), s.Pop().BigInt().Int64())
	assert.Equal(t, int64(1), s.Pop().BigInt().Int64())
	assert.Equal(t, int64(2), s.Pop().BigInt().Int64())
	assert.Equal(t, int64(3), s.Pop().BigInt().Int64())

	s.PushVal(1)
	s.PushVal(2)
	s.PushVal(3)
	s.PushVal(4)

	assert.Error(t, s.Swap(-1, 0))
	assert.Error(t, s.Swap(0, -3))
	assert.Error(t, s.Swap(0, 4))
	assert.Error(t, s.Swap(5, 0))

	assert.NoError(t, s.Swap(1, 1))
	assert.Equal(t, int64(4), s.Pop().BigInt().Int64())
	assert.Equal(t, int64(3), s.Pop().BigInt().Int64())
	assert.Equal(t, int64(2), s.Pop().BigInt().Int64())
	assert.Equal(t, int64(1), s.Pop().BigInt().Int64())
}

func TestRoll(t *testing.T) {
	s := NewStack("test")

	s.PushVal(1)
	s.PushVal(2)
	s.PushVal(3)
	s.PushVal(4)

	assert.NoError(t, s.Roll(2))
	assert.Equal(t, int64(2), s.Pop().BigInt().Int64())
	assert.Equal(t, int64(4), s.Pop().BigInt().Int64())
	assert.Equal(t, int64(3), s.Pop().BigInt().Int64())
	assert.Equal(t, int64(1), s.Pop().BigInt().Int64())

	s.PushVal(1)
	s.PushVal(2)
	s.PushVal(3)
	s.PushVal(4)

	assert.NoError(t, s.Roll(3))
	assert.Equal(t, int64(1), s.Pop().BigInt().Int64())
	assert.Equal(t, int64(4), s.Pop().BigInt().Int64())
	assert.Equal(t, int64(3), s.Pop().BigInt().Int64())
	assert.Equal(t, int64(2), s.Pop().BigInt().Int64())

	s.PushVal(1)
	s.PushVal(2)
	s.PushVal(3)
	s.PushVal(4)

	assert.Error(t, s.Roll(-1))
	assert.Error(t, s.Roll(4))

	assert.NoError(t, s.Roll(0))
	assert.Equal(t, int64(4), s.Pop().BigInt().Int64())
	assert.Equal(t, int64(3), s.Pop().BigInt().Int64())
	assert.Equal(t, int64(2), s.Pop().BigInt().Int64())
	assert.Equal(t, int64(1), s.Pop().BigInt().Int64())
}

func TestInsertAt(t *testing.T) {
	s := NewStack("stack")
	s.PushVal(1)
	s.PushVal(2)
	s.PushVal(3)
	s.PushVal(4)
	s.PushVal(5)

	e := s.Dup(1) // it's `4`
	s.InsertAt(e, 3)

	assert.Equal(t, int64(5), s.Peek(0).BigInt().Int64())
	assert.Equal(t, int64(4), s.Peek(1).BigInt().Int64())
	assert.Equal(t, int64(3), s.Peek(2).BigInt().Int64())
	assert.Equal(t, int64(4), s.Peek(3).BigInt().Int64())
	assert.Equal(t, int64(2), s.Peek(4).BigInt().Int64())
	assert.Equal(t, int64(1), s.Peek(5).BigInt().Int64())
}

func TestPopSigElements(t *testing.T) {
	s := NewStack("test")

	_, err := s.PopSigElements()
	assert.NotNil(t, err)

	s.PushVal([]stackitem.Item{})
	_, err = s.PopSigElements()
	assert.NotNil(t, err)

	s.PushVal([]stackitem.Item{stackitem.NewBool(false)})
	_, err = s.PopSigElements()
	assert.NotNil(t, err)

	b1 := []byte("smth")
	b2 := []byte("strange")
	s.PushVal([]stackitem.Item{stackitem.NewByteArray(b1), stackitem.NewByteArray(b2)})
	z, err := s.PopSigElements()
	assert.Nil(t, err)
	assert.Equal(t, z, [][]byte{b1, b2})

	s.PushVal(2)
	_, err = s.PopSigElements()
	assert.NotNil(t, err)

	s.PushVal(b1)
	s.PushVal(2)
	_, err = s.PopSigElements()
	assert.NotNil(t, err)

	s.PushVal(b2)
	s.PushVal(b1)
	s.PushVal(2)
	z, err = s.PopSigElements()
	assert.Nil(t, err)
	assert.Equal(t, z, [][]byte{b1, b2})
}

func makeElements(n int) []Element {
	elems := make([]Element, n)
	for i := range n {
		elems[i] = NewElement(i)
	}
	return elems
}
