package state

import (
	"testing"

	"github.com/davecgh/go-spew/spew"

	"github.com/vapor/consensus"
	"github.com/vapor/database/storage"
	"github.com/vapor/errors"
	"github.com/vapor/protocol/bc"
	"github.com/vapor/testutil"
)

var defaultEntry = map[bc.Hash]bc.Entry{
	bc.Hash{V0: 0}: &bc.IntraChainOutput{
		Source: &bc.ValueSource{
			Value: &bc.AssetAmount{
				AssetId: &bc.AssetID{V0: 0},
			},
		},
	},
}

var coinbaseEntry = map[bc.Hash]bc.Entry{
	bc.Hash{V0: 0}: &bc.IntraChainOutput{
		Source: &bc.ValueSource{
			Value: &bc.AssetAmount{
				AssetId: consensus.BTMAssetID,
			},
		},
	},
	bc.Hash{V0: 1}: &bc.IntraChainOutput{
		Source: &bc.ValueSource{
			Value: &bc.AssetAmount{
				AssetId: consensus.BTMAssetID,
				Amount:  uint64(100),
			},
		},
	},
}

var voteEntry = map[bc.Hash]bc.Entry{
	bc.Hash{V0: 0}: &bc.VoteOutput{
		Source: &bc.ValueSource{
			Value: &bc.AssetAmount{
				AssetId: &bc.AssetID{V0: 0},
			},
		},
	},
}

var gasOnlyTxEntry = map[bc.Hash]bc.Entry{
	bc.Hash{V1: 0}: &bc.IntraChainOutput{
		Source: &bc.ValueSource{
			Value: &bc.AssetAmount{
				AssetId: consensus.BTMAssetID,
			},
		},
	},
	bc.Hash{V1: 1}: &bc.IntraChainOutput{
		Source: &bc.ValueSource{
			Value: &bc.AssetAmount{
				AssetId: &bc.AssetID{V0: 999},
			},
		},
	},
}

