package core

import (
	"math/big"
	"testing"

	"github.com/CityOfZion/neo-go/pkg/core/block"
	"github.com/CityOfZion/neo-go/pkg/core/state"
	"github.com/CityOfZion/neo-go/pkg/core/storage"
	"github.com/CityOfZion/neo-go/pkg/core/transaction"
	"github.com/CityOfZion/neo-go/pkg/crypto/keys"
	"github.com/CityOfZion/neo-go/pkg/internal/random"
	"github.com/CityOfZion/neo-go/pkg/smartcontract"
	"github.com/CityOfZion/neo-go/pkg/smartcontract/trigger"
	"github.com/CityOfZion/neo-go/pkg/util"
	"github.com/CityOfZion/neo-go/pkg/vm"
	"github.com/CityOfZion/neo-go/pkg/vm/opcode"
	"github.com/stretchr/testify/require"
)

/*  Missing tests:
 *  TestTxGetReferences
 *  TestTxGetUnspentCoins
 *  TestTxGetWitnesses
 *  TestBcGetAccount
 *  TestBcGetAsset
 *  TestAccountGetBalance
 *  TestAccountIsStandard
 *  TestCreateContractStateFromVM
 *  TestContractCreate
 *  TestContractMigrate
 *  TestAssetCreate
 *  TestAssetRenew
 *  TestRuntimeSerialize
 *  TestRuntimeDeserialize
 */

func TestStorageFind(t *testing.T) {
	v, contractState, context, chain := createVMAndContractState(t)
	defer chain.Close()

	skeys := [][]byte{{0x01, 0x02}, {0x02, 0x01}}
	items := []*state.StorageItem{
		{
			Value: []byte{0x01, 0x02, 0x03, 0x04},
		},
		{
			Value: []byte{0x04, 0x03, 0x02, 0x01},
		},
	}

	require.NoError(t, context.dao.PutContractState(contractState))

	scriptHash := contractState.ScriptHash()

	for i := range skeys {
		err := context.dao.PutStorageItem(scriptHash, skeys[i], items[i])
		require.NoError(t, err)
	}

	t.Run("normal invocation", func(t *testing.T) {
		v.Estack().PushVal([]byte{0x01})
		v.Estack().PushVal(vm.NewInteropItem(&StorageContext{ScriptHash: scriptHash}))

		err := context.storageFind(v)
		require.NoError(t, err)

		var iter *vm.InteropItem
		require.NotPanics(t, func() { iter = v.Estack().Top().Interop() })

		require.NoError(t, context.enumeratorNext(v))
		require.True(t, v.Estack().Pop().Bool())

		v.Estack().PushVal(iter)
		require.NoError(t, context.iteratorKey(v))
		require.Equal(t, []byte{0x01, 0x02}, v.Estack().Pop().Bytes())

		v.Estack().PushVal(iter)
		require.NoError(t, context.enumeratorValue(v))
		require.Equal(t, []byte{0x01, 0x02, 0x03, 0x04}, v.Estack().Pop().Bytes())

		v.Estack().PushVal(iter)
		require.NoError(t, context.enumeratorNext(v))
		require.False(t, v.Estack().Pop().Bool())
	})

	t.Run("invalid type for StorageContext", func(t *testing.T) {
		v.Estack().PushVal([]byte{0x01})
		v.Estack().PushVal(vm.NewInteropItem(nil))

		require.Error(t, context.storageFind(v))
	})

	t.Run("invalid script hash", func(t *testing.T) {
		invalidHash := scriptHash
		invalidHash[0] = ^invalidHash[0]

		v.Estack().PushVal([]byte{0x01})
		v.Estack().PushVal(vm.NewInteropItem(&StorageContext{ScriptHash: invalidHash}))

		require.Error(t, context.storageFind(v))
	})
}

func TestHeaderGetVersion(t *testing.T) {
	v, block, context, chain := createVMAndPushBlock(t)
	defer chain.Close()

	err := context.headerGetVersion(v)
	require.NoError(t, err)
	value := v.Estack().Pop().Value().(*big.Int)
	require.Equal(t, uint64(block.Version), value.Uint64())
}

