package dex

import (
	"crypto/ecdsa"
	"fmt"
	"math/big"
	"testing"
	"time"

	coreCommon "github.com/dexon-foundation/dexon-consensus/common"
	coreTypes "github.com/dexon-foundation/dexon-consensus/core/types"

	"github.com/dexon-foundation/dexon/common"
	"github.com/dexon-foundation/dexon/consensus/dexcon"
	"github.com/dexon-foundation/dexon/core"
	"github.com/dexon-foundation/dexon/core/types"
	"github.com/dexon-foundation/dexon/core/vm"
	"github.com/dexon-foundation/dexon/crypto"
	"github.com/dexon-foundation/dexon/ethdb"
	"github.com/dexon-foundation/dexon/params"
	"github.com/dexon-foundation/dexon/rlp"
)

func TestPreparePayload(t *testing.T) {
	key, err := crypto.GenerateKey()
	if err != nil {
		t.Errorf("hex to ecdsa error: %v", err)
	}

	dex, err := newTestDexonWithGenesis(key)
	if err != nil {
		t.Errorf("new test dexon error: %v", err)
	}

	signer := types.NewEIP155Signer(dex.chainConfig.ChainID)

	var expectTx types.Transactions
	for i := 0; i < 5; i++ {
		tx, err := addTx(dex, i, signer, key)
		if err != nil {
			t.Errorf("add tx error: %v", err)
		}
		expectTx = append(expectTx, tx)
	}

	// This transaction will not be included.
	_, err = addTx(dex, 100, signer, key)
	if err != nil {
		t.Errorf("add tx error: %v", err)
	}

	chainID := new(big.Int).Mod(crypto.PubkeyToAddress(key.PublicKey).Big(),
		big.NewInt(int64(dex.chainConfig.Dexcon.NumChains)))
	var chainNum uint32
	for chainNum = 0; chainNum < dex.chainConfig.Dexcon.NumChains; chainNum++ {

		payload, err := dex.app.PreparePayload(coreTypes.Position{ChainID: chainNum})
		if err != nil {
			t.Errorf("prepare payload error: %v", err)
		}

		var transactions types.Transactions
		err = rlp.DecodeBytes(payload, &transactions)
		if err != nil {
			t.Errorf("rlp decode error: %v", err)
		}

		// Only one chain id allow prepare transactions.
		if chainID.Uint64() == uint64(chainNum) && len(transactions) != 5 {
			t.Errorf("incorrect transaction num expect %v but %v", 5, len(transactions))
		} else if chainID.Uint64() != uint64(chainNum) && len(transactions) != 0 {
			t.Errorf("expect empty slice but %v", len(transactions))
		}

		for i, tx := range transactions {
			if expectTx[i].Gas() != tx.Gas() {
				t.Errorf("unexpected gas expect %v but %v", expectTx[i].Gas(), tx.Gas())
			}

			if expectTx[i].Hash() != tx.Hash() {
				t.Errorf("unexpected hash expect %v but %v", expectTx[i].Hash(), tx.Hash())
			}

			if expectTx[i].Nonce() != tx.Nonce() {
				t.Errorf("unexpected nonce expect %v but %v", expectTx[i].Nonce(), tx.Nonce())
			}

			if expectTx[i].GasPrice().Uint64() != tx.GasPrice().Uint64() {
				t.Errorf("unexpected gas price expect %v but %v",
					expectTx[i].GasPrice().Uint64(), tx.GasPrice().Uint64())
			}
		}
	}
}

func TestPrepareWitness(t *testing.T) {
	key, err := crypto.GenerateKey()
	if err != nil {
		t.Errorf("hex to ecdsa error: %v", err)
	}

	dex, err := newTestDexonWithGenesis(key)
	if err != nil {
		t.Errorf("new test dexon error: %v", err)
	}

	currentBlock := dex.blockchain.CurrentBlock()

	witness, err := dex.app.PrepareWitness(0)
	if err != nil {
		t.Errorf("prepare witness error: %v", err)
	}

	if witness.Height != currentBlock.NumberU64() {
		t.Errorf("unexpeted witness height %v", witness.Height)
	}

	var witnessData witnessData
	err = rlp.DecodeBytes(witness.Data, &witnessData)
	if err != nil {
		t.Errorf("rlp decode error: %v", err)
	}

	if witnessData.TxHash != currentBlock.TxHash() {
		t.Errorf("expect tx hash %v but %v", currentBlock.TxHash(), witnessData.TxHash)
	}

	if witnessData.Root != currentBlock.Root() {
		t.Errorf("expect root %v but %v", currentBlock.Root(), witnessData.Root)
	}

	if witnessData.ReceiptHash != currentBlock.ReceiptHash() {
		t.Errorf("expect receipt hash %v but %v", currentBlock.ReceiptHash(), witnessData.ReceiptHash)
	}

	if _, err := dex.app.PrepareWitness(999); err == nil {
		t.Errorf("it must be get error from prepare")
	} else {
		t.Logf("Nice error: %v", err)
	}
}

