// +build functional

package test

import (
	"os"
	"testing"

	"github.com/vapor/consensus"
	dbm "github.com/vapor/database/leveldb"
	"github.com/vapor/protocol/bc/types"
	"github.com/vapor/protocol/vm"
)

// case1:           |------c1(height=7)
// --------(height=5)
//                  |------------c2(height=9)
func TestForkCase1(t *testing.T) {
	c1, err := declChain("chain1", nil, 0, 7)
	defer os.RemoveAll("chain1")
	if err != nil {
		t.Fatal(err)
	}

	c2, err := declChain("chain2", c1, 5, 9)
	defer os.RemoveAll("chain2")
	if err != nil {
		t.Fatal(err)
	}

	bestBlockHash := c2.BestBlockHash()
	if err := merge(c1, c2); err != nil {
		t.Fatal(err)
	}

	if *c1.BestBlockHash() != *bestBlockHash || *c2.BestBlockHash() != *bestBlockHash {
		t.Fatalf("test fork case1 failed")
	}

	if !c1.InMainChain(*bestBlockHash) || !c2.InMainChain(*bestBlockHash) {
		t.Fatalf("best block is not in main chain")
	}
}

// case2:            |----c1(height=6)
// ---------(height 5)
//                   |----c2(height=6)
func TestForkCase2(t *testing.T) {
	c1, err := declChain("chain1", nil, 0, 6)
	defer os.RemoveAll("chain1")
	if err != nil {
		t.Fatal(err)
	}

	c2, err := declChain("chain2", c1, 5, 6)
	defer os.RemoveAll("chain2")
	if err != nil {
		t.Fatal(err)
	}

	c1BestBlockHash := c1.BestBlockHash()
	c2BestBlockHash := c2.BestBlockHash()
	if err := merge(c1, c2); err != nil {
		t.Fatal(err)
	}

	if *c1.BestBlockHash() != *c1BestBlockHash || *c2.BestBlockHash() != *c2BestBlockHash {
		t.Fatalf("test fork case2 failed")
	}

	if !c1.InMainChain(*c1BestBlockHash) || !c2.InMainChain(*c2BestBlockHash) {
		t.Fatalf("best block is not in main chain")
	}
}

func TestBlockSync(t *testing.T) {
	c1, err := declChain("chain1", nil, 0, 5)
	defer os.RemoveAll("chain1")
	if err != nil {
		t.Fatal(err)
	}

	c2, err := declChain("chain2", c1, 5, 8)
	defer os.RemoveAll("chain2")
	if err != nil {
		t.Fatal(err)
	}

	bestBlockHash := c2.BestBlockHash()
	if err := merge(c1, c2); err != nil {
		t.Fatal(err)
	}

	if *c1.BestBlockHash() != *bestBlockHash || *c2.BestBlockHash() != *bestBlockHash {
		t.Fatalf("test block sync failed")
	}

	if !c1.InMainChain(*bestBlockHash) || !c2.InMainChain(*bestBlockHash) {
		t.Fatalf("test block sync failed, best block is not in main chain")
	}
}

func TestDoubleSpentInDiffBlock(t *testing.T) {
	chainDB := dbm.NewDB("tx_pool_test", "leveldb", "tx_pool_test")
	defer os.RemoveAll("tx_pool_test")
	chain, _, txPool, err := MockChain(chainDB)
	if err != nil {
		t.Fatal(err)
	}
	if err := AppendBlocks(chain, consensus.ActiveNetParams.CoinbasePendingBlockNumber+1); err != nil {
		t.Fatal(err)
	}

	// create tx spend the coinbase output in block 1
	block, err := chain.GetBlockByHeight(1)
	if err != nil {
		t.Fatal(err)
	}
	tx, err := CreateTxFromTx(block.Transactions[0], 0, 10000, []byte{byte(vm.OP_TRUE)})
	if err != nil {
		t.Fatal(err)
	}

	newBlock, err := NewBlock(chain, []*types.Tx{tx}, []byte{byte(vm.OP_TRUE)})
	_, err = chain.ProcessBlock(newBlock)
	if err != nil {
		t.Fatal(err)
	}

	// create a double spent tx in another block
	tx, err = CreateTxFromTx(block.Transactions[0], 0, 10000, []byte{byte(vm.OP_TRUE)})
	if err != nil {
		t.Fatal(err)
	}

	if isOrphan, err := chain.ValidateTx(tx); isOrphan == false && err == nil {
		t.Fatal("validate double spent tx success")
	}
	if txPool.HaveTransaction(&tx.ID) {
		t.Fatalf("tx pool have double spent tx")
	}
}