func TestHeaderGetVersion_Negative(t *testing.T) {
	v := vm.New()
	block := newDumbBlock()
	chain := newTestChain(t)
	defer chain.Close()
	context := chain.newInteropContext(trigger.Application, storage.NewMemoryStore(), block, nil)
	v.Estack().PushVal(vm.NewBoolItem(false))

	err := context.headerGetVersion(v)
	require.Errorf(t, err, "value is not a header or block")
}

func TestHeaderGetConsensusData(t *testing.T) {
	v, block, context, chain := createVMAndPushBlock(t)
	defer chain.Close()

	err := context.headerGetConsensusData(v)
	require.NoError(t, err)
	value := v.Estack().Pop().Value().(*big.Int)
	require.Equal(t, block.ConsensusData, value.Uint64())
}

func TestHeaderGetMerkleRoot(t *testing.T) {
	v, block, context, chain := createVMAndPushBlock(t)
	defer chain.Close()

	err := context.headerGetMerkleRoot(v)
	require.NoError(t, err)
	value := v.Estack().Pop().Value()
	require.Equal(t, block.MerkleRoot.BytesLE(), value)
}

func TestHeaderGetNextConsensus(t *testing.T) {
	v, block, context, chain := createVMAndPushBlock(t)
	defer chain.Close()

	err := context.headerGetNextConsensus(v)
	require.NoError(t, err)
	value := v.Estack().Pop().Value()
	require.Equal(t, block.NextConsensus.BytesLE(), value)
}

func TestTxGetAttributes(t *testing.T) {
	v, tx, context, chain := createVMAndPushTX(t)
	defer chain.Close()

	err := context.txGetAttributes(v)
	require.NoError(t, err)
	value := v.Estack().Pop().Value().([]vm.StackItem)
	require.Equal(t, tx.Attributes[0].Usage, value[0].Value().(*transaction.Attribute).Usage)
}

func TestTxGetInputs(t *testing.T) {
	v, tx, context, chain := createVMAndPushTX(t)
	defer chain.Close()

	err := context.txGetInputs(v)
	require.NoError(t, err)
	value := v.Estack().Pop().Value().([]vm.StackItem)
	require.Equal(t, tx.Inputs[0], *value[0].Value().(*transaction.Input))
}

func TestTxGetOutputs(t *testing.T) {
	v, tx, context, chain := createVMAndPushTX(t)
	defer chain.Close()

	err := context.txGetOutputs(v)
	require.NoError(t, err)
	value := v.Estack().Pop().Value().([]vm.StackItem)
	require.Equal(t, tx.Outputs[0], *value[0].Value().(*transaction.Output))
}

func TestTxGetType(t *testing.T) {
	v, tx, context, chain := createVMAndPushTX(t)
	defer chain.Close()

	err := context.txGetType(v)
	require.NoError(t, err)
	value := v.Estack().Pop().Value().(*big.Int)
	require.Equal(t, big.NewInt(int64(tx.Type)), value)
}

func TestInvocationTxGetScript(t *testing.T) {
	v, tx, context, chain := createVMAndPushTX(t)
	defer chain.Close()

	err := context.invocationTxGetScript(v)
	require.NoError(t, err)
	value := v.Estack().Pop().Value().([]byte)
	inv := tx.Data.(*transaction.InvocationTX)
	require.Equal(t, inv.Script, value)
}

func TestWitnessGetVerificationScript(t *testing.T) {
	v := vm.New()
	script := []byte{byte(opcode.PUSHM1), byte(opcode.RET)}
	witness := transaction.Witness{InvocationScript: nil, VerificationScript: script}

	chain := newTestChain(t)
	defer chain.Close()

	context := chain.newInteropContext(trigger.Application, storage.NewMemoryStore(), nil, nil)
	v.Estack().PushVal(vm.NewInteropItem(&witness))
	err := context.witnessGetVerificationScript(v)
	require.NoError(t, err)
	value := v.Estack().Pop().Value().([]byte)
	require.Equal(t, witness.VerificationScript, value)
}

func TestPopInputFromVM(t *testing.T) {
	v, tx, _, chain := createVMAndTX(t)
	defer chain.Close()
	v.Estack().PushVal(vm.NewInteropItem(&tx.Inputs[0]))

	input, err := popInputFromVM(v)
	require.NoError(t, err)
	require.Equal(t, tx.Inputs[0], *input)
}

