package state

import (
	"encoding/hex"
	"math"
	"reflect"
	"testing"

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

	"github.com/vapor/consensus"
	"github.com/vapor/crypto/ed25519/chainkd"
	"github.com/vapor/errors"
	"github.com/vapor/math/checked"
	"github.com/vapor/protocol/bc"
	"github.com/vapor/protocol/bc/types"
	"github.com/vapor/testutil"
)

func TestApplyTransaction(t *testing.T) {
	testXpub, _ := hex.DecodeString("a8018a1ba4d85fc7118bbd065612da78b2c503e61a1a093d9c659567c5d3a591b3752569fbcafa951b2304b8f576f3f220e03b957ca819840e7c29e4b7fb2c4d")

	cases := []struct {
		desc                string
		tx                  *types.Tx
		prevConsensusResult *ConsensusResult
		postConsensusResult *ConsensusResult
		wantErr             error
	}{
		{
			desc: "test num Of vote overflow",
			tx: &types.Tx{
				TxData: types.TxData{
					Inputs:  []*types.TxInput{types.NewSpendInput(nil, bc.NewHash([32]byte{0xff}), *consensus.BTMAssetID, 600000000, 0, nil)},
					Outputs: []*types.TxOutput{types.NewVoteOutput(*consensus.BTMAssetID, math.MaxUint64-1000, []byte{0x51}, testXpub)},
				},
			},
			prevConsensusResult: &ConsensusResult{
				NumOfVote: map[string]uint64{
					hex.EncodeToString(testXpub): 1000000,
				},
			},
			postConsensusResult: &ConsensusResult{
				NumOfVote: map[string]uint64{},
			},
			wantErr: checked.ErrOverflow,
		},
		{
			desc: "test num Of veto overflow",
			tx: &types.Tx{
				TxData: types.TxData{
					Inputs:  []*types.TxInput{types.NewVetoInput(nil, bc.NewHash([32]byte{0xff}), *consensus.BTMAssetID, 100000000, 0, []byte{0x51}, testXpub)},
					Outputs: []*types.TxOutput{types.NewIntraChainOutput(*consensus.BTMAssetID, 100000000, []byte{0x51})},
				},
			},
			prevConsensusResult: &ConsensusResult{
				NumOfVote: map[string]uint64{
					hex.EncodeToString(testXpub): 1000000,
				},
			},
			postConsensusResult: &ConsensusResult{
				NumOfVote: map[string]uint64{},
			},
			wantErr: checked.ErrOverflow,
		},
		{
			desc: "test del pubkey from NumOfVote",
			tx: &types.Tx{
				TxData: types.TxData{
					Inputs:  []*types.TxInput{types.NewVetoInput(nil, bc.NewHash([32]byte{0xff}), *consensus.BTMAssetID, 1000000, 0, []byte{0x51}, testXpub)},
					Outputs: []*types.TxOutput{types.NewIntraChainOutput(*consensus.BTMAssetID, 100000000, []byte{0x51})},
				},
			},
			prevConsensusResult: &ConsensusResult{
				NumOfVote: map[string]uint64{
					hex.EncodeToString(testXpub): 1000000,
				},
			},
			postConsensusResult: &ConsensusResult{
				NumOfVote: map[string]uint64{},
			},
			wantErr: nil,
		},
	}

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

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

func TestAttachCoinbaseReward(t *testing.T) {
	cases := []struct {
		desc                string
		block               *types.Block
		prevConsensusResult *ConsensusResult
		postConsensusResult *ConsensusResult
		wantErr             error
	}{
		{
			desc: "normal test with block contain coinbase tx and other tx",
			block: &types.Block{
				BlockHeader: types.BlockHeader{
					Height: 1,
				},
				Transactions: []*types.Tx{
					{
						TxData: types.TxData{
							Inputs:  []*types.TxInput{types.NewCoinbaseInput([]byte{0x01})},
							Outputs: []*types.TxOutput{types.NewIntraChainOutput(bc.AssetID{}, 0, []byte{0x51})},
						},
					},
					{
						TxData: types.TxData{
							Inputs:  []*types.TxInput{types.NewSpendInput(nil, bc.NewHash([32]byte{0xff}), *consensus.BTMAssetID, 300000000, 0, nil)},
							Outputs: []*types.TxOutput{types.NewIntraChainOutput(*consensus.BTMAssetID, 250000000, []byte{0x51})},
						},
					},
				},
			},
			prevConsensusResult: &ConsensusResult{
				CoinbaseReward: map[string]uint64{
					hex.EncodeToString([]byte{0x51}): 50000000,
					hex.EncodeToString([]byte{0x52}): 80000000,
				},
			},
			postConsensusResult: &ConsensusResult{
				CoinbaseReward: map[string]uint64{
					hex.EncodeToString([]byte{0x51}): consensus.BlockSubsidy(1) + 50000000,
				},
			},
			wantErr: nil,
		},
		{
			desc: "test coinbase reward overflow",
			block: &types.Block{
				BlockHeader: types.BlockHeader{
					Height: 100,
				},
				Transactions: []*types.Tx{
					{
						TxData: types.TxData{
							Inputs:  []*types.TxInput{types.NewCoinbaseInput([]byte{0x01})},
							Outputs: []*types.TxOutput{types.NewIntraChainOutput(bc.AssetID{}, 0, []byte{0x51})},
						},
					},
					{
						TxData: types.TxData{
							Inputs:  []*types.TxInput{types.NewSpendInput(nil, bc.NewHash([32]byte{0xff}), *consensus.BTMAssetID, math.MaxUint64-80000000, 0, nil)},
							Outputs: []*types.TxOutput{types.NewIntraChainOutput(*consensus.BTMAssetID, 0, []byte{0x52})},
						},
					},
				},
			},
			prevConsensusResult: &ConsensusResult{
				CoinbaseReward: map[string]uint64{
					hex.EncodeToString([]byte{0x51}): 80000000,
					hex.EncodeToString([]byte{0x52}): 50000000,
				},
			},
			postConsensusResult: &ConsensusResult{
				CoinbaseReward: map[string]uint64{
					hex.EncodeToString([]byte{0x51}): consensus.BlockSubsidy(1) + 50000000,
				},
			},
			wantErr: checked.ErrOverflow,
		},
	}

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

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

func TestCalCoinbaseReward(t *testing.T) {
	cases := []struct {
		desc       string
		block      *types.Block
		wantReward *CoinbaseReward
		wantErr    error
	}{
		{
			desc: "normal test with block contain coinbase tx and other tx",
			block: &types.Block{
				BlockHeader: types.BlockHeader{
					Height: 1,
				},
				Transactions: []*types.Tx{
					&types.Tx{
						TxData: types.TxData{
							Inputs:  []*types.TxInput{types.NewCoinbaseInput([]byte{0x01})},
							Outputs: []*types.TxOutput{types.NewIntraChainOutput(bc.AssetID{}, 0, []byte{0x51})},
						},
					},
					&types.Tx{
						TxData: types.TxData{
							Inputs:  []*types.TxInput{types.NewSpendInput(nil, bc.NewHash([32]byte{0xff}), *consensus.BTMAssetID, 300000000, 0, nil)},
							Outputs: []*types.TxOutput{types.NewIntraChainOutput(*consensus.BTMAssetID, 250000000, []byte{0x51})},
						},
					},
				},
			},
			wantReward: &CoinbaseReward{
				Amount:         consensus.BlockSubsidy(1) + 50000000,
				ControlProgram: []byte{0x51},
			},
		},
		{
			desc: "normal test with block only contain coinbase tx",
			block: &types.Block{
				BlockHeader: types.BlockHeader{
					Height: 1200,
				},
				Transactions: []*types.Tx{
					&types.Tx{
						TxData: types.TxData{
							Inputs:  []*types.TxInput{types.NewCoinbaseInput([]byte{0x01})},
							Outputs: []*types.TxOutput{types.NewIntraChainOutput(bc.AssetID{}, 1000000000, []byte{0x51})},
						},
					},
				},
			},
			wantReward: &CoinbaseReward{
				Amount:         consensus.BlockSubsidy(1),
				ControlProgram: []byte{0x51},
			},
		},
		{
			desc: "abnormal test with block not contain coinbase tx",
			block: &types.Block{
				BlockHeader: types.BlockHeader{
					Height: 1,
				},
				Transactions: []*types.Tx{},
			},
			wantErr: errors.New("not found coinbase receiver"),
		},
	}

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

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

func TestConsensusApplyBlock(t *testing.T) {
	testXpub, _ := hex.DecodeString("a8018a1ba4d85fc7118bbd065612da78b2c503e61a1a093d9c659567c5d3a591b3752569fbcafa951b2304b8f576f3f220e03b957ca819840e7c29e4b7fb2c4d")
	cases := []struct {
		desc            string
		block           *types.Block
		consensusResult *ConsensusResult
		wantResult      *ConsensusResult
		wantErr         error
	}{
		{
			desc: "normal test with block height is equal to 1",
			block: &types.Block{
				BlockHeader: types.BlockHeader{
					Height:            1,
					PreviousBlockHash: bc.Hash{V0: 1},
				},
				Transactions: []*types.Tx{
					&types.Tx{
						TxData: types.TxData{
							Inputs:  []*types.TxInput{types.NewCoinbaseInput([]byte{0x01})},
							Outputs: []*types.TxOutput{types.NewIntraChainOutput(bc.AssetID{}, 0, []byte{0x51})},
						},
					},
					&types.Tx{
						TxData: types.TxData{
							Inputs:  []*types.TxInput{types.NewSpendInput(nil, bc.NewHash([32]byte{0xff}), *consensus.BTMAssetID, 300000000, 0, nil)},
							Outputs: []*types.TxOutput{types.NewIntraChainOutput(*consensus.BTMAssetID, 250000000, []byte{0x51})},
						},
					},
				},
			},
			consensusResult: &ConsensusResult{
				Seq:            0,
				NumOfVote:      map[string]uint64{},
				CoinbaseReward: map[string]uint64{},
				BlockHash:      bc.Hash{V0: 1},
				BlockHeight:    0,
			},
			wantResult: &ConsensusResult{
				Seq:       1,
				NumOfVote: map[string]uint64{},
				CoinbaseReward: map[string]uint64{
					"51": consensus.BlockSubsidy(1) + 50000000,
				},
				BlockHash:   testutil.MustDecodeHash("50da6990965a16e97b739accca7eb8a0fadd47c8a742f77d18fa51ab60dd8724"),
				BlockHeight: 1,
			},
		},
		{
			desc: "normal test with block height is equal to RoundVoteBlockNums - 1",
			block: &types.Block{
				BlockHeader: types.BlockHeader{
					Height:            consensus.MainNetParams.RoundVoteBlockNums - 1,
					PreviousBlockHash: bc.Hash{V0: 1},
				},
				Transactions: []*types.Tx{
					&types.Tx{
						TxData: types.TxData{
							Inputs:  []*types.TxInput{types.NewCoinbaseInput([]byte{0x01})},
							Outputs: []*types.TxOutput{types.NewIntraChainOutput(bc.AssetID{}, 0, []byte{0x51})},
						},
					},
					&types.Tx{
						TxData: types.TxData{
							Inputs:  []*types.TxInput{types.NewSpendInput(nil, bc.NewHash([32]byte{0xff}), *consensus.BTMAssetID, 600000000, 0, nil)},
							Outputs: []*types.TxOutput{types.NewVoteOutput(*consensus.BTMAssetID, 600000000, []byte{0x51}, testXpub)},
						},
					},
				},
			},
			consensusResult: &ConsensusResult{
				NumOfVote:      map[string]uint64{},
				CoinbaseReward: map[string]uint64{},
				BlockHash:      bc.Hash{V0: 1},
				BlockHeight:    consensus.MainNetParams.RoundVoteBlockNums - 2,
			},
			wantResult: &ConsensusResult{
				Seq: 1,
				NumOfVote: map[string]uint64{
					"a8018a1ba4d85fc7118bbd065612da78b2c503e61a1a093d9c659567c5d3a591b3752569fbcafa951b2304b8f576f3f220e03b957ca819840e7c29e4b7fb2c4d": 600000000,
				},
				CoinbaseReward: map[string]uint64{
					"51": consensus.BlockSubsidy(consensus.MainNetParams.RoundVoteBlockNums - 1),
				},
				BlockHash:   testutil.MustDecodeHash("4ebd9e7c00d3e0370931689c6eb9e2131c6700fe66e6b9718028dd75d7a4e329"),
				BlockHeight: consensus.MainNetParams.RoundVoteBlockNums - 1,
			},
		},
		{
			desc: "normal test with block height is equal to RoundVoteBlockNums",
			block: &types.Block{
				BlockHeader: types.BlockHeader{
					Height:            consensus.MainNetParams.RoundVoteBlockNums,
					PreviousBlockHash: bc.Hash{V0: 1},
				},
				Transactions: []*types.Tx{
					&types.Tx{
						TxData: types.TxData{
							Inputs: []*types.TxInput{types.NewCoinbaseInput([]byte{0x01})},
							Outputs: []*types.TxOutput{
								types.NewIntraChainOutput(bc.AssetID{}, 0, []byte{0x51}),
							},
						},
					},
					&types.Tx{
						TxData: types.TxData{
							Inputs:  []*types.TxInput{types.NewSpendInput(nil, bc.NewHash([32]byte{0xff}), *consensus.BTMAssetID, 600000000, 0, nil)},
							Outputs: []*types.TxOutput{types.NewVoteOutput(*consensus.BTMAssetID, 600000000, []byte{0x51}, testXpub)},
						},
					},
					&types.Tx{
						TxData: types.TxData{
							Inputs:  []*types.TxInput{types.NewVetoInput(nil, bc.NewHash([32]byte{0xff}), *consensus.BTMAssetID, 100000000, 0, []byte{0x51}, testXpub)},
							Outputs: []*types.TxOutput{types.NewIntraChainOutput(*consensus.BTMAssetID, 100000000, []byte{0x51})},
						},
					},
				},
			},
			consensusResult: &ConsensusResult{
				NumOfVote: map[string]uint64{},
				CoinbaseReward: map[string]uint64{
					"51": 10000000,
					"52": 20000000,
					"53": 30000000,
				},
				BlockHash:   bc.Hash{V0: 1},
				BlockHeight: consensus.MainNetParams.RoundVoteBlockNums - 1,
			},
			wantResult: &ConsensusResult{
				Seq: 1,
				NumOfVote: map[string]uint64{
					"a8018a1ba4d85fc7118bbd065612da78b2c503e61a1a093d9c659567c5d3a591b3752569fbcafa951b2304b8f576f3f220e03b957ca819840e7c29e4b7fb2c4d": 500000000,
				},
				CoinbaseReward: map[string]uint64{
					"51": consensus.BlockSubsidy(consensus.MainNetParams.RoundVoteBlockNums) + 10000000,
					"52": 20000000,
					"53": 30000000,
				},
				BlockHash:   testutil.MustDecodeHash("1b449ba1f9b0ae41e31238b32943b95e9ab292d0b4a93d690ef9bc689c31d362"),
				BlockHeight: consensus.MainNetParams.RoundVoteBlockNums,
			},
		},
		{
			desc: "normal test with block height is equal to RoundVoteBlockNums + 1",
			block: &types.Block{
				BlockHeader: types.BlockHeader{
					Height:            consensus.MainNetParams.RoundVoteBlockNums + 1,
					PreviousBlockHash: bc.Hash{V0: 1},
				},
				Transactions: []*types.Tx{
					&types.Tx{
						TxData: types.TxData{
							Inputs: []*types.TxInput{types.NewCoinbaseInput([]byte{0x01})},
							Outputs: []*types.TxOutput{
								types.NewIntraChainOutput(bc.AssetID{}, 0, []byte{0x51}),
								types.NewIntraChainOutput(bc.AssetID{}, consensus.BlockSubsidy(consensus.MainNetParams.RoundVoteBlockNums)+10000000, []byte{0x51}),
								types.NewIntraChainOutput(bc.AssetID{}, 20000000, []byte{0x53}),
								types.NewIntraChainOutput(bc.AssetID{}, 30000000, []byte{0x52}),
							},
						},
					},
				},
			},
			consensusResult: &ConsensusResult{
				NumOfVote: map[string]uint64{},
				CoinbaseReward: map[string]uint64{
					"51": consensus.BlockSubsidy(consensus.MainNetParams.RoundVoteBlockNums) + 10000000,
					"52": 20000000,
					"53": 30000000,
				},
				BlockHash:   bc.Hash{V0: 1},
				BlockHeight: consensus.MainNetParams.RoundVoteBlockNums,
			},
			wantResult: &ConsensusResult{
				Seq:       2,
				NumOfVote: map[string]uint64{},
				CoinbaseReward: map[string]uint64{
					"51": consensus.BlockSubsidy(consensus.MainNetParams.RoundVoteBlockNums + 1),
				},
				BlockHash:   testutil.MustDecodeHash("52681d209ab811359f92daaf46a771ecd0f28505ae5e0ac2f0feb80f76fdda59"),
				BlockHeight: consensus.MainNetParams.RoundVoteBlockNums + 1,
			},
		},
		{
			desc: "normal test with block height is equal to RoundVoteBlockNums + 2",
			block: &types.Block{
				BlockHeader: types.BlockHeader{
					Height:            consensus.MainNetParams.RoundVoteBlockNums + 2,
					PreviousBlockHash: bc.Hash{V0: 1},
				},
				Transactions: []*types.Tx{
					&types.Tx{
						TxData: types.TxData{
							Inputs: []*types.TxInput{types.NewCoinbaseInput([]byte{0x01})},
							Outputs: []*types.TxOutput{
								types.NewIntraChainOutput(bc.AssetID{}, 0, []byte{0x51}),
							},
						},
					},
				},
			},
			consensusResult: &ConsensusResult{
				NumOfVote: map[string]uint64{},
				CoinbaseReward: map[string]uint64{
					"51": consensus.BlockSubsidy(consensus.MainNetParams.RoundVoteBlockNums + 1),
				},
				BlockHash:   bc.Hash{V0: 1},
				BlockHeight: consensus.MainNetParams.RoundVoteBlockNums + 1,
			},
			wantResult: &ConsensusResult{
				Seq:       2,
				NumOfVote: map[string]uint64{},
				CoinbaseReward: map[string]uint64{
					"51": consensus.BlockSubsidy(consensus.MainNetParams.RoundVoteBlockNums+1) + consensus.BlockSubsidy(consensus.MainNetParams.RoundVoteBlockNums+2),
				},
				BlockHash:   testutil.MustDecodeHash("3de69f8af48b77e81232c71d30b25dd4ac482be45402a0fd417a4a040c135b76"),
				BlockHeight: consensus.MainNetParams.RoundVoteBlockNums + 2,
			},
		},
		{
			desc: "abnormal test with block parent hash is not equals last block hash of vote result",
			block: &types.Block{
				BlockHeader: types.BlockHeader{
					Height:            1,
					PreviousBlockHash: bc.Hash{V0: 0},
				},
				Transactions: []*types.Tx{},
			},
			consensusResult: &ConsensusResult{
				NumOfVote:      map[string]uint64{},
				CoinbaseReward: map[string]uint64{},
				BlockHash:      bc.Hash{V0: 1},
				BlockHeight:    2,
			},
			wantErr: errors.New("block parent hash is not equals last block hash of vote result"),
		},
		{
			desc: "abnormal test with arithmetic overflow for calculate transaction fee",
			block: &types.Block{
				BlockHeader: types.BlockHeader{
					Height:            1,
					PreviousBlockHash: bc.Hash{V0: 1},
				},
				Transactions: []*types.Tx{
					&types.Tx{
						TxData: types.TxData{
							Inputs:  []*types.TxInput{types.NewCoinbaseInput([]byte{0x01})},
							Outputs: []*types.TxOutput{types.NewIntraChainOutput(bc.AssetID{}, 0, []byte{0x51})},
						},
					},
					&types.Tx{
						TxData: types.TxData{
							Inputs:  []*types.TxInput{types.NewSpendInput(nil, bc.NewHash([32]byte{0xff}), *consensus.BTMAssetID, 100000000, 0, nil)},
							Outputs: []*types.TxOutput{types.NewIntraChainOutput(*consensus.BTMAssetID, 200000000, []byte{0x51})},
						},
					},
				},
			},
			consensusResult: &ConsensusResult{
				NumOfVote:      map[string]uint64{},
				CoinbaseReward: map[string]uint64{},
				BlockHash:      bc.Hash{V0: 1},
				BlockHeight:    2,
			},
			wantErr: errors.Wrap(checked.ErrOverflow, "calculate transaction fee"),
		},
		{
			desc: "abnormal test with not found coinbase receiver",
			block: &types.Block{
				BlockHeader: types.BlockHeader{
					Height:            1,
					PreviousBlockHash: bc.Hash{V0: 1},
				},
				Transactions: []*types.Tx{},
			},
			consensusResult: &ConsensusResult{
				NumOfVote:      map[string]uint64{},
				CoinbaseReward: map[string]uint64{},
				BlockHash:      bc.Hash{V0: 1},
				BlockHeight:    2,
			},
			wantErr: errors.New("not found coinbase receiver"),
		},
	}

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

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

func TestConsensusDetachBlock(t *testing.T) {
	testXpub, _ := hex.DecodeString("a8018a1ba4d85fc7118bbd065612da78b2c503e61a1a093d9c659567c5d3a591b3752569fbcafa951b2304b8f576f3f220e03b957ca819840e7c29e4b7fb2c4d")
	cases := []struct {
		desc            string
		block           *types.Block
		consensusResult *ConsensusResult
		wantResult      *ConsensusResult
		wantErr         error
	}{
		{
			desc: "normal test with block height is equal to 1",
			block: &types.Block{
				BlockHeader: types.BlockHeader{
					Height:            1,
					PreviousBlockHash: bc.Hash{V0: 1},
				},
				Transactions: []*types.Tx{
					&types.Tx{
						TxData: types.TxData{
							Inputs:  []*types.TxInput{types.NewCoinbaseInput([]byte{0x01})},
							Outputs: []*types.TxOutput{types.NewIntraChainOutput(bc.AssetID{}, 0, []byte{0x51})},
						},
					},
					&types.Tx{
						TxData: types.TxData{
							Inputs:  []*types.TxInput{types.NewSpendInput(nil, bc.NewHash([32]byte{0xff}), *consensus.BTMAssetID, 300000000, 0, nil)},
							Outputs: []*types.TxOutput{types.NewIntraChainOutput(*consensus.BTMAssetID, 250000000, []byte{0x51})},
						},
					},
				},
			},
			consensusResult: &ConsensusResult{
				Seq:       1,
				NumOfVote: map[string]uint64{},
				CoinbaseReward: map[string]uint64{
					"51": consensus.BlockSubsidy(1) + 50000000,
				},
				BlockHash:   testutil.MustDecodeHash("50da6990965a16e97b739accca7eb8a0fadd47c8a742f77d18fa51ab60dd8724"),
				BlockHeight: 1,
			},
			wantResult: &ConsensusResult{
				Seq:            0,
				NumOfVote:      map[string]uint64{},
				CoinbaseReward: map[string]uint64{},
				BlockHash:      bc.Hash{V0: 1},
				BlockHeight:    0,
			},
		},
		{
			desc: "normal test with block height is equal to RoundVoteBlockNums - 1",
			block: &types.Block{
				BlockHeader: types.BlockHeader{
					Height:            consensus.MainNetParams.RoundVoteBlockNums - 1,
					PreviousBlockHash: bc.Hash{V0: 1},
				},
				Transactions: []*types.Tx{
					&types.Tx{
						TxData: types.TxData{
							Inputs:  []*types.TxInput{types.NewCoinbaseInput([]byte{0x01})},
							Outputs: []*types.TxOutput{types.NewIntraChainOutput(bc.AssetID{}, 0, []byte{0x51})},
						},
					},
					&types.Tx{
						TxData: types.TxData{
							Inputs:  []*types.TxInput{types.NewSpendInput(nil, bc.NewHash([32]byte{0xff}), *consensus.BTMAssetID, 600000000, 0, nil)},
							Outputs: []*types.TxOutput{types.NewVoteOutput(*consensus.BTMAssetID, 600000000, []byte{0x51}, testXpub)},
						},
					},
				},
			},
			consensusResult: &ConsensusResult{
				NumOfVote: map[string]uint64{
					"a8018a1ba4d85fc7118bbd065612da78b2c503e61a1a093d9c659567c5d3a591b3752569fbcafa951b2304b8f576f3f220e03b957ca819840e7c29e4b7fb2c4d": 600000000,
				},
				CoinbaseReward: map[string]uint64{
					"51": consensus.BlockSubsidy(consensus.MainNetParams.RoundVoteBlockNums - 1),
				},
				BlockHash:   testutil.MustDecodeHash("4ebd9e7c00d3e0370931689c6eb9e2131c6700fe66e6b9718028dd75d7a4e329"),
				BlockHeight: consensus.MainNetParams.RoundVoteBlockNums - 1,
			},
			wantResult: &ConsensusResult{
				Seq:            1,
				NumOfVote:      map[string]uint64{},
				CoinbaseReward: map[string]uint64{},
				BlockHash:      bc.Hash{V0: 1},
				BlockHeight:    consensus.MainNetParams.RoundVoteBlockNums - 2,
			},
		},
		{
			desc: "normal test with block height is equal to RoundVoteBlockNums",
			block: &types.Block{
				BlockHeader: types.BlockHeader{
					Height:            consensus.MainNetParams.RoundVoteBlockNums,
					PreviousBlockHash: bc.Hash{V0: 1},
				},
				Transactions: []*types.Tx{
					&types.Tx{
						TxData: types.TxData{
							Inputs: []*types.TxInput{types.NewCoinbaseInput([]byte{0x01})},
							Outputs: []*types.TxOutput{
								types.NewIntraChainOutput(bc.AssetID{}, 0, []byte{0x51}),
							},
						},
					},
				},
			},
			consensusResult: &ConsensusResult{
				NumOfVote: map[string]uint64{
					"a8018a1ba4d85fc7118bbd065612da78b2c503e61a1a093d9c659567c5d3a591b3752569fbcafa951b2304b8f576f3f220e03b957ca819840e7c29e4b7fb2c4d": 500000000,
				},
				CoinbaseReward: map[string]uint64{
					"51": consensus.BlockSubsidy(consensus.MainNetParams.RoundVoteBlockNums) + 100000000,
				},
				BlockHash:   testutil.MustDecodeHash("1b449ba1f9b0ae41e31238b32943b95e9ab292d0b4a93d690ef9bc689c31d362"),
				BlockHeight: consensus.MainNetParams.RoundVoteBlockNums,
			},
			wantResult: &ConsensusResult{
				Seq: 1,
				NumOfVote: map[string]uint64{
					"a8018a1ba4d85fc7118bbd065612da78b2c503e61a1a093d9c659567c5d3a591b3752569fbcafa951b2304b8f576f3f220e03b957ca819840e7c29e4b7fb2c4d": 500000000,
				},
				CoinbaseReward: map[string]uint64{
					"51": 100000000,
				},
				BlockHash:   bc.Hash{V0: 1},
				BlockHeight: consensus.MainNetParams.RoundVoteBlockNums - 1,
			},
		},
		{
			desc: "normal test with block height is equal to RoundVoteBlockNums + 1",
			block: &types.Block{
				BlockHeader: types.BlockHeader{
					Height:            consensus.MainNetParams.RoundVoteBlockNums + 1,
					PreviousBlockHash: bc.Hash{V0: 1},
				},
				Transactions: []*types.Tx{
					&types.Tx{
						TxData: types.TxData{
							Inputs: []*types.TxInput{types.NewCoinbaseInput([]byte{0x01})},
							Outputs: []*types.TxOutput{
								types.NewIntraChainOutput(bc.AssetID{}, 0, []byte{0x51}),
								types.NewIntraChainOutput(bc.AssetID{}, consensus.BlockSubsidy(consensus.MainNetParams.RoundVoteBlockNums)+10000000, []byte{0x51}),
								types.NewIntraChainOutput(bc.AssetID{}, 20000000, []byte{0x52}),
							},
						},
					},
				},
			},
			consensusResult: &ConsensusResult{
				NumOfVote: map[string]uint64{},
				CoinbaseReward: map[string]uint64{
					"51": consensus.BlockSubsidy(consensus.MainNetParams.RoundVoteBlockNums + 1),
				},
				BlockHash:   testutil.MustDecodeHash("52681d209ab811359f92daaf46a771ecd0f28505ae5e0ac2f0feb80f76fdda59"),
				BlockHeight: consensus.MainNetParams.RoundVoteBlockNums,
			},
			wantResult: &ConsensusResult{
				Seq:       1,
				NumOfVote: map[string]uint64{},
				CoinbaseReward: map[string]uint64{
					"51": consensus.BlockSubsidy(consensus.MainNetParams.RoundVoteBlockNums) + 10000000,
					"52": 20000000,
				},
				BlockHash:   bc.Hash{V0: 1},
				BlockHeight: consensus.MainNetParams.RoundVoteBlockNums,
			},
		},
		{
			desc: "normal test with block height is equal to RoundVoteBlockNums + 2",
			block: &types.Block{
				BlockHeader: types.BlockHeader{
					Height:            consensus.MainNetParams.RoundVoteBlockNums + 2,
					PreviousBlockHash: bc.Hash{V0: 1},
				},
				Transactions: []*types.Tx{
					&types.Tx{
						TxData: types.TxData{
							Inputs: []*types.TxInput{types.NewCoinbaseInput([]byte{0x01})},
							Outputs: []*types.TxOutput{
								types.NewIntraChainOutput(bc.AssetID{}, 0, []byte{0x51}),
							},
						},
					},
				},
			},
			consensusResult: &ConsensusResult{
				NumOfVote: map[string]uint64{},
				CoinbaseReward: map[string]uint64{
					"51": consensus.BlockSubsidy(consensus.MainNetParams.RoundVoteBlockNums+1) + 1000000,
				},
				BlockHash:   testutil.MustDecodeHash("3de69f8af48b77e81232c71d30b25dd4ac482be45402a0fd417a4a040c135b76"),
				BlockHeight: consensus.MainNetParams.RoundVoteBlockNums + 1,
			},
			wantResult: &ConsensusResult{
				Seq:       2,
				NumOfVote: map[string]uint64{},
				CoinbaseReward: map[string]uint64{
					"51": 1000000,
				},
				BlockHash:   bc.Hash{V0: 1},
				BlockHeight: consensus.MainNetParams.RoundVoteBlockNums + 1,
			},
		},
		{
			desc: "abnormal test with block hash is not equals last block hash of vote result",
			block: &types.Block{
				BlockHeader: types.BlockHeader{
					Height:            2,
					PreviousBlockHash: bc.Hash{V0: 0},
				},
				Transactions: []*types.Tx{},
			},
			consensusResult: &ConsensusResult{
				NumOfVote:      map[string]uint64{},
				CoinbaseReward: map[string]uint64{},
				BlockHash:      bc.Hash{V0: 1},
				BlockHeight:    1,
			},
			wantErr: errors.New("block hash is not equals last block hash of vote result"),
		},
		{
			desc: "abnormal test with arithmetic overflow for calculate transaction fee",
			block: &types.Block{
				BlockHeader: types.BlockHeader{
					Height:            2,
					PreviousBlockHash: bc.Hash{V0: 1},
				},
				Transactions: []*types.Tx{
					&types.Tx{
						TxData: types.TxData{
							Inputs:  []*types.TxInput{types.NewCoinbaseInput([]byte{0x01})},
							Outputs: []*types.TxOutput{types.NewIntraChainOutput(bc.AssetID{}, 0, []byte{0x51})},
						},
					},
					&types.Tx{
						TxData: types.TxData{
							Inputs:  []*types.TxInput{types.NewSpendInput(nil, bc.NewHash([32]byte{0xff}), *consensus.BTMAssetID, 100000000, 0, nil)},
							Outputs: []*types.TxOutput{types.NewIntraChainOutput(*consensus.BTMAssetID, 200000000, []byte{0x51})},
						},
					},
				},
			},
			consensusResult: &ConsensusResult{
				NumOfVote:      map[string]uint64{},
				CoinbaseReward: map[string]uint64{},
				BlockHash:      testutil.MustDecodeHash("02b7fb48defc4f4a3e1ef8403f7c0be78c4414ee66aa81fd702caa1e41a906df"),
				BlockHeight:    1,
			},
			wantErr: errors.Wrap(checked.ErrOverflow, "calculate transaction fee"),
		},
		{
			desc: "abnormal test with not found coinbase receiver",
			block: &types.Block{
				BlockHeader: types.BlockHeader{
					Height:            1,
					PreviousBlockHash: bc.Hash{V0: 1},
				},
				Transactions: []*types.Tx{},
			},
			consensusResult: &ConsensusResult{
				NumOfVote:      map[string]uint64{},
				CoinbaseReward: map[string]uint64{},
				BlockHash:      testutil.MustDecodeHash("50da6990965a16e97b739accca7eb8a0fadd47c8a742f77d18fa51ab60dd8724"),
				BlockHeight:    1,
			},
			wantErr: errors.New("not found coinbase receiver"),
		},
		{
			desc: "test number of vote overflow",
			block: &types.Block{
				BlockHeader: types.BlockHeader{
					Height:            consensus.MainNetParams.RoundVoteBlockNums - 1,
					PreviousBlockHash: bc.Hash{V0: 1},
				},
				Transactions: []*types.Tx{
					&types.Tx{
						TxData: types.TxData{
							Inputs:  []*types.TxInput{types.NewCoinbaseInput([]byte{0x01})},
							Outputs: []*types.TxOutput{types.NewIntraChainOutput(bc.AssetID{}, 0, []byte{0x51})},
						},
					},
					&types.Tx{
						TxData: types.TxData{
							Inputs:  []*types.TxInput{types.NewSpendInput(nil, bc.NewHash([32]byte{0xff}), *consensus.BTMAssetID, 600000000, 0, nil)},
							Outputs: []*types.TxOutput{types.NewVoteOutput(*consensus.BTMAssetID, 600000000, []byte{0x51}, testXpub)},
						},
					},
				},
			},
			consensusResult: &ConsensusResult{
				BlockHash: testutil.MustDecodeHash("4ebd9e7c00d3e0370931689c6eb9e2131c6700fe66e6b9718028dd75d7a4e329"),
				CoinbaseReward: map[string]uint64{
					"51": 100000000,
				},
				NumOfVote: map[string]uint64{},
			},
			wantErr: checked.ErrOverflow,
		},
		{
			desc: "test number of veto overflow",
			block: &types.Block{
				BlockHeader: types.BlockHeader{
					Height:            consensus.MainNetParams.RoundVoteBlockNums - 1,
					PreviousBlockHash: bc.Hash{V0: 1},
				},
				Transactions: []*types.Tx{
					&types.Tx{
						TxData: types.TxData{
							Inputs:  []*types.TxInput{types.NewCoinbaseInput([]byte{0x01})},
							Outputs: []*types.TxOutput{types.NewIntraChainOutput(bc.AssetID{}, 0, []byte{0x51})},
						},
					},
					&types.Tx{
						TxData: types.TxData{
							Inputs:  []*types.TxInput{types.NewVetoInput(nil, bc.NewHash([32]byte{0xff}), *consensus.BTMAssetID, math.MaxUint64, 0, []byte{0x51}, testXpub)},
							Outputs: []*types.TxOutput{types.NewIntraChainOutput(*consensus.BTMAssetID, math.MaxUint64, []byte{0x51})},
						},
					},
				},
			},
			consensusResult: &ConsensusResult{
				BlockHash: testutil.MustDecodeHash("4ebd9e7c00d3e0370931689c6eb9e2131c6700fe66e6b9718028dd75d7a4e329"),
				CoinbaseReward: map[string]uint64{
					"51": 100000000,
				},
				NumOfVote: map[string]uint64{
					"a8018a1ba4d85fc7118bbd065612da78b2c503e61a1a093d9c659567c5d3a591b3752569fbcafa951b2304b8f576f3f220e03b957ca819840e7c29e4b7fb2c4d": 100,
				},
			},
			wantErr: checked.ErrOverflow,
		},
		{
			desc: "test detch coinbase overflow",
			block: &types.Block{
				BlockHeader: types.BlockHeader{
					Height:            consensus.MainNetParams.RoundVoteBlockNums - 1,
					PreviousBlockHash: bc.Hash{V0: 1},
				},
				Transactions: []*types.Tx{
					&types.Tx{
						TxData: types.TxData{
							Inputs:  []*types.TxInput{types.NewCoinbaseInput([]byte{0x01})},
							Outputs: []*types.TxOutput{types.NewIntraChainOutput(bc.AssetID{}, 0, []byte{0x51})},
						},
					},
					&types.Tx{
						TxData: types.TxData{
							Inputs:  []*types.TxInput{types.NewVetoInput(nil, bc.NewHash([32]byte{0xff}), *consensus.BTMAssetID, math.MaxUint64, 0, []byte{0x51}, testXpub)},
							Outputs: []*types.TxOutput{types.NewIntraChainOutput(*consensus.BTMAssetID, math.MaxUint64, []byte{0x51})},
						},
					},
				},
			},
			consensusResult: &ConsensusResult{
				BlockHash:      testutil.MustDecodeHash("4ebd9e7c00d3e0370931689c6eb9e2131c6700fe66e6b9718028dd75d7a4e329"),
				CoinbaseReward: map[string]uint64{},
				NumOfVote:      map[string]uint64{},
			},
			wantErr: checked.ErrOverflow,
		},
	}
	for i, c := range cases {
		if err := c.consensusResult.DetachBlock(c.block); err != nil {
			if err.Error() != c.wantErr.Error() {
				t.Errorf("test case #%d want err = %v, got err = %v", i, c.wantErr, err)
			}
			continue
		}

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

func TestGetCoinbaseRewards(t *testing.T) {
	cases := []struct {
		desc            string
		blockHeight     uint64
		consensusResult *ConsensusResult
		wantRewards     []CoinbaseReward
	}{
		{
			desc:        "the block height is RoundVoteBlockNums - 1",
			blockHeight: consensus.ActiveNetParams.RoundVoteBlockNums - 1,
			consensusResult: &ConsensusResult{
				CoinbaseReward: map[string]uint64{
					"51": 10,
				},
			},
			wantRewards: []CoinbaseReward{},
		},
		{
			desc:        "the block height is RoundVoteBlockNums",
			blockHeight: consensus.ActiveNetParams.RoundVoteBlockNums,
			consensusResult: &ConsensusResult{
				CoinbaseReward: map[string]uint64{
					"51": 10,
					"52": 20,
				},
			},
			wantRewards: []CoinbaseReward{
				CoinbaseReward{
					Amount:         20,
					ControlProgram: []byte{0x52},
				},
				CoinbaseReward{
					Amount:         10,
					ControlProgram: []byte{0x51},
				},
			},
		},
		{
			desc:        "the block height is RoundVoteBlockNums + 1",
			blockHeight: consensus.ActiveNetParams.RoundVoteBlockNums + 1,
			consensusResult: &ConsensusResult{
				CoinbaseReward: map[string]uint64{
					"51": 10,
				},
			},
			wantRewards: []CoinbaseReward{},
		},
		{
			desc:        "the block height is RoundVoteBlockNums * 2",
			blockHeight: consensus.ActiveNetParams.RoundVoteBlockNums * 2,
			consensusResult: &ConsensusResult{
				CoinbaseReward: map[string]uint64{
					"50": 20,
					"51": 10,
					"52": 20,
					"53": 30,
				},
			},
			wantRewards: []CoinbaseReward{
				CoinbaseReward{
					Amount:         30,
					ControlProgram: []byte{0x53},
				},
				CoinbaseReward{
					Amount:         20,
					ControlProgram: []byte{0x52},
				},
				CoinbaseReward{
					Amount:         20,
					ControlProgram: []byte{0x50},
				},
				CoinbaseReward{
					Amount:         10,
					ControlProgram: []byte{0x51},
				},
			},
		},
		{
			desc:        "the block height is 2*RoundVoteBlockNums + 1",
			blockHeight: 2*consensus.ActiveNetParams.RoundVoteBlockNums + 1,
			consensusResult: &ConsensusResult{
				CoinbaseReward: map[string]uint64{},
			},
			wantRewards: []CoinbaseReward{},
		},
	}

	for i, c := range cases {
		rewards, err := c.consensusResult.GetCoinbaseRewards(c.blockHeight)
		if err != nil {
			t.Fatal(err)
		}

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

func TestConsensusNodes(t *testing.T) {
	var xpub1, xpub2, xpub3, xpub4, xpub5, xpub6, xpub7 chainkd.XPub
	strPub1 := "0f8669abbd3cc0a167156188e428f940088d5b2f36bb3449df71d2bdc5e077814ea3f68628eef279ed435f51ee26cff00f8bd28fabfd500bedb2a9e369f5c825"
	strPub2 := "e7f458ee8d2ba19b0fdc7410d1fd57e9c2e1a79377c661d66c55effe49d7ffc920e40510442d4a10b7bea06c09fb0b41f52601135adaaa7136204db36106c093"
	strPub3 := "1bec3a35da038ec7a76c40986e80b5af2dcef60341970e3fc58b4db0797bd4ca9b2cbf3d7ab820832e22a80b5b86ae1427f7f706a7780089958b2862e7bc0842"
	strPub4 := "b7f463446a31b3792cd168d52b7a89b3657bca3e25d6854db1488c389ab6fc8d538155c25c1ee6975cc7def19710908c7d9b7463ca34a22058b456b45e498db9"
	strPub5 := "b928e46bb01e834fdf167185e31b15de7cc257af8bbdf17f9c7fefd5bb97b306d048b6bc0da2097152c1c2ff38333c756a543adbba7030a447dcc776b8ac64ef"
	strPub6 := "36695997983028c279c3360ca345a90e3af1f9e3df2506119fca31cdc844be31630f9a421f4d1658e15d67a15ce29c36332dd45020d2a0147fcce4949ccd9a67"
	strPub7 := "123"

	xpub1.UnmarshalText([]byte(strPub1))
	xpub2.UnmarshalText([]byte(strPub2))
	xpub3.UnmarshalText([]byte(strPub3))
	xpub4.UnmarshalText([]byte(strPub4))
	xpub5.UnmarshalText([]byte(strPub5))
	xpub6.UnmarshalText([]byte(strPub6))
	xpub7.UnmarshalText([]byte(strPub7))

	cases := []struct {
		consensusResult *ConsensusResult
		consensusNode   map[string]*ConsensusNode
		wantErr         error
	}{
		{
			consensusResult: &ConsensusResult{
				NumOfVote: map[string]uint64{
					strPub1: 838063475500000,  //1
					strPub2: 474794800000000,  //3
					strPub3: 833812985000000,  //2
					strPub4: 285918061999999,  //4
					strPub5: 1228455289930297, //0
					strPub6: 274387690000000,  //5
					strPub7: 1028455289930297,
				},
			},
			consensusNode: map[string]*ConsensusNode{
				strPub1: &ConsensusNode{XPub: xpub1, VoteNum: 838063475500000, Order: 1},
				strPub2: &ConsensusNode{XPub: xpub2, VoteNum: 474794800000000, Order: 3},
				strPub3: &ConsensusNode{XPub: xpub3, VoteNum: 833812985000000, Order: 2},
				strPub4: &ConsensusNode{XPub: xpub4, VoteNum: 285918061999999, Order: 4},
				strPub5: &ConsensusNode{XPub: xpub5, VoteNum: 1228455289930297, Order: 0},
				strPub6: &ConsensusNode{XPub: xpub6, VoteNum: 274387690000000, Order: 5},
			},
			wantErr: chainkd.ErrBadKeyStr,
		},
	}

	for i, c := range cases {
		consensusNode, err := c.consensusResult.ConsensusNodes()
		if err != nil {
			if err != c.wantErr {
				t.Errorf("test case #%d want err = %v, got err = %v", i, c.wantErr, err)
			}
			continue
		}

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

func TestFork(t *testing.T) {
	consensusResult := &ConsensusResult{
		Seq: 100,
		NumOfVote: map[string]uint64{
			"a": 100,
			"b": 200,
		},
		CoinbaseReward: map[string]uint64{
			"c": 300,
			"d": 400,
		},
		BlockHash:   bc.NewHash([32]byte{0x1, 0x2}),
		BlockHeight: 1024,
	}
	copy := consensusResult.Fork()

	if !reflect.DeepEqual(consensusResult, copy) {
		t.Fatalf("failed on test consensusResult got %s want %s", spew.Sdump(copy), spew.Sdump(consensusResult))
	}
}
