package vm

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

	"github.com/nspcc-dev/neo-go/internal/random"
	"github.com/nspcc-dev/neo-go/pkg/core/interop/interopnames"
	"github.com/nspcc-dev/neo-go/pkg/smartcontract"
	"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
	"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
	"github.com/stretchr/testify/require"
)

type arrayIterator struct {
	index  int
	values []stackitem.Item
}

func TestCreateCallAndUnwrapIteratorScript(t *testing.T) {
	ctrHash := random.Uint160()
	ctrMethod := "mymethod"
	param := stackitem.NewBigInteger(big.NewInt(42))

	const totalItems = 8
	values := make([]stackitem.Item, totalItems)
	for i := range values {
		values[i] = stackitem.NewBigInteger(big.NewInt(int64(i)))
	}

	checkStack := func(t *testing.T, script []byte, index int, prefetch bool) {
		v := load(script)
		it := &arrayIterator{index: -1, values: values}
		v.SyscallHandler = func(v *VM, id uint32) error {
			switch id {
			case interopnames.ToID([]byte(interopnames.SystemContractCall)):
				require.Equal(t, ctrHash.BytesBE(), v.Estack().Pop().Value())
				require.Equal(t, []byte(ctrMethod), v.Estack().Pop().Value())
				require.Equal(t, big.NewInt(int64(callflag.All)), v.Estack().Pop().Value())
				require.Equal(t, []stackitem.Item{param}, v.Estack().Pop().Value())
				v.Estack().PushItem(stackitem.NewInterop(it))
			case interopnames.ToID([]byte(interopnames.SystemIteratorNext)):
				require.Equal(t, it, v.Estack().Pop().Value())
				it.index++
				v.Estack().PushVal(it.index < len(it.values))
			case interopnames.ToID([]byte(interopnames.SystemIteratorValue)):
				require.Equal(t, it, v.Estack().Pop().Value())
				v.Estack().PushVal(it.values[it.index])
			default:
				return fmt.Errorf("unexpected syscall: %d", id)
			}
			return nil
		}
		require.NoError(t, v.Run())

		if prefetch && index <= len(values) {
			require.Equal(t, 2, v.Estack().Len())

			it, ok := v.Estack().Pop().Interop().Value().(*arrayIterator)
			require.True(t, ok)
			require.Equal(t, index-1, it.index)
			require.Equal(t, values[:index], v.Estack().Pop().Array())
			return
		}
		if len(values) < index {
			index = len(values)
		}
		require.Equal(t, 1, v.Estack().Len())
		require.Equal(t, values[:index], v.Estack().Pop().Array())
	}

	t.Run("truncate", func(t *testing.T) {
		t.Run("zero", func(t *testing.T) {
			const index = 0
			script, err := smartcontract.CreateCallAndUnwrapIteratorScript(ctrHash, ctrMethod, index, param)
			require.NoError(t, err)

			// The behaviour is a bit unexpected, but not a problem (why would anyone fetch 0 items).
			// Let's have test, to make it obvious.
			checkStack(t, script, index+1, false)
		})
		t.Run("all", func(t *testing.T) {
			const index = totalItems + 1
			script, err := smartcontract.CreateCallAndUnwrapIteratorScript(ctrHash, ctrMethod, index, param)
			require.NoError(t, err)

			checkStack(t, script, index, false)
		})
		t.Run("partial", func(t *testing.T) {
			const index = totalItems / 2
			script, err := smartcontract.CreateCallAndUnwrapIteratorScript(ctrHash, ctrMethod, index, param)
			require.NoError(t, err)

			checkStack(t, script, index, false)
		})
	})
	t.Run("prefetch", func(t *testing.T) {
		t.Run("zero", func(t *testing.T) {
			const index = 0
			script, err := smartcontract.CreateCallAndPrefetchIteratorScript(ctrHash, ctrMethod, index, param)
			require.NoError(t, err)

			checkStack(t, script, index+1, true)
		})
		t.Run("all", func(t *testing.T) {
			const index = totalItems + 1 // +1 to test with iterator dropped
			script, err := smartcontract.CreateCallAndPrefetchIteratorScript(ctrHash, ctrMethod, index, param)
			require.NoError(t, err)

			checkStack(t, script, index, true)
		})
		t.Run("partial", func(t *testing.T) {
			const index = totalItems / 2
			script, err := smartcontract.CreateCallAndPrefetchIteratorScript(ctrHash, ctrMethod, index, param)
			require.NoError(t, err)

			checkStack(t, script, index, true)
		})
	})
}