func TestApplyBlock(t *testing.T) {
	cases := []struct {
		block     *bc.Block
		inputView *UtxoViewpoint
		fetchView *UtxoViewpoint
		gasOnlyTx bool
		err       bool
	}{
		{
			// can't find prevout in tx entries
			block: &bc.Block{
				BlockHeader: &bc.BlockHeader{
					TransactionStatus: bc.NewTransactionStatus(),
				},
				Transactions: []*bc.Tx{
					&bc.Tx{
						SpentOutputIDs: []bc.Hash{
							bc.Hash{V0: 1},
						},
						Entries: defaultEntry,
					},
				},
			},
			inputView: &UtxoViewpoint{
				Entries: map[bc.Hash]*storage.UtxoEntry{
					bc.Hash{V0: 0}: storage.NewUtxoEntry(storage.NormalUTXOType, 0, false),
				},
			},
			fetchView: NewUtxoViewpoint(),
			err:       true,
		},
		{
			block: &bc.Block{
				BlockHeader: &bc.BlockHeader{
					TransactionStatus: bc.NewTransactionStatus(),
				},
				Transactions: []*bc.Tx{
					&bc.Tx{
						SpentOutputIDs: []bc.Hash{
							bc.Hash{V0: 0},
						},
						Entries: defaultEntry,
					},
				},
			},
			inputView: NewUtxoViewpoint(),
			fetchView: NewUtxoViewpoint(),
			err:       true,
		},
		{
			block: &bc.Block{
				BlockHeader: &bc.BlockHeader{
					TransactionStatus: bc.NewTransactionStatus(),
				},
				Transactions: []*bc.Tx{
					&bc.Tx{
						SpentOutputIDs: []bc.Hash{
							bc.Hash{V0: 0},
						},
						Entries: defaultEntry,
					},
				},
			},
			inputView: &UtxoViewpoint{
				Entries: map[bc.Hash]*storage.UtxoEntry{
					bc.Hash{V0: 0}: storage.NewUtxoEntry(storage.NormalUTXOType, 0, true),
				},
			},
			err: true,
		},
		{
			block: &bc.Block{
				BlockHeader: &bc.BlockHeader{
					TransactionStatus: bc.NewTransactionStatus(),
				},
				Transactions: []*bc.Tx{
					&bc.Tx{
						TxHeader: &bc.TxHeader{
							ResultIds: []*bc.Hash{},
						},
						SpentOutputIDs: []bc.Hash{
							bc.Hash{V0: 0},
						},
						Entries: defaultEntry,
					},
				},
			},
			inputView: &UtxoViewpoint{
				Entries: map[bc.Hash]*storage.UtxoEntry{
					bc.Hash{V0: 0}: storage.NewUtxoEntry(storage.NormalUTXOType, 0, false),
				},
			},
			fetchView: &UtxoViewpoint{
				Entries: map[bc.Hash]*storage.UtxoEntry{
					bc.Hash{V0: 0}: storage.NewUtxoEntry(storage.NormalUTXOType, 0, true),
				},
			},
			err: false,
		},
		{
			block: &bc.Block{
				BlockHeader: &bc.BlockHeader{
					Height:            consensus.MainNetParams.CoinbasePendingBlockNumber + 1,
					TransactionStatus: bc.NewTransactionStatus(),
				},
				Transactions: []*bc.Tx{
					&bc.Tx{
						TxHeader: &bc.TxHeader{
							ResultIds: []*bc.Hash{},
						},
						SpentOutputIDs: []bc.Hash{
							bc.Hash{V0: 0},
						},
						Entries: defaultEntry,
					},
				},
			},
			inputView: &UtxoViewpoint{
				Entries: map[bc.Hash]*storage.UtxoEntry{
					bc.Hash{V0: 0}: storage.NewUtxoEntry(storage.CoinbaseUTXOType, 0, false),
				},
			},
			fetchView: &UtxoViewpoint{
				Entries: map[bc.Hash]*storage.UtxoEntry{
					bc.Hash{V0: 0}: storage.NewUtxoEntry(storage.CoinbaseUTXOType, 0, true),
				},
			},
			err: false,
		},
		{
			block: &bc.Block{
				BlockHeader: &bc.BlockHeader{
					Height:            0,
					TransactionStatus: bc.NewTransactionStatus(),
				},
				Transactions: []*bc.Tx{
					&bc.Tx{
						TxHeader: &bc.TxHeader{
							ResultIds: []*bc.Hash{},
						},
						SpentOutputIDs: []bc.Hash{
							bc.Hash{V0: 0},
						},
						Entries: defaultEntry,
					},
				},
			},
			inputView: &UtxoViewpoint{
				Entries: map[bc.Hash]*storage.UtxoEntry{
					bc.Hash{V0: 0}: storage.NewUtxoEntry(storage.CoinbaseUTXOType, 0, false),
				},
			},
			fetchView: &UtxoViewpoint{
				Entries: map[bc.Hash]*storage.UtxoEntry{
					bc.Hash{V0: 0}: storage.NewUtxoEntry(storage.CoinbaseUTXOType, 0, true),
				},
			},
			err: true,
		},
		{
			// output will be store
			block: &bc.Block{
				BlockHeader: &bc.BlockHeader{
					TransactionStatus: bc.NewTransactionStatus(),
				},
				Transactions: []*bc.Tx{
					&bc.Tx{
						TxHeader: &bc.TxHeader{
							ResultIds: []*bc.Hash{
								&bc.Hash{V0: 0},
								&bc.Hash{V0: 1},
							},
						},
						SpentOutputIDs: []bc.Hash{},
						Entries:        coinbaseEntry,
					},
				},
			},
			inputView: NewUtxoViewpoint(),
			fetchView: &UtxoViewpoint{
				Entries: map[bc.Hash]*storage.UtxoEntry{
					bc.Hash{V0: 1}: storage.NewUtxoEntry(storage.CoinbaseUTXOType, 0, false),
				},
			},
			err: false,
		},
		{
			// apply gas only tx, non-btm asset spent input will not be spent
			block: &bc.Block{
				BlockHeader: &bc.BlockHeader{
					TransactionStatus: bc.NewTransactionStatus(),
				},
				Transactions: []*bc.Tx{
					&bc.Tx{
						TxHeader: &bc.TxHeader{
							ResultIds: []*bc.Hash{},
						},
						SpentOutputIDs: []bc.Hash{
							bc.Hash{V1: 0},
							bc.Hash{V1: 1},
						},
						Entries: gasOnlyTxEntry,
					},
				},
			},
			inputView: &UtxoViewpoint{
				Entries: map[bc.Hash]*storage.UtxoEntry{
					bc.Hash{V1: 0}: storage.NewUtxoEntry(storage.NormalUTXOType, 0, false),
					bc.Hash{V1: 1}: storage.NewUtxoEntry(storage.NormalUTXOType, 0, false),
				},
			},
			fetchView: &UtxoViewpoint{
				Entries: map[bc.Hash]*storage.UtxoEntry{
					bc.Hash{V1: 0}: storage.NewUtxoEntry(storage.NormalUTXOType, 0, true),
					bc.Hash{V1: 1}: storage.NewUtxoEntry(storage.NormalUTXOType, 0, false),
				},
			},
			gasOnlyTx: true,
			err:       false,
		},
		{
			// apply gas only tx, non-btm asset spent output will not be store
			block: &bc.Block{
				BlockHeader: &bc.BlockHeader{
					TransactionStatus: bc.NewTransactionStatus(),
				},
				Transactions: []*bc.Tx{
					&bc.Tx{
						TxHeader: &bc.TxHeader{
							ResultIds: []*bc.Hash{
								&bc.Hash{V1: 0},
								&bc.Hash{V1: 1},
							},
						},
						SpentOutputIDs: []bc.Hash{},
						Entries:        gasOnlyTxEntry,
					},
				},
			},
			inputView: NewUtxoViewpoint(),
			fetchView: NewUtxoViewpoint(),
			gasOnlyTx: true,
			err:       false,
		},
		{
			block: &bc.Block{
				BlockHeader: &bc.BlockHeader{
					TransactionStatus: bc.NewTransactionStatus(),
				},
				Transactions: []*bc.Tx{
					&bc.Tx{
						TxHeader: &bc.TxHeader{
							ResultIds: []*bc.Hash{},
						},
						SpentOutputIDs: []bc.Hash{
							bc.Hash{V0: 0},
						},
						Entries: voteEntry,
					},
				},
			},
			inputView: &UtxoViewpoint{
				Entries: map[bc.Hash]*storage.UtxoEntry{
					bc.Hash{V0: 0}: storage.NewUtxoEntry(storage.VoteUTXOType, 0, false),
				},
			},
			fetchView: &UtxoViewpoint{
				Entries: map[bc.Hash]*storage.UtxoEntry{
					bc.Hash{V0: 0}: storage.NewUtxoEntry(storage.VoteUTXOType, 0, true),
				},
			},
			err: true,
		},
		{
			block: &bc.Block{
				BlockHeader: &bc.BlockHeader{
					Height:            consensus.MainNetParams.VotePendingBlockNumber + 1,
					TransactionStatus: bc.NewTransactionStatus(),
				},
				Transactions: []*bc.Tx{
					&bc.Tx{
						TxHeader: &bc.TxHeader{
							ResultIds: []*bc.Hash{},
						},
						SpentOutputIDs: []bc.Hash{
							bc.Hash{V0: 0},
						},
						Entries: voteEntry,
					},
				},
			},
			inputView: &UtxoViewpoint{
				Entries: map[bc.Hash]*storage.UtxoEntry{
					bc.Hash{V0: 0}: storage.NewUtxoEntry(storage.VoteUTXOType, 1, false),
				},
			},
			fetchView: &UtxoViewpoint{
				Entries: map[bc.Hash]*storage.UtxoEntry{
					bc.Hash{V0: 0}: storage.NewUtxoEntry(storage.VoteUTXOType, 1, true),
				},
			},
			err: false,
		},
	}

	for i, c := range cases {
		c.block.TransactionStatus.SetStatus(0, c.gasOnlyTx)
		if err := c.inputView.ApplyBlock(c.block, c.block.TransactionStatus); c.err != (err != nil) {
			t.Errorf("case #%d want err = %v, get err = %v", i, c.err, err)
		}
		if c.err {
			continue
		}
		if !testutil.DeepEqual(c.inputView, c.fetchView) {
			t.Errorf("test case %d, want %v, get %v", i, c.fetchView, c.inputView)
		}
	}
}