func TestVerifyBlock(t *testing.T) {
	key, err := crypto.GenerateKey()
	if err != nil {
		t.Errorf("hex to ecdsa error: %v", err)
	}

	dex, err := newTestDexonWithGenesis(key)
	if err != nil {
		t.Errorf("new test dexon error: %v", err)
	}

	// Prepare first confirmed block.
	prepareConfirmedBlocks(dex, []*ecdsa.PrivateKey{key}, 0)

	chainID := new(big.Int).Mod(crypto.PubkeyToAddress(key.PublicKey).Big(),
		big.NewInt(int64(dex.chainConfig.Dexcon.NumChains)))

	// Prepare normal block.
	block := coreTypes.NewBlock()
	block.Hash = coreCommon.NewRandomHash()
	block.Position.ChainID = uint32(chainID.Uint64())
	block.Position.Height = 1
	block.Payload, block.Witness, err = prepareDataWithoutTxPool(dex, key, 0, 100)
	if err != nil {
		t.Errorf("prepare data error: %v", err)
	}

	// Expect ok.
	status := dex.app.VerifyBlock(block)
	if status != coreTypes.VerifyOK {
		t.Errorf("verify fail: %v", status)
	}

	// Prepare invalid nonce tx.
	block = coreTypes.NewBlock()
	block.Hash = coreCommon.NewRandomHash()
	block.Position.ChainID = uint32(chainID.Uint64())
	block.Position.Height = 1
	block.Payload, block.Witness, err = prepareDataWithoutTxPool(dex, key, 1, 100)
	if err != nil {
		t.Errorf("prepare data error: %v", err)
	}

	// Expect invalid block.
	status = dex.app.VerifyBlock(block)
	if status != coreTypes.VerifyInvalidBlock {
		t.Errorf("verify fail: %v", status)
	}

	// Prepare invalid block height.
	block = coreTypes.NewBlock()
	block.Hash = coreCommon.NewRandomHash()
	block.Position.ChainID = uint32(chainID.Uint64())
	block.Position.Height = 2
	block.Payload, block.Witness, err = prepareDataWithoutTxPool(dex, key, 0, 100)
	if err != nil {
		t.Errorf("prepare data error: %v", err)
	}

	// Expect retry later.
	status = dex.app.VerifyBlock(block)
	if status != coreTypes.VerifyRetryLater {
		t.Errorf("verify fail expect retry later but get %v", status)
	}

	// Prepare reach block limit.
	block = coreTypes.NewBlock()
	block.Hash = coreCommon.NewRandomHash()
	block.Position.ChainID = uint32(chainID.Uint64())
	block.Position.Height = 1
	block.Payload, block.Witness, err = prepareDataWithoutTxPool(dex, key, 0, 10000)
	if err != nil {
		t.Errorf("prepare data error: %v", err)
	}

	// Expect invalid block.
	status = dex.app.VerifyBlock(block)
	if status != coreTypes.VerifyInvalidBlock {
		t.Errorf("verify fail expect invalid block but get %v", status)
	}

	// Prepare insufficient funds.
	block = coreTypes.NewBlock()
	block.Hash = coreCommon.NewRandomHash()
	block.Position.ChainID = uint32(chainID.Uint64())
	block.Position.Height = 1
	_, block.Witness, err = prepareDataWithoutTxPool(dex, key, 0, 0)
	if err != nil {
		t.Errorf("prepare data error: %v", err)
	}

	signer := types.NewEIP155Signer(dex.chainConfig.ChainID)
	tx := types.NewTransaction(
		0,
		common.BytesToAddress([]byte{9}),
		big.NewInt(50000000000000001),
		params.TxGas,
		big.NewInt(1),
		nil)
	tx, err = types.SignTx(tx, signer, key)
	if err != nil {
		return
	}

	block.Payload, err = rlp.EncodeToBytes(types.Transactions{tx})
	if err != nil {
		return
	}

	// expect invalid block
	status = dex.app.VerifyBlock(block)
	if status != coreTypes.VerifyInvalidBlock {
		t.Errorf("verify fail expect invalid block but get %v", status)
	}

	// Prepare invalid intrinsic gas.
	block = coreTypes.NewBlock()
	block.Hash = coreCommon.NewRandomHash()
	block.Position.ChainID = uint32(chainID.Uint64())
	block.Position.Height = 1
	_, block.Witness, err = prepareDataWithoutTxPool(dex, key, 0, 0)
	if err != nil {
		t.Errorf("prepare data error: %v", err)
	}

	signer = types.NewEIP155Signer(dex.chainConfig.ChainID)
	tx = types.NewTransaction(
		0,
		common.BytesToAddress([]byte{9}),
		big.NewInt(1),
		params.TxGas,
		big.NewInt(1),
		make([]byte, 1))
	tx, err = types.SignTx(tx, signer, key)
	if err != nil {
		return
	}

	block.Payload, err = rlp.EncodeToBytes(types.Transactions{tx})
	if err != nil {
		return
	}

	// Expect invalid block.
	status = dex.app.VerifyBlock(block)
	if status != coreTypes.VerifyInvalidBlock {
		t.Errorf("verify fail expect invalid block but get %v", status)
	}

	// Prepare invalid transactions with nonce.
	block = coreTypes.NewBlock()
	block.Hash = coreCommon.NewRandomHash()
	block.Position.ChainID = uint32(chainID.Uint64())
	block.Position.Height = 1
	_, block.Witness, err = prepareDataWithoutTxPool(dex, key, 0, 0)
	if err != nil {
		t.Errorf("prepare data error: %v", err)
	}

	signer = types.NewEIP155Signer(dex.chainConfig.ChainID)
	tx1 := types.NewTransaction(
		0,
		common.BytesToAddress([]byte{9}),
		big.NewInt(1),
		params.TxGas,
		big.NewInt(1),
		make([]byte, 1))
	tx1, err = types.SignTx(tx, signer, key)
	if err != nil {
		return
	}

	// Invalid nonce.
	tx2 := types.NewTransaction(
		2,
		common.BytesToAddress([]byte{9}),
		big.NewInt(1),
		params.TxGas,
		big.NewInt(1),
		make([]byte, 1))
	tx2, err = types.SignTx(tx, signer, key)
	if err != nil {
		return
	}

	block.Payload, err = rlp.EncodeToBytes(types.Transactions{tx1, tx2})
	if err != nil {
		return
	}

	// Expect invalid block.
	status = dex.app.VerifyBlock(block)
	if status != coreTypes.VerifyInvalidBlock {
		t.Errorf("verify fail expect invalid block but get %v", status)
	}
}

