// Copyright (c) 2013-2014 Conformal Systems LLC.
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.

package btcscript_test

import (
	"bytes"
	"errors"
	"fmt"
	"github.com/conformal/btcscript"
	"math/big"
	"testing"
)

type stackTest struct {
	name           string
	before         [][]byte
	operation      func(*btcscript.Stack) error
	expectedReturn error
	after          [][]byte
}

var stackTests = []stackTest{
	{
		"noop",
		[][]byte{{1}, {2}, {3}, {4}, {5}},
		func(stack *btcscript.Stack) error {
			return nil
		},
		nil,
		[][]byte{{1}, {2}, {3}, {4}, {5}},
	},
	{
		"peek underflow (byte)",
		[][]byte{{1}, {2}, {3}, {4}, {5}},
		func(stack *btcscript.Stack) error {
			_, err := stack.PeekByteArray(5)
			return err
		},
		btcscript.StackErrUnderflow,
		[][]byte{},
	},
	{
		"peek underflow (int)",
		[][]byte{{1}, {2}, {3}, {4}, {5}},
		func(stack *btcscript.Stack) error {
			_, err := stack.PeekInt(5)
			return err
		},
		btcscript.StackErrUnderflow,
		[][]byte{},
	},
	{
		"peek underflow (bool)",
		[][]byte{{1}, {2}, {3}, {4}, {5}},
		func(stack *btcscript.Stack) error {
			_, err := stack.PeekBool(5)
			return err
		},
		btcscript.StackErrUnderflow,
		[][]byte{},
	},
	{
		"pop",
		[][]byte{{1}, {2}, {3}, {4}, {5}},
		func(stack *btcscript.Stack) error {
			val, err := stack.PopByteArray()
			if err != nil {
				return err
			}
			if !bytes.Equal(val, []byte{5}) {
				return errors.New("not equal!")
			}
			return err
		},
		nil,
		[][]byte{{1}, {2}, {3}, {4}},
	},
	{
		"pop",
		[][]byte{{1}, {2}, {3}, {4}, {5}},
		func(stack *btcscript.Stack) error {
			val, err := stack.PopByteArray()
			if err != nil {
				return err
			}
			if !bytes.Equal(val, []byte{5}) {
				return errors.New("not equal!")
			}
			return err
		},
		nil,
		[][]byte{{1}, {2}, {3}, {4}},
	},
	{
		"pop everything",
		[][]byte{{1}, {2}, {3}, {4}, {5}},
		func(stack *btcscript.Stack) error {
			for i := 0; i < 5; i++ {
				_, err := stack.PopByteArray()
				if err != nil {
					return err
				}
			}
			return nil
		},
		nil,
		[][]byte{},
	},
	{
		"pop underflow",
		[][]byte{{1}, {2}, {3}, {4}, {5}},
		func(stack *btcscript.Stack) error {
			for i := 0; i < 6; i++ {
				_, err := stack.PopByteArray()
				if err != nil {
					return err
				}
			}
			return nil
		},
		btcscript.StackErrUnderflow,
		[][]byte{},
	},
	{
		"pop bool",
		[][]byte{{0}},
		func(stack *btcscript.Stack) error {
			val, err := stack.PopBool()
			if err != nil {
				return err
			}

			if val != false {
				return errors.New("unexpected value")
			}
			return nil
		},
		nil,
		[][]byte{},
	},
	{
		"pop bool",
		[][]byte{{1}},
		func(stack *btcscript.Stack) error {
			val, err := stack.PopBool()
			if err != nil {
				return err
			}

			if val != true {
				return errors.New("unexpected value")
			}
			return nil
		},
		nil,
		[][]byte{},
	},
	{
		"pop bool",
		[][]byte{},
		func(stack *btcscript.Stack) error {
			_, err := stack.PopBool()
			if err != nil {
				return err
			}

			return nil
		},
		btcscript.StackErrUnderflow,
		[][]byte{},
	},
	{
		"popInt 0",
		[][]byte{{0x0}},
		func(stack *btcscript.Stack) error {
			v, err := stack.PopInt()
			if err != nil {
				return err
			}
			if v.Sign() != 0 {
				return errors.New("0 != 0 on popInt")
			}
			return nil
		},
		nil,
		[][]byte{},
	},
	{
		"popInt -0",
		[][]byte{{0x80}},
		func(stack *btcscript.Stack) error {
			v, err := stack.PopInt()
			if err != nil {
				return err
			}
			if v.Sign() != 0 {
				return errors.New("-0 != 0 on popInt")
			}
			return nil
		},
		nil,
		[][]byte{},
	},
	{
		"popInt 1",
		[][]byte{{0x01}},
		func(stack *btcscript.Stack) error {
			v, err := stack.PopInt()
			if err != nil {
				return err
			}
			if v.Cmp(big.NewInt(1)) != 0 {
				return errors.New("1 != 1 on popInt")
			}
			return nil
		},
		nil,
		[][]byte{},
	},
	{
		"popInt 1 leading 0",
		[][]byte{{0x01, 0x00, 0x00, 0x00}},
		func(stack *btcscript.Stack) error {
			v, err := stack.PopInt()
			if err != nil {
				return err
			}
			if v.Cmp(big.NewInt(1)) != 0 {
				fmt.Printf("%v != %v\n", v, big.NewInt(1))
				return errors.New("1 != 1 on popInt")
			}
			return nil
		},
		nil,
		[][]byte{},
	},
	{
		"popInt -1",
		[][]byte{{0x81}},
		func(stack *btcscript.Stack) error {
			v, err := stack.PopInt()
			if err != nil {
				return err
			}
			if v.Cmp(big.NewInt(-1)) != 0 {
				return errors.New("1 != 1 on popInt")
			}
			return nil
		},
		nil,
		[][]byte{},
	},
	{
		"popInt -1 leading 0",
		[][]byte{{0x01, 0x00, 0x00, 0x80}},
		func(stack *btcscript.Stack) error {
			v, err := stack.PopInt()
			if err != nil {
				return err
			}
			if v.Cmp(big.NewInt(-1)) != 0 {
				fmt.Printf("%v != %v\n", v, big.NewInt(-1))
				return errors.New("-1 != -1 on popInt")
			}
			return nil
		},
		nil,
		[][]byte{},
	},
	// Triggers the multibyte case in asInt
	{
		"popInt -513",
		[][]byte{{0x1, 0x82}},
		func(stack *btcscript.Stack) error {
			v, err := stack.PopInt()
			if err != nil {
				return err
			}
			if v.Cmp(big.NewInt(-513)) != 0 {
				fmt.Printf("%v != %v\n", v, big.NewInt(-513))
				return errors.New("1 != 1 on popInt")
			}
			return nil
		},
		nil,
		[][]byte{},
	},
	// Confirm that the asInt code doesn't modify the base data.
	{
		"peekint nomodify -1",
		[][]byte{{0x01, 0x00, 0x00, 0x80}},
		func(stack *btcscript.Stack) error {
			v, err := stack.PeekInt(0)
			if err != nil {
				return err
			}
			if v.Cmp(big.NewInt(-1)) != 0 {
				fmt.Printf("%v != %v\n", v, big.NewInt(-1))
				return errors.New("-1 != -1 on popInt")
			}
			return nil
		},
		nil,
		[][]byte{{0x01, 0x00, 0x00, 0x80}},
	},
	{
		"PushInt 0",
		[][]byte{},
		func(stack *btcscript.Stack) error {
			stack.PushInt(big.NewInt(0))
			return nil
		},
		nil,
		[][]byte{{}},
	},
	{
		"PushInt 1",
		[][]byte{},
		func(stack *btcscript.Stack) error {
			stack.PushInt(big.NewInt(1))
			return nil
		},
		nil,
		[][]byte{{0x1}},
	},
	{
		"PushInt -1",
		[][]byte{},
		func(stack *btcscript.Stack) error {
			stack.PushInt(big.NewInt(-1))
			return nil
		},
		nil,
		[][]byte{{0x81}},
	},
	{
		"PushInt two bytes",
		[][]byte{},
		func(stack *btcscript.Stack) error {
			stack.PushInt(big.NewInt(256))
			return nil
		},
		nil,
		// little endian.. *sigh*
		[][]byte{{0x00, 0x01}},
	},
	{
		"PushInt leading zeros",
		[][]byte{},
		func(stack *btcscript.Stack) error {
			// this will have the highbit set
			stack.PushInt(big.NewInt(128))
			return nil
		},
		nil,
		[][]byte{{0x80, 0x00}},
	},
	{
		"dup",
		[][]byte{{1}},
		func(stack *btcscript.Stack) error {
			err := stack.DupN(1)
			if err != nil {
				return err
			}

			return nil
		},
		nil,
		[][]byte{{1}, {1}},
	},
	{
		"dup2",
		[][]byte{{1}, {2}},
		func(stack *btcscript.Stack) error {
			err := stack.DupN(2)
			if err != nil {
				return err
			}

			return nil
		},
		nil,
		[][]byte{{1}, {2}, {1}, {2}},
	},
	{
		"dup3",
		[][]byte{{1}, {2}, {3}},
		func(stack *btcscript.Stack) error {
			err := stack.DupN(3)
			if err != nil {
				return err
			}

			return nil
		},
		nil,
		[][]byte{{1}, {2}, {3}, {1}, {2}, {3}},
	},
	{
		"dup0",
		[][]byte{{1}},
		func(stack *btcscript.Stack) error {
			err := stack.DupN(0)
			if err != nil {
				return err
			}

			return nil
		},
		btcscript.StackErrInvalidArgs,
		[][]byte{},
	},
	{
		"dup-1",
		[][]byte{{1}},
		func(stack *btcscript.Stack) error {
			err := stack.DupN(-1)
			if err != nil {
				return err
			}

			return nil
		},
		btcscript.StackErrInvalidArgs,
		[][]byte{},
	},
	{
		"dup too much",
		[][]byte{{1}},
		func(stack *btcscript.Stack) error {
			err := stack.DupN(2)
			if err != nil {
				return err
			}

			return nil
		},
		btcscript.StackErrUnderflow,
		[][]byte{},
	},
	{
		"dup-1",
		[][]byte{{1}},
		func(stack *btcscript.Stack) error {
			err := stack.DupN(-1)
			if err != nil {
				return err
			}

			return nil
		},
		btcscript.StackErrInvalidArgs,
		[][]byte{},
	},
	{
		"PushBool true",
		[][]byte{},
		func(stack *btcscript.Stack) error {
			stack.PushBool(true)

			return nil
		},
		nil,
		[][]byte{{1}},
	},
	{
		"PushBool false",
		[][]byte{},
		func(stack *btcscript.Stack) error {
			stack.PushBool(false)

			return nil
		},
		nil,
		[][]byte{{0}},
	},
	{
		"PushBool PopBool",
		[][]byte{},
		func(stack *btcscript.Stack) error {
			stack.PushBool(true)
			val, err := stack.PopBool()
			if err != nil {
				return err
			}
			if val != true {
				return errors.New("unexpected value")
			}

			return nil
		},
		nil,
		[][]byte{},
	},
	{
		"PushBool PopBool 2",
		[][]byte{},
		func(stack *btcscript.Stack) error {
			stack.PushBool(false)
			val, err := stack.PopBool()
			if err != nil {
				return err
			}
			if val != false {
				return errors.New("unexpected value")
			}

			return nil
		},
		nil,
		[][]byte{},
	},
	{
		"PushInt PopBool",
		[][]byte{},
		func(stack *btcscript.Stack) error {
			stack.PushInt(big.NewInt(1))
			val, err := stack.PopBool()
			if err != nil {
				return err
			}
			if val != true {
				return errors.New("unexpected value")
			}

			return nil
		},
		nil,
		[][]byte{},
	},
	{
		"PushInt PopBool 2",
		[][]byte{},
		func(stack *btcscript.Stack) error {
			stack.PushInt(big.NewInt(0))
			val, err := stack.PopBool()
			if err != nil {
				return err
			}
			if val != false {
				return errors.New("unexpected value")
			}

			return nil
		},
		nil,
		[][]byte{},
	},
	{
		"PushInt PopBool 2",
		[][]byte{},
		func(stack *btcscript.Stack) error {
			stack.PushInt(big.NewInt(0))
			val, err := stack.PopBool()
			if err != nil {
				return err
			}
			if val != false {
				return errors.New("unexpected value")
			}

			return nil
		},
		nil,
		[][]byte{},
	},
	{
		"Nip top",
		[][]byte{{1}, {2}, {3}},
		func(stack *btcscript.Stack) error {
			return stack.NipN(0)
		},
		nil,
		[][]byte{{1}, {2}},
	},
	{
		"Nip middle",
		[][]byte{{1}, {2}, {3}},
		func(stack *btcscript.Stack) error {
			return stack.NipN(1)
		},
		nil,
		[][]byte{{1}, {3}},
	},
	{
		"Nip low",
		[][]byte{{1}, {2}, {3}},
		func(stack *btcscript.Stack) error {
			return stack.NipN(2)
		},
		nil,
		[][]byte{{2}, {3}},
	},
	{
		"Nip too much",
		[][]byte{{1}, {2}, {3}},
		func(stack *btcscript.Stack) error {
			// bite off more than we can chew
			return stack.NipN(3)
		},
		btcscript.StackErrUnderflow,
		[][]byte{{2}, {3}},
	},
	{
		"Nip too much",
		[][]byte{{1}, {2}, {3}},
		func(stack *btcscript.Stack) error {
			// bite off more than we can chew
			return stack.NipN(3)
		},
		btcscript.StackErrUnderflow,
		[][]byte{{2}, {3}},
	},
	{
		"keep on tucking",
		[][]byte{{1}, {2}, {3}},
		func(stack *btcscript.Stack) error {
			return stack.Tuck()
		},
		nil,
		[][]byte{{1}, {3}, {2}, {3}},
	},
	{
		"a little tucked up",
		[][]byte{{1}}, // too few arguments for tuck
		func(stack *btcscript.Stack) error {
			return stack.Tuck()
		},
		btcscript.StackErrUnderflow,
		[][]byte{},
	},
	{
		"all tucked up",
		[][]byte{}, // too few arguments  for tuck
		func(stack *btcscript.Stack) error {
			return stack.Tuck()
		},
		btcscript.StackErrUnderflow,
		[][]byte{},
	},
	{
		"drop 1",
		[][]byte{{1}, {2}, {3}, {4}},
		func(stack *btcscript.Stack) error {
			return stack.DropN(1)
		},
		nil,
		[][]byte{{1}, {2}, {3}},
	},
	{
		"drop 2",
		[][]byte{{1}, {2}, {3}, {4}},
		func(stack *btcscript.Stack) error {
			return stack.DropN(2)
		},
		nil,
		[][]byte{{1}, {2}},
	},
	{
		"drop 3",
		[][]byte{{1}, {2}, {3}, {4}},
		func(stack *btcscript.Stack) error {
			return stack.DropN(3)
		},
		nil,
		[][]byte{{1}},
	},
	{
		"drop 4",
		[][]byte{{1}, {2}, {3}, {4}},
		func(stack *btcscript.Stack) error {
			return stack.DropN(4)
		},
		nil,
		[][]byte{},
	},
	{
		"drop 4/5",
		[][]byte{{1}, {2}, {3}, {4}},
		func(stack *btcscript.Stack) error {
			return stack.DropN(5)
		},
		btcscript.StackErrUnderflow,
		[][]byte{},
	},
	{
		"drop invalid",
		[][]byte{{1}, {2}, {3}, {4}},
		func(stack *btcscript.Stack) error {
			return stack.DropN(0)
		},
		btcscript.StackErrInvalidArgs,
		[][]byte{},
	},
	{
		"Rot1",
		[][]byte{{1}, {2}, {3}, {4}},
		func(stack *btcscript.Stack) error {
			return stack.RotN(1)
		},
		nil,
		[][]byte{{1}, {3}, {4}, {2}},
	},
	{
		"Rot2",
		[][]byte{{1}, {2}, {3}, {4}, {5}, {6}},
		func(stack *btcscript.Stack) error {
			return stack.RotN(2)
		},
		nil,
		[][]byte{{3}, {4}, {5}, {6}, {1}, {2}},
	},
	{
		"Rot too little",
		[][]byte{{1}, {2}},
		func(stack *btcscript.Stack) error {
			return stack.RotN(1)
		},
		btcscript.StackErrUnderflow,
		[][]byte{},
	},
	{
		"Rot0",
		[][]byte{{1}, {2}, {3}},
		func(stack *btcscript.Stack) error {
			return stack.RotN(0)
		},
		btcscript.StackErrInvalidArgs,
		[][]byte{},
	},
	{
		"Swap1",
		[][]byte{{1}, {2}, {3}, {4}},
		func(stack *btcscript.Stack) error {
			return stack.SwapN(1)
		},
		nil,
		[][]byte{{1}, {2}, {4}, {3}},
	},
	{
		"Swap2",
		[][]byte{{1}, {2}, {3}, {4}},
		func(stack *btcscript.Stack) error {
			return stack.SwapN(2)
		},
		nil,
		[][]byte{{3}, {4}, {1}, {2}},
	},
	{
		"Swap too little",
		[][]byte{{1}},
		func(stack *btcscript.Stack) error {
			return stack.SwapN(1)
		},
		btcscript.StackErrUnderflow,
		[][]byte{},
	},
	{
		"Swap0",
		[][]byte{{1}, {2}, {3}},
		func(stack *btcscript.Stack) error {
			return stack.SwapN(0)
		},
		btcscript.StackErrInvalidArgs,
		[][]byte{},
	},
	{
		"Over1",
		[][]byte{{1}, {2}, {3}, {4}},
		func(stack *btcscript.Stack) error {
			return stack.OverN(1)
		},
		nil,
		[][]byte{{1}, {2}, {3}, {4}, {3}},
	},
	{
		"Over2",
		[][]byte{{1}, {2}, {3}, {4}},
		func(stack *btcscript.Stack) error {
			return stack.OverN(2)
		},
		nil,
		[][]byte{{1}, {2}, {3}, {4}, {1}, {2}},
	},
	{
		"Over too little",
		[][]byte{{1}},
		func(stack *btcscript.Stack) error {
			return stack.OverN(1)
		},
		btcscript.StackErrUnderflow,
		[][]byte{},
	},
	{
		"Over0",
		[][]byte{{1}, {2}, {3}},
		func(stack *btcscript.Stack) error {
			return stack.OverN(0)
		},
		btcscript.StackErrInvalidArgs,
		[][]byte{},
	},
	{
		"Pick1",
		[][]byte{{1}, {2}, {3}, {4}},
		func(stack *btcscript.Stack) error {
			return stack.PickN(1)
		},
		nil,
		[][]byte{{1}, {2}, {3}, {4}, {3}},
	},
	{
		"Pick2",
		[][]byte{{1}, {2}, {3}, {4}},
		func(stack *btcscript.Stack) error {
			return stack.PickN(2)
		},
		nil,
		[][]byte{{1}, {2}, {3}, {4}, {2}},
	},
	{
		"Pick too little",
		[][]byte{{1}},
		func(stack *btcscript.Stack) error {
			return stack.PickN(1)
		},
		btcscript.StackErrUnderflow,
		[][]byte{},
	},
	{
		"Roll1",
		[][]byte{{1}, {2}, {3}, {4}},
		func(stack *btcscript.Stack) error {
			return stack.RollN(1)
		},
		nil,
		[][]byte{{1}, {2}, {4}, {3}},
	},
	{
		"Roll2",
		[][]byte{{1}, {2}, {3}, {4}},
		func(stack *btcscript.Stack) error {
			return stack.RollN(2)
		},
		nil,
		[][]byte{{1}, {3}, {4}, {2}},
	},
	{
		"Roll too little",
		[][]byte{{1}},
		func(stack *btcscript.Stack) error {
			return stack.RollN(1)
		},
		btcscript.StackErrUnderflow,
		[][]byte{},
	},
	{
		"Peek bool",
		[][]byte{{1}},
		func(stack *btcscript.Stack) error {
			// Peek bool is otherwise pretty well tested, just check
			// it works.
			val, err := stack.PeekBool(0)
			if err != nil {
				return err
			}
			if val != true {
				return errors.New("invalid result")
			}
			return nil
		},
		nil,
		[][]byte{{1}},
	},
	{
		"Peek bool 2",
		[][]byte{{0}},
		func(stack *btcscript.Stack) error {
			// Peek bool is otherwise pretty well tested, just check
			// it works.
			val, err := stack.PeekBool(0)
			if err != nil {
				return err
			}
			if val != false {
				return errors.New("invalid result")
			}
			return nil
		},
		nil,
		[][]byte{{0}},
	},
	{
		"Peek int",
		[][]byte{{1}},
		func(stack *btcscript.Stack) error {
			// Peek int is otherwise pretty well tested, just check
			// it works.
			val, err := stack.PeekInt(0)
			if err != nil {
				return err
			}
			if val.Cmp(big.NewInt(1)) != 0 {
				return errors.New("invalid result")
			}
			return nil
		},
		nil,
		[][]byte{{1}},
	},
	{
		"Peek int 2",
		[][]byte{{0}},
		func(stack *btcscript.Stack) error {
			// Peek int is otherwise pretty well tested, just check
			// it works.
			val, err := stack.PeekInt(0)
			if err != nil {
				return err
			}
			if val.Cmp(big.NewInt(0)) != 0 {
				return errors.New("invalid result")
			}
			return nil
		},
		nil,
		[][]byte{{0}},
	},
	{
		"pop int",
		[][]byte{},
		func(stack *btcscript.Stack) error {
			stack.PushInt(big.NewInt(1))
			// Peek int is otherwise pretty well tested, just check
			// it works.
			val, err := stack.PopInt()
			if err != nil {
				return err
			}
			if val.Cmp(big.NewInt(1)) != 0 {
				return errors.New("invalid result")
			}
			return nil
		},
		nil,
		[][]byte{},
	},
	{
		"pop empty",
		[][]byte{},
		func(stack *btcscript.Stack) error {
			// Peek int is otherwise pretty well tested, just check
			// it works.
			_, err := stack.PopInt()
			return err
		},
		btcscript.StackErrUnderflow,
		[][]byte{},
	},
}

func doTest(t *testing.T, test stackTest) {
	stack := btcscript.Stack{}

	for i := range test.before {
		stack.PushByteArray(test.before[i])
	}
	err := test.operation(&stack)
	if err != test.expectedReturn {
		t.Errorf("%s: operation return not what expected: %v vs %v",
			test.name, err, test.expectedReturn)
	}
	if err != nil {
		return
	}
	if len(test.after) != stack.Depth() {
		t.Errorf("%s: stack depth doesn't match expected: %v vs %v",
			test.name, len(test.after), stack.Depth())
	}
	for i := range test.after {
		val, err := stack.PeekByteArray(stack.Depth() - i - 1)
		if err != nil {
			t.Errorf("%s: can't peek %dth stack entry: %v",
				test.name, i, err)
			break
		}

		if !bytes.Equal(val, test.after[i]) {
			t.Errorf("%s: %dth stack entry doesn't match "+
				"expected: %v vs %v", test.name, i, val,
				test.after[i])
			break
		}
	}
}

func TestStack(t *testing.T) {
	for i := range stackTests {
		doTest(t, stackTests[i])
	}
}