func TestInputGetHash(t *testing.T) {
	v, tx, context, chain := createVMAndTX(t)
	defer chain.Close()
	v.Estack().PushVal(vm.NewInteropItem(&tx.Inputs[0]))

	err := context.inputGetHash(v)
	require.NoError(t, err)
	hash := v.Estack().Pop().Value()
	require.Equal(t, tx.Inputs[0].PrevHash.BytesBE(), hash)
}

func TestInputGetIndex(t *testing.T) {
	v, tx, context, chain := createVMAndTX(t)
	defer chain.Close()
	v.Estack().PushVal(vm.NewInteropItem(&tx.Inputs[0]))

	err := context.inputGetIndex(v)
	require.NoError(t, err)
	index := v.Estack().Pop().Value()
	require.Equal(t, big.NewInt(int64(tx.Inputs[0].PrevIndex)), index)
}

func TestPopOutputFromVM(t *testing.T) {
	v, tx, _, chain := createVMAndTX(t)
	defer chain.Close()
	v.Estack().PushVal(vm.NewInteropItem(&tx.Outputs[0]))

	output, err := popOutputFromVM(v)
	require.NoError(t, err)
	require.Equal(t, tx.Outputs[0], *output)
}

func TestOutputGetAssetID(t *testing.T) {
	v, tx, context, chain := createVMAndTX(t)
	defer chain.Close()
	v.Estack().PushVal(vm.NewInteropItem(&tx.Outputs[0]))

	err := context.outputGetAssetID(v)
	require.NoError(t, err)
	assetID := v.Estack().Pop().Value()
	require.Equal(t, tx.Outputs[0].AssetID.BytesBE(), assetID)
}

func TestOutputGetScriptHash(t *testing.T) {
	v, tx, context, chain := createVMAndTX(t)
	defer chain.Close()
	v.Estack().PushVal(vm.NewInteropItem(&tx.Outputs[0]))

	err := context.outputGetScriptHash(v)
	require.NoError(t, err)
	scriptHash := v.Estack().Pop().Value()
	require.Equal(t, tx.Outputs[0].ScriptHash.BytesBE(), scriptHash)
}

func TestOutputGetValue(t *testing.T) {
	v, tx, context, chain := createVMAndTX(t)
	defer chain.Close()
	v.Estack().PushVal(vm.NewInteropItem(&tx.Outputs[0]))

	err := context.outputGetValue(v)
	require.NoError(t, err)
	amount := v.Estack().Pop().Value()
	require.Equal(t, big.NewInt(int64(tx.Outputs[0].Amount)), amount)
}

func TestAttrGetData(t *testing.T) {
	v, tx, context, chain := createVMAndTX(t)
	defer chain.Close()
	v.Estack().PushVal(vm.NewInteropItem(&tx.Attributes[0]))

	err := context.attrGetData(v)
	require.NoError(t, err)
	data := v.Estack().Pop().Value()
	require.Equal(t, tx.Attributes[0].Data, data)
}

func TestAttrGetUsage(t *testing.T) {
	v, tx, context, chain := createVMAndTX(t)
	defer chain.Close()
	v.Estack().PushVal(vm.NewInteropItem(&tx.Attributes[0]))

	err := context.attrGetUsage(v)
	require.NoError(t, err)
	usage := v.Estack().Pop().Value()
	require.Equal(t, big.NewInt(int64(tx.Attributes[0].Usage)), usage)
}

func TestAccountGetScriptHash(t *testing.T) {
	v, accState, context, chain := createVMAndAccState(t)
	defer chain.Close()
	v.Estack().PushVal(vm.NewInteropItem(accState))

	err := context.accountGetScriptHash(v)
	require.NoError(t, err)
	hash := v.Estack().Pop().Value()
	require.Equal(t, accState.ScriptHash.BytesBE(), hash)
}

func TestAccountGetVotes(t *testing.T) {
	v, accState, context, chain := createVMAndAccState(t)
	defer chain.Close()
	v.Estack().PushVal(vm.NewInteropItem(accState))

	err := context.accountGetVotes(v)
	require.NoError(t, err)
	votes := v.Estack().Pop().Value().([]vm.StackItem)
	require.Equal(t, vm.NewByteArrayItem(accState.Votes[0].Bytes()), votes[0])
}