func TestBlockConfirmed(t *testing.T) {
	key, err := crypto.GenerateKey()
	if err != nil {
		t.Errorf("hex to ecdsa error: %v", err)
	}

	dex, err := newTestDexonWithGenesis(key)
	if err != nil {
		t.Errorf("new test dexon error: %v", err)
	}

	chainID := new(big.Int).Mod(crypto.PubkeyToAddress(key.PublicKey).Big(),
		big.NewInt(int64(dex.chainConfig.Dexcon.NumChains)))

	var (
		expectCost    big.Int
		expectNonce   uint64
		expectCounter uint64
	)
	for i := 0; i < 10; i++ {
		var startNonce int
		if expectNonce != 0 {
			startNonce = int(expectNonce) + 1
		}
		payload, witness, cost, nonce, err := prepareData(dex, key, startNonce, 50)
		if err != nil {
			t.Errorf("prepare data error: %v", err)
		}
		expectCost.Add(&expectCost, &cost)
		expectNonce = nonce

		block := coreTypes.NewBlock()
		block.Hash = coreCommon.NewRandomHash()
		block.Witness = witness
		block.Payload = payload
		block.Position.ChainID = uint32(chainID.Uint64())

		dex.app.BlockConfirmed(*block)
		expectCounter++
	}

	info := dex.app.blockchain.GetAddressInfo(uint32(chainID.Uint64()),
		crypto.PubkeyToAddress(key.PublicKey))

	if info.Counter != expectCounter {
		t.Errorf("expect address counter is %v but %v", expectCounter, info.Counter)
	}

	if info.Cost.Cmp(&expectCost) != 0 {
		t.Errorf("expect address cost is %v but %v", expectCost.Uint64(), info.Cost.Uint64())
	}

	if info.Nonce != expectNonce {
		t.Errorf("expect address nonce is %v but %v", expectNonce, info.Nonce)
	}
}