func TestDetachBlock(t *testing.T) {
	cases := []struct {
		block     *bc.Block
		inputView *UtxoViewpoint
		fetchView *UtxoViewpoint
		gasOnlyTx bool
		err       bool
	}{
		{
			block: &bc.Block{
				BlockHeader: &bc.BlockHeader{
					TransactionStatus: bc.NewTransactionStatus(),
				},
				Transactions: []*bc.Tx{
					&bc.Tx{
						TxHeader: &bc.TxHeader{
							ResultIds: []*bc.Hash{},
						},
						SpentOutputIDs: []bc.Hash{
							bc.Hash{V0: 0},
						},
						Entries: defaultEntry,
					},
				},
			},
			inputView: NewUtxoViewpoint(),
			fetchView: &UtxoViewpoint{
				Entries: map[bc.Hash]*storage.UtxoEntry{
					bc.Hash{V0: 0}: storage.NewUtxoEntry(storage.NormalUTXOType, 0, false),
				},
			},
			err: false,
		},
		{
			block: &bc.Block{
				BlockHeader: &bc.BlockHeader{
					TransactionStatus: bc.NewTransactionStatus(),
				},
				Transactions: []*bc.Tx{
					&bc.Tx{
						TxHeader: &bc.TxHeader{
							ResultIds: []*bc.Hash{
								&bc.Hash{V0: 0},
								&bc.Hash{V0: 1},
							},
						},
						SpentOutputIDs: []bc.Hash{},
						Entries:        coinbaseEntry,
					},
				},
			},
			inputView: &UtxoViewpoint{
				Entries: map[bc.Hash]*storage.UtxoEntry{
					bc.Hash{V0: 1}: storage.NewUtxoEntry(storage.NormalUTXOType, 0, false),
				},
			},
			fetchView: &UtxoViewpoint{
				Entries: map[bc.Hash]*storage.UtxoEntry{
					bc.Hash{V0: 1}: storage.NewUtxoEntry(storage.NormalUTXOType, 0, true),
				},
			},
			err: false,
		},
		{
			block: &bc.Block{
				BlockHeader: &bc.BlockHeader{
					TransactionStatus: bc.NewTransactionStatus(),
				},
				Transactions: []*bc.Tx{
					&bc.Tx{
						TxHeader: &bc.TxHeader{
							ResultIds: []*bc.Hash{},
						},
						SpentOutputIDs: []bc.Hash{
							bc.Hash{V0: 0},
						},
						Entries: defaultEntry,
					},
				},
			},
			inputView: &UtxoViewpoint{
				Entries: map[bc.Hash]*storage.UtxoEntry{
					bc.Hash{V0: 0}: storage.NewUtxoEntry(storage.NormalUTXOType, 0, false),
				},
			},
			err: true,
		},
		{
			block: &bc.Block{
				BlockHeader: &bc.BlockHeader{
					TransactionStatus: bc.NewTransactionStatus(),
				},
				Transactions: []*bc.Tx{
					&bc.Tx{
						TxHeader: &bc.TxHeader{
							ResultIds: []*bc.Hash{},
						},
						SpentOutputIDs: []bc.Hash{
							bc.Hash{V0: 0},
						},
						Entries: defaultEntry,
					},
				},
			},
			inputView: &UtxoViewpoint{
				Entries: map[bc.Hash]*storage.UtxoEntry{
					bc.Hash{V0: 0}: storage.NewUtxoEntry(storage.NormalUTXOType, 0, true),
				},
			},
			fetchView: &UtxoViewpoint{
				Entries: map[bc.Hash]*storage.UtxoEntry{
					bc.Hash{V0: 0}: storage.NewUtxoEntry(storage.NormalUTXOType, 0, false),
				},
			},
			err: false,
		},
		{
			block: &bc.Block{
				BlockHeader: &bc.BlockHeader{
					TransactionStatus: bc.NewTransactionStatus(),
				},
				Transactions: []*bc.Tx{
					&bc.Tx{
						TxHeader: &bc.TxHeader{
							ResultIds: []*bc.Hash{},
						},
						SpentOutputIDs: []bc.Hash{
							bc.Hash{V1: 0},
							bc.Hash{V1: 1},
						},
						Entries: gasOnlyTxEntry,
					},
				},
			},
			inputView: &UtxoViewpoint{
				Entries: map[bc.Hash]*storage.UtxoEntry{
					bc.Hash{V1: 0}: storage.NewUtxoEntry(storage.NormalUTXOType, 0, true),
					bc.Hash{V1: 1}: storage.NewUtxoEntry(storage.NormalUTXOType, 0, true),
				},
			},
			fetchView: &UtxoViewpoint{
				Entries: map[bc.Hash]*storage.UtxoEntry{
					bc.Hash{V1: 0}: storage.NewUtxoEntry(storage.NormalUTXOType, 0, false),
					bc.Hash{V1: 1}: storage.NewUtxoEntry(storage.NormalUTXOType, 0, true),
				},
			},
			gasOnlyTx: true,
			err:       false,
		},
		{
			block: &bc.Block{
				BlockHeader: &bc.BlockHeader{
					TransactionStatus: bc.NewTransactionStatus(),
				},
				Transactions: []*bc.Tx{
					&bc.Tx{
						TxHeader: &bc.TxHeader{
							ResultIds: []*bc.Hash{
								&bc.Hash{V1: 0},
								&bc.Hash{V1: 1},
							},
						},
						SpentOutputIDs: []bc.Hash{},
						Entries:        gasOnlyTxEntry,
					},
				},
			},
			inputView: NewUtxoViewpoint(),
			fetchView: NewUtxoViewpoint(),
			gasOnlyTx: true,
			err:       false,
		},
		{
			block: &bc.Block{
				BlockHeader: &bc.BlockHeader{
					TransactionStatus: bc.NewTransactionStatus(),
				},
				Transactions: []*bc.Tx{
					&bc.Tx{
						TxHeader: &bc.TxHeader{
							ResultIds: []*bc.Hash{},
						},
						SpentOutputIDs: []bc.Hash{
							bc.Hash{V0: 0},
						},
						Entries: voteEntry,
					},
				},
			},
			inputView: NewUtxoViewpoint(),
			fetchView: &UtxoViewpoint{
				Entries: map[bc.Hash]*storage.UtxoEntry{
					bc.Hash{V0: 0}: storage.NewUtxoEntry(storage.VoteUTXOType, 0, false),
				},
			},
			err: false,
		},
	}

	for i, c := range cases {
		c.block.TransactionStatus.SetStatus(0, c.gasOnlyTx)
		if err := c.inputView.DetachBlock(c.block, c.block.TransactionStatus); c.err != (err != nil) {
			t.Errorf("case %d want err = %v, get err = %v", i, c.err, err)
		}
		if c.err {
			continue
		}
		if !testutil.DeepEqual(c.inputView, c.fetchView) {
			t.Errorf("test case %d, want %v, get %v", i, c.fetchView, c.inputView)
		}
	}
}