func TestContractGetScript(t *testing.T) {
	v, contractState, context, chain := createVMAndContractState(t)
	defer chain.Close()
	v.Estack().PushVal(vm.NewInteropItem(contractState))

	err := context.contractGetScript(v)
	require.NoError(t, err)
	script := v.Estack().Pop().Value()
	require.Equal(t, contractState.Script, script)
}

func TestContractIsPayable(t *testing.T) {
	v, contractState, context, chain := createVMAndContractState(t)
	defer chain.Close()
	v.Estack().PushVal(vm.NewInteropItem(contractState))

	err := context.contractIsPayable(v)
	require.NoError(t, err)
	isPayable := v.Estack().Pop().Value()
	require.Equal(t, contractState.IsPayable(), isPayable)
}

func TestAssetGetAdmin(t *testing.T) {
	v, assetState, context, chain := createVMAndAssetState(t)
	defer chain.Close()
	v.Estack().PushVal(vm.NewInteropItem(assetState))

	err := context.assetGetAdmin(v)
	require.NoError(t, err)
	admin := v.Estack().Pop().Value()
	require.Equal(t, assetState.Admin.BytesBE(), admin)
}

func TestAssetGetAmount(t *testing.T) {
	v, assetState, context, chain := createVMAndAssetState(t)
	defer chain.Close()
	v.Estack().PushVal(vm.NewInteropItem(assetState))

	err := context.assetGetAmount(v)
	require.NoError(t, err)
	amount := v.Estack().Pop().Value()
	require.Equal(t, big.NewInt(int64(assetState.Amount)), amount)
}

func TestAssetGetAssetID(t *testing.T) {
	v, assetState, context, chain := createVMAndAssetState(t)
	defer chain.Close()
	v.Estack().PushVal(vm.NewInteropItem(assetState))

	err := context.assetGetAssetID(v)
	require.NoError(t, err)
	assetID := v.Estack().Pop().Value()
	require.Equal(t, assetState.ID.BytesBE(), assetID)
}

func TestAssetGetAssetType(t *testing.T) {
	v, assetState, context, chain := createVMAndAssetState(t)
	defer chain.Close()
	v.Estack().PushVal(vm.NewInteropItem(assetState))

	err := context.assetGetAssetType(v)
	require.NoError(t, err)
	assetType := v.Estack().Pop().Value()
	require.Equal(t, big.NewInt(int64(assetState.AssetType)), assetType)
}

func TestAssetGetAvailable(t *testing.T) {
	v, assetState, context, chain := createVMAndAssetState(t)
	defer chain.Close()
	v.Estack().PushVal(vm.NewInteropItem(assetState))

	err := context.assetGetAvailable(v)
	require.NoError(t, err)
	available := v.Estack().Pop().Value()
	require.Equal(t, big.NewInt(int64(assetState.Available)), available)
}

func TestAssetGetIssuer(t *testing.T) {
	v, assetState, context, chain := createVMAndAssetState(t)
	defer chain.Close()
	v.Estack().PushVal(vm.NewInteropItem(assetState))

	err := context.assetGetIssuer(v)
	require.NoError(t, err)
	issuer := v.Estack().Pop().Value()
	require.Equal(t, assetState.Issuer.BytesBE(), issuer)
}

func TestAssetGetOwner(t *testing.T) {
	v, assetState, context, chain := createVMAndAssetState(t)
	defer chain.Close()
	v.Estack().PushVal(vm.NewInteropItem(assetState))

	err := context.assetGetOwner(v)
	require.NoError(t, err)
	owner := v.Estack().Pop().Value()
	require.Equal(t, assetState.Owner.Bytes(), owner)
}

func TestAssetGetPrecision(t *testing.T) {
	v, assetState, context, chain := createVMAndAssetState(t)
	defer chain.Close()
	v.Estack().PushVal(vm.NewInteropItem(assetState))

	err := context.assetGetPrecision(v)
	require.NoError(t, err)
	precision := v.Estack().Pop().Value()
	require.Equal(t, big.NewInt(int64(assetState.Precision)), precision)
}