func TestBlockDelivered(t *testing.T) {
	key, err := crypto.GenerateKey()
	if err != nil {
		t.Errorf("hex to ecdsa error: %v", err)
	}

	dex, err := newTestDexonWithGenesis(key)
	if err != nil {
		t.Errorf("new test dexon error: %v", err)
	}

	address := crypto.PubkeyToAddress(key.PublicKey)
	firstBlocksInfo, err := prepareConfirmedBlocks(dex, []*ecdsa.PrivateKey{key}, 50)
	if err != nil {
		t.Errorf("preapare confirmed block error: %v", err)
	}

	dex.app.BlockDelivered(firstBlocksInfo[0].Block.Hash, firstBlocksInfo[0].Block.Position,
		coreTypes.FinalizationResult{
			Timestamp: time.Now(),
			Height:    1,
		})

	_, pendingState := dex.blockchain.GetPending()
	currentBlock := dex.blockchain.CurrentBlock()
	if currentBlock.NumberU64() != 0 {
		t.Errorf("unexpected current block number %v", currentBlock.NumberU64())
	}

	pendingNonce := pendingState.GetNonce(address)
	if pendingNonce != firstBlocksInfo[0].Nonce+1 {
		t.Errorf("unexpected pending state nonce %v", pendingNonce)
	}

	balance := pendingState.GetBalance(address)
	if new(big.Int).Add(balance, &firstBlocksInfo[0].Cost).Cmp(big.NewInt(50000000000000000)) != 0 {
		t.Errorf("unexpected pending state balance %v", balance)
	}

	// prepare second block to witness first block
	secondBlocksInfo, err := prepareConfirmedBlocks(dex, []*ecdsa.PrivateKey{key}, 25)
	if err != nil {
		t.Errorf("preapare confirmed block error: %v", err)
	}

	dex.app.BlockDelivered(secondBlocksInfo[0].Block.Hash, secondBlocksInfo[0].Block.Position,
		coreTypes.FinalizationResult{
			Timestamp: time.Now(),
			Height:    2,
		})

	// second block witness first block, so current block number should be 1
	currentBlock = dex.blockchain.CurrentBlock()
	if currentBlock.NumberU64() != 1 {
		t.Errorf("unexpected current block number %v", currentBlock.NumberU64())
	}

	currentState, err := dex.blockchain.State()
	if err != nil {
		t.Errorf("current state error: %v", err)
	}

	currentNonce := currentState.GetNonce(address)
	if currentNonce != firstBlocksInfo[0].Nonce+1 {
		t.Errorf("unexpected current state nonce %v", currentNonce)
	}

	balance = currentState.GetBalance(address)
	if new(big.Int).Add(balance, &firstBlocksInfo[0].Cost).Cmp(big.NewInt(50000000000000000)) != 0 {
		t.Errorf("unexpected current state balance %v", balance)
	}
}

func BenchmarkBlockDeliveredFlow(b *testing.B) {
	key, err := crypto.GenerateKey()
	if err != nil {
		b.Errorf("hex to ecdsa error: %v", err)
		return
	}

	dex, err := newTestDexonWithGenesis(key)
	if err != nil {
		b.Errorf("new test dexon error: %v", err)
	}

	b.ResetTimer()
	for i := 1; i <= b.N; i++ {
		blocksInfo, err := prepareConfirmedBlocks(dex, []*ecdsa.PrivateKey{key}, 100)
		if err != nil {
			b.Errorf("preapare confirmed block error: %v", err)
			return
		}

		dex.app.BlockDelivered(blocksInfo[0].Block.Hash, blocksInfo[0].Block.Position,
			coreTypes.FinalizationResult{
				Timestamp: time.Now(),
				Height:    uint64(i),
			})
	}
}

func newTestDexonWithGenesis(allocKey *ecdsa.PrivateKey) (*Dexon, error) {
	db := ethdb.NewMemDatabase()

	testBankAddress := crypto.PubkeyToAddress(allocKey.PublicKey)
	genesis := core.DefaultTestnetGenesisBlock()
	genesis.Alloc = core.GenesisAlloc{
		testBankAddress: {
			Balance: big.NewInt(100000000000000000),
			Staked:  big.NewInt(50000000000000000),
		},
	}
	chainConfig, _, err := core.SetupGenesisBlock(db, genesis)
	if err != nil {
		return nil, err
	}

	key, err := crypto.GenerateKey()
	if err != nil {
		return nil, err
	}

	config := Config{PrivateKey: key}
	vmConfig := vm.Config{IsBlockProposer: true}

	engine := dexcon.New()

	dex := &Dexon{
		chainDb:     db,
		chainConfig: chainConfig,
		networkID:   config.NetworkId,
		engine:      engine,
	}

	dex.blockchain, err = core.NewBlockChain(db, nil, chainConfig, engine, vmConfig, nil)
	if err != nil {
		return nil, err
	}

	txPoolConfig := core.DefaultTxPoolConfig
	dex.txPool = core.NewTxPool(txPoolConfig, chainConfig, dex.blockchain, true)

	dex.APIBackend = &DexAPIBackend{dex, nil}
	dex.governance = NewDexconGovernance(dex.APIBackend, dex.chainConfig, config.PrivateKey)
	engine.SetConfigFetcher(dex.governance)
	dex.app = NewDexconApp(dex.txPool, dex.blockchain, dex.governance, db, &config)

	return dex, nil
}