func TestApplyCrossChainUTXO(t *testing.T) {
	cases := []struct {
		desc         string
		block        *bc.Block
		tx           *bc.Tx
		prevUTXOView *UtxoViewpoint
		postUTXOView *UtxoViewpoint
		err          error
	}{
		{
			desc: "normal test",
			block: &bc.Block{
				BlockHeader: &bc.BlockHeader{
					Height: 100,
				},
			},
			tx: &bc.Tx{
				TxHeader: &bc.TxHeader{
					ResultIds: []*bc.Hash{},
				},
				MainchainOutputIDs: []bc.Hash{
					bc.Hash{V0: 0},
				},
				Entries: voteEntry,
			},
			prevUTXOView: &UtxoViewpoint{
				Entries: map[bc.Hash]*storage.UtxoEntry{
					bc.Hash{V0: 0}: storage.NewUtxoEntry(storage.CrosschainUTXOType, 0, false),
				},
			},
			postUTXOView: &UtxoViewpoint{
				Entries: map[bc.Hash]*storage.UtxoEntry{
					bc.Hash{V0: 0}: storage.NewUtxoEntry(storage.CrosschainUTXOType, 100, true),
				},
			},
			err: nil,
		},
		{
			desc: "test failed to find mainchain output entry",
			block: &bc.Block{
				BlockHeader: &bc.BlockHeader{},
			},
			tx: &bc.Tx{
				TxHeader: &bc.TxHeader{
					ResultIds: []*bc.Hash{},
				},
				MainchainOutputIDs: []bc.Hash{
					bc.Hash{V0: 0},
				},
				Entries: voteEntry,
			},
			prevUTXOView: &UtxoViewpoint{
				Entries: map[bc.Hash]*storage.UtxoEntry{},
			},
			postUTXOView: &UtxoViewpoint{
				Entries: map[bc.Hash]*storage.UtxoEntry{},
			},
			err: errors.New("fail to find mainchain output entry"),
		},
		{
			desc: "test mainchain output has been spent",
			block: &bc.Block{
				BlockHeader: &bc.BlockHeader{},
			},
			tx: &bc.Tx{
				TxHeader: &bc.TxHeader{
					ResultIds: []*bc.Hash{},
				},
				MainchainOutputIDs: []bc.Hash{
					bc.Hash{V0: 0},
				},
				Entries: voteEntry,
			},
			prevUTXOView: &UtxoViewpoint{
				Entries: map[bc.Hash]*storage.UtxoEntry{
					bc.Hash{V0: 0}: storage.NewUtxoEntry(storage.CrosschainUTXOType, 0, true),
				},
			},
			postUTXOView: &UtxoViewpoint{
				Entries: map[bc.Hash]*storage.UtxoEntry{},
			},
			err: errors.New("mainchain output has been spent"),
		},
	}

	for i, c := range cases {
		if err := c.prevUTXOView.applyCrossChainUtxo(c.block, c.tx); err != nil {
			if err.Error() != c.err.Error() {
				t.Errorf("test case #%d want err = %v, got err = %v", i, c.err, err)
			}
			continue
		}

		if !testutil.DeepEqual(c.prevUTXOView, c.postUTXOView) {
			t.Errorf("test case #%d, want %v, got %v", i, c.postUTXOView, c.prevUTXOView)
		}
	}
}