// Helper functions to create VM, InteropContext, TX, Account, Contract, Asset.

func createVMAndPushBlock(t *testing.T) (*vm.VM, *block.Block, *interopContext, *Blockchain) {
	v := vm.New()
	block := newDumbBlock()
	chain := newTestChain(t)
	context := chain.newInteropContext(trigger.Application, storage.NewMemoryStore(), block, nil)
	v.Estack().PushVal(vm.NewInteropItem(block))
	return v, block, context, chain
}

func createVMAndPushTX(t *testing.T) (*vm.VM, *transaction.Transaction, *interopContext, *Blockchain) {
	v, tx, context, chain := createVMAndTX(t)
	v.Estack().PushVal(vm.NewInteropItem(tx))
	return v, tx, context, chain
}

func createVMAndAssetState(t *testing.T) (*vm.VM, *state.Asset, *interopContext, *Blockchain) {
	v := vm.New()
	assetState := &state.Asset{
		ID:         util.Uint256{},
		AssetType:  transaction.GoverningToken,
		Name:       "TestAsset",
		Amount:     1,
		Available:  2,
		Precision:  1,
		FeeMode:    1,
		FeeAddress: random.Uint160(),
		Owner:      keys.PublicKey{X: big.NewInt(1), Y: big.NewInt(1)},
		Admin:      random.Uint160(),
		Issuer:     random.Uint160(),
		Expiration: 10,
		IsFrozen:   false,
	}

	chain := newTestChain(t)
	context := chain.newInteropContext(trigger.Application, storage.NewMemoryStore(), nil, nil)
	return v, assetState, context, chain
}

func createVMAndContractState(t *testing.T) (*vm.VM, *state.Contract, *interopContext, *Blockchain) {
	v := vm.New()
	contractState := &state.Contract{
		Script:      []byte("testscript"),
		ParamList:   []smartcontract.ParamType{smartcontract.StringType, smartcontract.IntegerType, smartcontract.Hash160Type},
		ReturnType:  smartcontract.ArrayType,
		Properties:  smartcontract.HasStorage,
		Name:        random.String(10),
		CodeVersion: random.String(10),
		Author:      random.String(10),
		Email:       random.String(10),
		Description: random.String(10),
	}

	chain := newTestChain(t)
	context := chain.newInteropContext(trigger.Application, storage.NewMemoryStore(), nil, nil)
	return v, contractState, context, chain
}

func createVMAndAccState(t *testing.T) (*vm.VM, *state.Account, *interopContext, *Blockchain) {
	v := vm.New()
	rawHash := "4d3b96ae1bcc5a585e075e3b81920210dec16302"
	hash, err := util.Uint160DecodeStringBE(rawHash)
	accountState := state.NewAccount(hash)

	key := &keys.PublicKey{X: big.NewInt(1), Y: big.NewInt(1)}
	accountState.Votes = []*keys.PublicKey{key}

	require.NoError(t, err)
	chain := newTestChain(t)
	context := chain.newInteropContext(trigger.Application, storage.NewMemoryStore(), nil, nil)
	return v, accountState, context, chain
}

func createVMAndTX(t *testing.T) (*vm.VM, *transaction.Transaction, *interopContext, *Blockchain) {
	v := vm.New()
	script := []byte{byte(opcode.PUSH1), byte(opcode.RET)}
	tx := transaction.NewInvocationTX(script, 0)

	bytes := make([]byte, 1)
	attributes := append(tx.Attributes, transaction.Attribute{
		Usage: transaction.Description,
		Data:  bytes,
	})

	inputs := append(tx.Inputs, transaction.Input{
		PrevHash:  random.Uint256(),
		PrevIndex: 1,
	})

	outputs := append(tx.Outputs, transaction.Output{
		AssetID:    random.Uint256(),
		Amount:     10,
		ScriptHash: random.Uint160(),
		Position:   1,
	})

	tx.Attributes = attributes
	tx.Inputs = inputs
	tx.Outputs = outputs
	chain := newTestChain(t)
	context := chain.newInteropContext(trigger.Application, storage.NewMemoryStore(), nil, tx)
	return v, tx, context, chain
}