// Add tx into tx pool.
func addTx(dex *Dexon, nonce int, signer types.Signer, key *ecdsa.PrivateKey) (
	*types.Transaction, error) {
	tx := types.NewTransaction(
		uint64(nonce),
		common.BytesToAddress([]byte{9}),
		big.NewInt(int64(nonce*1)),
		params.TxGas,
		big.NewInt(1),
		nil)
	tx, err := types.SignTx(tx, signer, key)
	if err != nil {
		return nil, err
	}

	if err := dex.txPool.AddRemote(tx); err != nil {
		return nil, err
	}

	return tx, nil
}

// Prepare data with given transaction number and start nonce.
func prepareData(dex *Dexon, key *ecdsa.PrivateKey, startNonce, txNum int) (
	payload []byte, witness coreTypes.Witness, cost big.Int, nonce uint64, err error) {
	signer := types.NewEIP155Signer(dex.chainConfig.ChainID)
	chainID := new(big.Int).Mod(crypto.PubkeyToAddress(key.PublicKey).Big(),
		big.NewInt(int64(dex.chainConfig.Dexcon.NumChains)))

	for n := startNonce; n < startNonce+txNum; n++ {
		var tx *types.Transaction
		tx, err = addTx(dex, n, signer, key)
		if err != nil {
			return
		}

		cost.Add(&cost, tx.Cost())
		nonce = uint64(n)
	}

	payload, err = dex.app.PreparePayload(coreTypes.Position{ChainID: uint32(chainID.Uint64())})
	if err != nil {
		return
	}

	witness, err = dex.app.PrepareWitness(0)
	if err != nil {
		return
	}

	return
}

func prepareDataWithoutTxPool(dex *Dexon, key *ecdsa.PrivateKey, startNonce, txNum int) (
	payload []byte, witness coreTypes.Witness, err error) {
	signer := types.NewEIP155Signer(dex.chainConfig.ChainID)

	var transactions types.Transactions
	for n := startNonce; n < startNonce+txNum; n++ {
		tx := types.NewTransaction(
			uint64(n),
			common.BytesToAddress([]byte{9}),
			big.NewInt(int64(n*1)),
			params.TxGas,
			big.NewInt(1),
			nil)
		tx, err = types.SignTx(tx, signer, key)
		if err != nil {
			return
		}
		transactions = append(transactions, tx)
	}

	payload, err = rlp.EncodeToBytes(&transactions)
	if err != nil {
		return
	}

	witness, err = dex.app.PrepareWitness(0)
	if err != nil {
		return
	}

	return
}

func prepareConfirmedBlocks(dex *Dexon, keys []*ecdsa.PrivateKey, txNum int) (blocksInfo []struct {
	Block *coreTypes.Block
	Cost  big.Int
	Nonce uint64
}, err error) {
	for _, key := range keys {
		address := crypto.PubkeyToAddress(key.PublicKey)
		chainID := new(big.Int).Mod(address.Big(),
			big.NewInt(int64(dex.chainConfig.Dexcon.NumChains)))

		// Prepare one block for pending.
		var (
			payload []byte
			witness coreTypes.Witness
			cost    big.Int
			nonce   uint64
		)
		startNonce := dex.txPool.State().GetNonce(address)
		payload, witness, cost, nonce, err = prepareData(dex, key, int(startNonce), txNum)
		if err != nil {
			err = fmt.Errorf("prepare data error: %v", err)
			return
		}

		block := coreTypes.NewBlock()
		block.Hash = coreCommon.NewRandomHash()
		block.Witness = witness
		block.Payload = payload
		block.Position.ChainID = uint32(chainID.Uint64())

		status := dex.app.VerifyBlock(block)
		if status != coreTypes.VerifyOK {
			err = fmt.Errorf("verify fail: %v", status)
			return
		}

		dex.app.BlockConfirmed(*block)

		blocksInfo = append(blocksInfo, struct {
			Block *coreTypes.Block
			Cost  big.Int
			Nonce uint64
		}{Block: block, Cost: cost, Nonce: nonce})
	}

	return
}