func TestApplyOutputUTXO(t *testing.T) {
	cases := []struct {
		desc         string
		block        *bc.Block
		tx           *bc.Tx
		statusFail   bool
		prevUTXOView *UtxoViewpoint
		postUTXOView *UtxoViewpoint
		err          error
	}{
		{
			desc: "normal test IntraChainOutput,VoteOutput,Retirement",
			block: &bc.Block{
				BlockHeader: &bc.BlockHeader{},
			},
			tx: &bc.Tx{
				TxHeader: &bc.TxHeader{
					ResultIds: []*bc.Hash{&bc.Hash{V0: 0}, &bc.Hash{V0: 1}, &bc.Hash{V0: 2}},
				},
				Entries: map[bc.Hash]bc.Entry{
					bc.Hash{V0: 0}: &bc.IntraChainOutput{
						Source: &bc.ValueSource{
							Value: &bc.AssetAmount{
								AssetId: &bc.AssetID{V0: 0},
								Amount:  100,
							},
						},
					},
					bc.Hash{V0: 1}: &bc.VoteOutput{
						Source: &bc.ValueSource{
							Value: &bc.AssetAmount{
								AssetId: &bc.AssetID{V0: 1},
							},
						},
					},
					bc.Hash{V0: 2}: &bc.Retirement{
						Source: &bc.ValueSource{
							Value: &bc.AssetAmount{
								AssetId: &bc.AssetID{V0: 1},
							},
						},
					},
				},
			},
			prevUTXOView: &UtxoViewpoint{
				Entries: map[bc.Hash]*storage.UtxoEntry{},
			},
			postUTXOView: &UtxoViewpoint{
				Entries: map[bc.Hash]*storage.UtxoEntry{
					bc.Hash{V0: 0}: storage.NewUtxoEntry(storage.NormalUTXOType, 0, false),
					bc.Hash{V0: 1}: storage.NewUtxoEntry(storage.VoteUTXOType, 0, false),
				},
			},
			err: nil,
		},
		{
			desc: "test statusFail",
			block: &bc.Block{
				BlockHeader: &bc.BlockHeader{},
			},
			tx: &bc.Tx{
				TxHeader: &bc.TxHeader{
					ResultIds: []*bc.Hash{&bc.Hash{V0: 0}, &bc.Hash{V0: 1}, &bc.Hash{V0: 2}},
				},
				Entries: map[bc.Hash]bc.Entry{
					bc.Hash{V0: 0}: &bc.IntraChainOutput{
						Source: &bc.ValueSource{
							Value: &bc.AssetAmount{
								AssetId: &bc.AssetID{V0: 0},
								Amount:  100,
							},
						},
					},
					bc.Hash{V0: 1}: &bc.VoteOutput{
						Source: &bc.ValueSource{
							Value: &bc.AssetAmount{
								AssetId: consensus.BTMAssetID,
							},
						},
					},
					bc.Hash{V0: 2}: &bc.Retirement{
						Source: &bc.ValueSource{
							Value: &bc.AssetAmount{
								AssetId: &bc.AssetID{V0: 1},
							},
						},
					},
				},
			},
			statusFail: true,
			prevUTXOView: &UtxoViewpoint{
				Entries: map[bc.Hash]*storage.UtxoEntry{},
			},
			postUTXOView: &UtxoViewpoint{
				Entries: map[bc.Hash]*storage.UtxoEntry{
					bc.Hash{V0: 1}: storage.NewUtxoEntry(storage.VoteUTXOType, 0, false),
				},
			},
			err: nil,
		},
		{
			desc: "test failed on found id from tx entry",
			block: &bc.Block{
				BlockHeader: &bc.BlockHeader{},
			},
			tx: &bc.Tx{
				TxHeader: &bc.TxHeader{
					ResultIds: []*bc.Hash{&bc.Hash{V0: 0}, &bc.Hash{V0: 1}, &bc.Hash{V0: 2}},
				},
				Entries: map[bc.Hash]bc.Entry{
					bc.Hash{V0: 1}: &bc.VoteOutput{
						Source: &bc.ValueSource{
							Value: &bc.AssetAmount{
								AssetId: consensus.BTMAssetID,
							},
						},
					},
					bc.Hash{V0: 2}: &bc.Retirement{
						Source: &bc.ValueSource{
							Value: &bc.AssetAmount{
								AssetId: &bc.AssetID{V0: 1},
							},
						},
					},
				},
			},
			statusFail: false,
			prevUTXOView: &UtxoViewpoint{
				Entries: map[bc.Hash]*storage.UtxoEntry{},
			},
			postUTXOView: &UtxoViewpoint{
				Entries: map[bc.Hash]*storage.UtxoEntry{},
			},
			err: bc.ErrMissingEntry,
		},
	}

	for i, c := range cases {
		if err := c.prevUTXOView.applyOutputUtxo(c.block, c.tx, c.statusFail); err != nil {
			if errors.Root(err) != errors.Root(c.err) {
				t.Errorf("test case #%d want err = %v, got err = %v", i, c.err.Error(), err.Error())
			}
			continue
		}

		if !testutil.DeepEqual(c.prevUTXOView, c.postUTXOView) {
			t.Errorf("test case #%d, want %v, got %v", i, c.postUTXOView, c.prevUTXOView)
		}
	}
}