func TestDoubleSpentInSameBlock(t *testing.T) {
	chainDB := dbm.NewDB("tx_pool_test", "leveldb", "tx_pool_test")
	defer os.RemoveAll("tx_pool_test")
	chain, _, txPool, err := MockChain(chainDB)
	if err != nil {
		t.Fatal(err)
	}
	if err := AppendBlocks(chain, consensus.ActiveNetParams.CoinbasePendingBlockNumber+1); err != nil {
		t.Fatal(err)
	}

	// create tx spend the coinbase output in block 1
	block, err := chain.GetBlockByHeight(1)
	if err != nil {
		t.Fatal(err)
	}
	tx1, err := CreateTxFromTx(block.Transactions[0], 0, 10000, []byte{byte(vm.OP_TRUE)})
	if err != nil {
		t.Fatal(err)
	}

	// create tx spend the coinbase output in block 1
	tx2, err := CreateTxFromTx(block.Transactions[0], 0, 10000, []byte{byte(vm.OP_TRUE)})
	if err != nil {
		t.Fatal(err)
	}

	_, err = chain.ValidateTx(tx1)
	if err != nil {
		t.Fatal(err)
	}
	_, err = chain.ValidateTx(tx2)
	if err != nil {
		t.Fatal(err)
	}

	if !txPool.HaveTransaction(&tx1.ID) {
		t.Fatalf("can't find tx in tx pool")
	}
	if !txPool.HaveTransaction(&tx2.ID) {
		t.Fatalf("can't find tx in tx pool")
	}

	block, err = NewBlock(chain, []*types.Tx{tx1, tx2}, []byte{byte(vm.OP_TRUE)})
	if err != nil {
		t.Fatal(err)
	}

	if _, err := chain.ProcessBlock(block); err == nil {
		t.Fatalf("process double spent tx success")
	}
}

func TestTxPoolDependencyTx(t *testing.T) {
	chainDB := dbm.NewDB("tx_pool_test", "leveldb", "tx_pool_test")
	defer os.RemoveAll("tx_pool_test")
	chain, _, txPool, err := MockChain(chainDB)
	if err != nil {
		t.Fatal(err)
	}
	if err := AppendBlocks(chain, consensus.ActiveNetParams.CoinbasePendingBlockNumber+1); err != nil {
		t.Fatal(err)
	}

	block, err := chain.GetBlockByHeight(1)
	if err != nil {
		t.Fatal(err)
	}

	tx, err := CreateTxFromTx(block.Transactions[0], 0, 5000000000, []byte{byte(vm.OP_TRUE)})
	if err != nil {
		t.Fatal(err)
	}

	outputAmount := uint64(5000000000)
	txs := []*types.Tx{nil}
	txs[0] = tx
	for i := 1; i < 10; i++ {
		outputAmount -= 50000000
		tx, err := CreateTxFromTx(txs[i-1], 0, outputAmount, []byte{byte(vm.OP_TRUE)})
		if err != nil {
			t.Fatal(err)
		}
		txs = append(txs, tx)
	}

	// validate tx and put it into tx pool
	for _, tx := range txs {
		if _, err := chain.ValidateTx(tx); err != nil {
			t.Fatal(err)
		}
		if !txPool.HaveTransaction(&tx.ID) {
			t.Fatal("can't find tx in txpool")
		}
	}

	block, err = NewBlock(chain, txs, []byte{byte(vm.OP_TRUE)})
	if err != nil {
		t.Fatal(err)
	}

	if _, err := chain.ProcessBlock(block); err != nil {
		t.Fatal("process dependency tx failed")
	}
}

func TestAddInvalidTxToTxPool(t *testing.T) {
	chainDB := dbm.NewDB("tx_pool_test", "leveldb", "tx_pool_test")
	defer os.RemoveAll("tx_pool_test")

	chain, _, txPool, err := MockChain(chainDB)
	if err != nil {
		t.Fatal(err)
	}

	if err := AppendBlocks(chain, consensus.ActiveNetParams.CoinbasePendingBlockNumber+1); err != nil {
		t.Fatal(err)
	}

	block, err := chain.GetBlockByHeight(1)
	if err != nil {
		t.Fatal(err)
	}

	//invalid tx, output amount greater than input
	tx, err := CreateTxFromTx(block.Transactions[0], 0, 60000000000, []byte{byte(vm.OP_TRUE)})
	if err != nil {
		t.Fatal(err)
	}

	if _, err := chain.ValidateTx(tx); err == nil {
		t.Fatalf("add invalid tx to txpool success")
	}

	if txPool.IsTransactionInPool(&tx.ID) {
		t.Fatalf("add invalid tx to txpool success")
	}

	if !txPool.IsTransactionInErrCache(&tx.ID) {
		t.Fatalf("can't find invalid tx in txpool err cache")
	}
}