func TestApplySpendUTXO(t *testing.T) {
	cases := []struct {
		desc         string
		block        *bc.Block
		tx           *bc.Tx
		statusFail   bool
		prevUTXOView *UtxoViewpoint
		postUTXOView *UtxoViewpoint
		err          error
	}{
		{
			desc: "normal test",
			block: &bc.Block{
				BlockHeader: &bc.BlockHeader{
					Height: consensus.ActiveNetParams.VotePendingBlockNumber,
				},
			},
			tx: &bc.Tx{
				TxHeader:       &bc.TxHeader{},
				SpentOutputIDs: []bc.Hash{{V0: 0}, {V0: 1}, {V0: 2}},
				Entries: map[bc.Hash]bc.Entry{
					bc.Hash{V0: 0}: &bc.IntraChainOutput{
						Source: &bc.ValueSource{
							Value: &bc.AssetAmount{
								AssetId: &bc.AssetID{V0: 0},
								Amount:  100,
							},
						},
					},
					bc.Hash{V0: 1}: &bc.VoteOutput{
						Source: &bc.ValueSource{
							Value: &bc.AssetAmount{
								AssetId: &bc.AssetID{V0: 1},
							},
						},
					},
					bc.Hash{V0: 2}: &bc.IntraChainOutput{
						Source: &bc.ValueSource{
							Value: &bc.AssetAmount{
								AssetId: consensus.BTMAssetID,
								Amount:  100,
							},
						},
					},
				},
			},
			prevUTXOView: &UtxoViewpoint{
				Entries: map[bc.Hash]*storage.UtxoEntry{
					bc.Hash{V0: 0}: storage.NewUtxoEntry(storage.NormalUTXOType, 0, false),
					bc.Hash{V0: 1}: storage.NewUtxoEntry(storage.VoteUTXOType, 0, false),
					bc.Hash{V0: 2}: storage.NewUtxoEntry(storage.CoinbaseUTXOType, 0, false),
				},
			},
			postUTXOView: &UtxoViewpoint{
				Entries: map[bc.Hash]*storage.UtxoEntry{
					bc.Hash{V0: 0}: storage.NewUtxoEntry(storage.NormalUTXOType, 0, true),
					bc.Hash{V0: 1}: storage.NewUtxoEntry(storage.VoteUTXOType, 0, true),
					bc.Hash{V0: 2}: storage.NewUtxoEntry(storage.CoinbaseUTXOType, 0, true),
				},
			},
			err: nil,
		},
		{
			desc: "test coinbase is not ready for use",
			block: &bc.Block{
				BlockHeader: &bc.BlockHeader{
					Height: consensus.ActiveNetParams.CoinbasePendingBlockNumber - 1,
				},
			},
			tx: &bc.Tx{
				TxHeader:       &bc.TxHeader{},
				SpentOutputIDs: []bc.Hash{{V0: 1}, {V0: 2}},
				Entries: map[bc.Hash]bc.Entry{
					bc.Hash{V0: 1}: &bc.VoteOutput{
						Source: &bc.ValueSource{
							Value: &bc.AssetAmount{
								AssetId: &bc.AssetID{V0: 1},
							},
						},
					},
					bc.Hash{V0: 2}: &bc.IntraChainOutput{
						Source: &bc.ValueSource{
							Value: &bc.AssetAmount{
								AssetId: consensus.BTMAssetID,
								Amount:  100,
							},
						},
					},
				},
			},
			prevUTXOView: &UtxoViewpoint{
				Entries: map[bc.Hash]*storage.UtxoEntry{
					bc.Hash{V0: 1}: storage.NewUtxoEntry(storage.NormalUTXOType, 0, false),
					bc.Hash{V0: 2}: storage.NewUtxoEntry(storage.CoinbaseUTXOType, 0, false),
				},
			},
			postUTXOView: &UtxoViewpoint{
				Entries: map[bc.Hash]*storage.UtxoEntry{},
			},
			err: errors.New("coinbase utxo is not ready for use"),
		},
		{
			desc: "test Coin is  within the voting lock time",
			block: &bc.Block{
				BlockHeader: &bc.BlockHeader{
					Height: consensus.ActiveNetParams.VotePendingBlockNumber - 1,
				},
			},
			tx: &bc.Tx{
				TxHeader:       &bc.TxHeader{},
				SpentOutputIDs: []bc.Hash{{V0: 1}, {V0: 2}},
				Entries: map[bc.Hash]bc.Entry{
					bc.Hash{V0: 1}: &bc.VoteOutput{
						Source: &bc.ValueSource{
							Value: &bc.AssetAmount{
								AssetId: &bc.AssetID{V0: 1},
							},
						},
					},
					bc.Hash{V0: 2}: &bc.IntraChainOutput{
						Source: &bc.ValueSource{
							Value: &bc.AssetAmount{
								AssetId: consensus.BTMAssetID,
								Amount:  100,
							},
						},
					},
				},
			},
			prevUTXOView: &UtxoViewpoint{
				Entries: map[bc.Hash]*storage.UtxoEntry{
					bc.Hash{V0: 1}: storage.NewUtxoEntry(storage.NormalUTXOType, 0, false),
					bc.Hash{V0: 2}: storage.NewUtxoEntry(storage.VoteUTXOType, 0, false),
				},
			},
			postUTXOView: &UtxoViewpoint{
				Entries: map[bc.Hash]*storage.UtxoEntry{},
			},
			err: errors.New("Coin is  within the voting lock time"),
		},
		{
			desc: "test utxo has been spent",
			block: &bc.Block{
				BlockHeader: &bc.BlockHeader{
					Height: 0,
				},
			},
			tx: &bc.Tx{
				TxHeader:       &bc.TxHeader{},
				SpentOutputIDs: []bc.Hash{{V0: 0}, {V0: 1}, {V0: 2}},
				Entries: map[bc.Hash]bc.Entry{
					bc.Hash{V0: 0}: &bc.IntraChainOutput{
						Source: &bc.ValueSource{
							Value: &bc.AssetAmount{
								AssetId: &bc.AssetID{V0: 0},
								Amount:  100,
							},
						},
					},
					bc.Hash{V0: 1}: &bc.VoteOutput{
						Source: &bc.ValueSource{
							Value: &bc.AssetAmount{
								AssetId: &bc.AssetID{V0: 1},
							},
						},
					},
					bc.Hash{V0: 2}: &bc.IntraChainOutput{
						Source: &bc.ValueSource{
							Value: &bc.AssetAmount{
								AssetId: consensus.BTMAssetID,
								Amount:  100,
							},
						},
					},
				},
			},
			prevUTXOView: &UtxoViewpoint{
				Entries: map[bc.Hash]*storage.UtxoEntry{
					bc.Hash{V0: 0}: storage.NewUtxoEntry(storage.NormalUTXOType, 0, true),
					bc.Hash{V0: 1}: storage.NewUtxoEntry(storage.VoteUTXOType, 0, false),
					bc.Hash{V0: 2}: storage.NewUtxoEntry(storage.CoinbaseUTXOType, 0, false),
				},
			},
			postUTXOView: &UtxoViewpoint{
				Entries: map[bc.Hash]*storage.UtxoEntry{},
			},
			err: errors.New("utxo has been spent"),
		},
		{
			desc: "test faild to find utxo entry",
			block: &bc.Block{
				BlockHeader: &bc.BlockHeader{
					Height: 0,
				},
			},
			tx: &bc.Tx{
				TxHeader:       &bc.TxHeader{},
				SpentOutputIDs: []bc.Hash{{V0: 0}, {V0: 1}, {V0: 2}},
				Entries: map[bc.Hash]bc.Entry{
					bc.Hash{V0: 0}: &bc.IntraChainOutput{
						Source: &bc.ValueSource{
							Value: &bc.AssetAmount{
								AssetId: &bc.AssetID{V0: 0},
								Amount:  100,
							},
						},
					},
					bc.Hash{V0: 1}: &bc.VoteOutput{
						Source: &bc.ValueSource{
							Value: &bc.AssetAmount{
								AssetId: &bc.AssetID{V0: 1},
							},
						},
					},
					bc.Hash{V0: 2}: &bc.IntraChainOutput{
						Source: &bc.ValueSource{
							Value: &bc.AssetAmount{
								AssetId: consensus.BTMAssetID,
								Amount:  100,
							},
						},
					},
				},
			},
			prevUTXOView: &UtxoViewpoint{
				Entries: map[bc.Hash]*storage.UtxoEntry{
					bc.Hash{V0: 1}: storage.NewUtxoEntry(storage.VoteUTXOType, 0, false),
					bc.Hash{V0: 2}: storage.NewUtxoEntry(storage.CoinbaseUTXOType, 0, false),
				},
			},
			postUTXOView: &UtxoViewpoint{
				Entries: map[bc.Hash]*storage.UtxoEntry{},
			},
			err: errors.New("fail to find utxo entry"),
		},
	}

	for i, c := range cases {
		if err := c.prevUTXOView.applySpendUtxo(c.block, c.tx, c.statusFail); err != nil {
			if err.Error() != c.err.Error() {
				t.Errorf("test case #%d want err = %v, got err = %v", i, err.Error(), c.err.Error())
			}
			continue
		}

		if !testutil.DeepEqual(c.prevUTXOView, c.postUTXOView) {
			t.Errorf("test case #%d, want %v, got %v", i, spew.Sdump(c.postUTXOView), spew.Sdump(c.prevUTXOView))
		}
	}
}

func TestDetachCrossChainUTXO(t *testing.T) {
	cases := []struct {
		desc         string
		tx           *bc.Tx
		prevUTXOView *UtxoViewpoint
		postUTXOView *UtxoViewpoint
		err          error
	}{
		{
			desc: "normal test",
			tx: &bc.Tx{
				TxHeader: &bc.TxHeader{
					ResultIds: []*bc.Hash{},
				},
				MainchainOutputIDs: []bc.Hash{
					bc.Hash{V0: 0},
				},
				Entries: voteEntry,
			},
			prevUTXOView: &UtxoViewpoint{
				Entries: map[bc.Hash]*storage.UtxoEntry{
					bc.Hash{V0: 0}: storage.NewUtxoEntry(storage.CrosschainUTXOType, 0, true),
				},
			},
			postUTXOView: &UtxoViewpoint{
				Entries: map[bc.Hash]*storage.UtxoEntry{
					bc.Hash{V0: 0}: storage.NewUtxoEntry(storage.CrosschainUTXOType, 0, false),
				},
			},
			err: nil,
		},
		{
			desc: "test failed to find mainchain output entry",
			tx: &bc.Tx{
				TxHeader: &bc.TxHeader{
					ResultIds: []*bc.Hash{},
				},
				MainchainOutputIDs: []bc.Hash{
					bc.Hash{V0: 0},
				},
				Entries: voteEntry,
			},
			prevUTXOView: &UtxoViewpoint{
				Entries: map[bc.Hash]*storage.UtxoEntry{},
			},
			postUTXOView: &UtxoViewpoint{
				Entries: map[bc.Hash]*storage.UtxoEntry{},
			},
			err: errors.New("fail to find mainchain output entry"),
		},
		{
			desc: "test revert output is unspent",
			tx: &bc.Tx{
				TxHeader: &bc.TxHeader{
					ResultIds: []*bc.Hash{},
				},
				MainchainOutputIDs: []bc.Hash{
					bc.Hash{V0: 0},
				},
				Entries: voteEntry,
			},
			prevUTXOView: &UtxoViewpoint{
				Entries: map[bc.Hash]*storage.UtxoEntry{
					bc.Hash{V0: 0}: storage.NewUtxoEntry(storage.CrosschainUTXOType, 0, false),
				},
			},
			postUTXOView: &UtxoViewpoint{
				Entries: map[bc.Hash]*storage.UtxoEntry{},
			},
			err: errors.New("mainchain output is unspent"),
		},
	}

	for i, c := range cases {
		if err := c.prevUTXOView.detachCrossChainUtxo(c.tx); err != nil {
			if err.Error() != c.err.Error() {
				t.Errorf("test case #%d want err = %v, got err = %v", i, c.err, err)
			}
			continue
		}

		if !testutil.DeepEqual(c.prevUTXOView, c.postUTXOView) {
			t.Errorf("test case #%d, want %v, got %v", i, c.postUTXOView, c.prevUTXOView)
		}
	}
}

func TestDetachOutputUTXO(t *testing.T) {
	cases := []struct {
		desc         string
		tx           *bc.Tx
		statusFail   bool
		prevUTXOView *UtxoViewpoint
		postUTXOView *UtxoViewpoint
		err          error
	}{
		{
			desc: "normal test IntraChainOutput,VoteOutput",
			tx: &bc.Tx{
				TxHeader: &bc.TxHeader{
					ResultIds: []*bc.Hash{&bc.Hash{V0: 0}, &bc.Hash{V0: 1}, &bc.Hash{V0: 2}},
				},
				Entries: map[bc.Hash]bc.Entry{
					bc.Hash{V0: 0}: &bc.IntraChainOutput{
						Source: &bc.ValueSource{
							Value: &bc.AssetAmount{
								AssetId: &bc.AssetID{V0: 0},
								Amount:  100,
							},
						},
					},
					bc.Hash{V0: 1}: &bc.VoteOutput{
						Source: &bc.ValueSource{
							Value: &bc.AssetAmount{
								AssetId: &bc.AssetID{V0: 1},
							},
						},
					},
					bc.Hash{V0: 2}: &bc.Retirement{
						Source: &bc.ValueSource{
							Value: &bc.AssetAmount{
								AssetId: &bc.AssetID{V0: 1},
							},
						},
					},
				},
			},
			prevUTXOView: &UtxoViewpoint{
				Entries: map[bc.Hash]*storage.UtxoEntry{
					bc.Hash{V0: 0}: storage.NewUtxoEntry(storage.NormalUTXOType, 0, true),
					bc.Hash{V0: 1}: storage.NewUtxoEntry(storage.VoteUTXOType, 0, true),
				},
			},
			postUTXOView: &UtxoViewpoint{
				Entries: map[bc.Hash]*storage.UtxoEntry{
					bc.Hash{V0: 0}: storage.NewUtxoEntry(storage.NormalUTXOType, 0, true),
					bc.Hash{V0: 1}: storage.NewUtxoEntry(storage.VoteUTXOType, 0, true),
				},
			},
			err: nil,
		},
		{
			desc: "test statusFail",
			tx: &bc.Tx{
				TxHeader: &bc.TxHeader{
					ResultIds: []*bc.Hash{&bc.Hash{V0: 0}, &bc.Hash{V0: 1}},
				},
				Entries: map[bc.Hash]bc.Entry{
					bc.Hash{V0: 0}: &bc.IntraChainOutput{
						Source: &bc.ValueSource{
							Value: &bc.AssetAmount{
								AssetId: &bc.AssetID{V0: 0},
								Amount:  100,
							},
						},
					},
					bc.Hash{V0: 1}: &bc.VoteOutput{
						Source: &bc.ValueSource{
							Value: &bc.AssetAmount{
								AssetId: consensus.BTMAssetID,
							},
						},
					},
				},
			},
			statusFail: true,
			prevUTXOView: &UtxoViewpoint{
				Entries: map[bc.Hash]*storage.UtxoEntry{},
			},
			postUTXOView: &UtxoViewpoint{
				Entries: map[bc.Hash]*storage.UtxoEntry{
					bc.Hash{V0: 1}: storage.NewUtxoEntry(storage.VoteUTXOType, 0, true),
				},
			},
			err: nil,
		},
		{
			desc: "test failed on found id from tx entry",
			tx: &bc.Tx{
				TxHeader: &bc.TxHeader{
					ResultIds: []*bc.Hash{&bc.Hash{V0: 0}, &bc.Hash{V0: 1}, &bc.Hash{V0: 2}},
				},
				Entries: map[bc.Hash]bc.Entry{
					bc.Hash{V0: 1}: &bc.VoteOutput{
						Source: &bc.ValueSource{
							Value: &bc.AssetAmount{
								AssetId: consensus.BTMAssetID,
							},
						},
					},
					bc.Hash{V0: 2}: &bc.Retirement{
						Source: &bc.ValueSource{
							Value: &bc.AssetAmount{
								AssetId: &bc.AssetID{V0: 1},
							},
						},
					},
				},
			},
			statusFail: false,
			prevUTXOView: &UtxoViewpoint{
				Entries: map[bc.Hash]*storage.UtxoEntry{},
			},
			postUTXOView: &UtxoViewpoint{
				Entries: map[bc.Hash]*storage.UtxoEntry{},
			},
			err: bc.ErrMissingEntry,
		},
	}

	for i, c := range cases {
		if err := c.prevUTXOView.detachOutputUtxo(c.tx, c.statusFail); err != nil {
			if errors.Root(err) != errors.Root(c.err) {
				t.Errorf("test case #%d want err = %v, got err = %v", i, c.err.Error(), err.Error())
			}
			continue
		}

		if !testutil.DeepEqual(c.prevUTXOView, c.postUTXOView) {
			t.Errorf("test case #%d, want %v, got %v", i, c.postUTXOView, c.prevUTXOView)
		}
	}
}

func TestDetachSpendUTXO(t *testing.T) {
	cases := []struct {
		desc         string
		tx           *bc.Tx
		statusFail   bool
		prevUTXOView *UtxoViewpoint
		postUTXOView *UtxoViewpoint
		err          error
	}{
		{
			desc: "normal test",
			tx: &bc.Tx{
				TxHeader:       &bc.TxHeader{},
				SpentOutputIDs: []bc.Hash{{V0: 0}, {V0: 1}},
				Entries: map[bc.Hash]bc.Entry{
					bc.Hash{V0: 0}: &bc.IntraChainOutput{
						Source: &bc.ValueSource{
							Value: &bc.AssetAmount{
								AssetId: &bc.AssetID{V0: 0},
								Amount:  100,
							},
						},
					},
					bc.Hash{V0: 1}: &bc.VoteOutput{
						Source: &bc.ValueSource{
							Value: &bc.AssetAmount{
								AssetId: &bc.AssetID{V0: 1},
							},
						},
					},
				},
			},
			prevUTXOView: &UtxoViewpoint{
				Entries: map[bc.Hash]*storage.UtxoEntry{
					bc.Hash{V0: 0}: storage.NewUtxoEntry(storage.NormalUTXOType, 0, true),
					bc.Hash{V0: 1}: storage.NewUtxoEntry(storage.VoteUTXOType, 0, true),
				},
			},
			postUTXOView: &UtxoViewpoint{
				Entries: map[bc.Hash]*storage.UtxoEntry{
					bc.Hash{V0: 0}: storage.NewUtxoEntry(storage.NormalUTXOType, 0, false),
					bc.Hash{V0: 1}: storage.NewUtxoEntry(storage.VoteUTXOType, 0, false),
				},
			},
			err: nil,
		},
		{
			desc: "test utxo has been spent",
			tx: &bc.Tx{
				TxHeader:       &bc.TxHeader{},
				SpentOutputIDs: []bc.Hash{{V0: 0}, {V0: 1}, {V0: 2}},
				Entries: map[bc.Hash]bc.Entry{
					bc.Hash{V0: 0}: &bc.IntraChainOutput{
						Source: &bc.ValueSource{
							Value: &bc.AssetAmount{
								AssetId: consensus.BTMAssetID,
								Amount:  100,
							},
						},
					},
					bc.Hash{V0: 1}: &bc.VoteOutput{
						Source: &bc.ValueSource{
							Value: &bc.AssetAmount{
								AssetId: &bc.AssetID{V0: 1},
							},
						},
					},
					bc.Hash{V0: 2}: &bc.IntraChainOutput{
						Source: &bc.ValueSource{
							Value: &bc.AssetAmount{
								AssetId: consensus.BTMAssetID,
								Amount:  100,
							},
						},
					},
				},
			},
			prevUTXOView: &UtxoViewpoint{
				Entries: map[bc.Hash]*storage.UtxoEntry{
					bc.Hash{V0: 0}: storage.NewUtxoEntry(storage.NormalUTXOType, 0, false),
					bc.Hash{V0: 1}: storage.NewUtxoEntry(storage.VoteUTXOType, 0, true),
				},
			},
			postUTXOView: &UtxoViewpoint{
				Entries: map[bc.Hash]*storage.UtxoEntry{},
			},
			err: errors.New("try to revert an unspent utxo"),
		},
	}

	for i, c := range cases {
		if err := c.prevUTXOView.detachSpendUtxo(c.tx, c.statusFail); err != nil {
			if err.Error() != c.err.Error() {
				t.Errorf("test case #%d want err = %v, got err = %v", i, err.Error(), c.err.Error())
			}
			continue
		}

		if !testutil.DeepEqual(c.prevUTXOView, c.postUTXOView) {
			t.Errorf("test case #%d, want %v, got %v", i, spew.Sdump(c.postUTXOView), spew.Sdump(c.prevUTXOView))
		}
	}
}
