package test

import (
	"fmt"
	"io/ioutil"
	"os"
	"testing"
	"time"

	"github.com/vapor/account"
	"github.com/vapor/blockchain/pseudohsm"
	"github.com/vapor/blockchain/signers"
	"github.com/vapor/blockchain/txbuilder"
	"github.com/vapor/config"
	"github.com/vapor/consensus"
	"github.com/vapor/crypto/ed25519/chainkd"
	"github.com/vapor/database"
	dbm "github.com/vapor/database/leveldb"
	"github.com/vapor/database/storage"
	"github.com/vapor/event"
	"github.com/vapor/proposal"
	"github.com/vapor/protocol"
	"github.com/vapor/protocol/bc"
	"github.com/vapor/protocol/bc/types"
	"github.com/vapor/protocol/state"
)

func BenchmarkChain_CoinBaseTx_NoAsset(b *testing.B) {
	benchInsertChain(b, 0, 0, "")
}

func BenchmarkChain_BtmTx_NoAsset_BASE(b *testing.B) {
	benchInsertChain(b, 1, 0, "")
}

func BenchmarkChain_5000BtmTx_NoAsset_BASE(b *testing.B) {
	benchInsertChain(b, 5000, 0, "")
}

func BenchmarkChain_5000BtmTx_1Asset_BASE(b *testing.B) {
	benchInsertChain(b, 5000, 1, "")
}

// standard Transaction
func BenchmarkChain_BtmTx_NoAsset_P2PKH(b *testing.B) {
	benchInsertChain(b, 1000, 0, "P2PKH")
}

func BenchmarkChain_BtmTx_1Asset_P2PKH(b *testing.B) {
	benchInsertChain(b, 1000, 1, "P2PKH")
}

func BenchmarkChain_BtmTx_NoAsset_P2SH(b *testing.B) {
	benchInsertChain(b, 100, 0, "P2SH")
}

func BenchmarkChain_BtmTx_1Asset_P2SH(b *testing.B) {
	benchInsertChain(b, 100, 1, "P2SH")
}

func BenchmarkChain_BtmTx_NoAsset_MultiSign(b *testing.B) {
	benchInsertChain(b, 100, 0, "MultiSign")
}

func BenchmarkChain_BtmTx_1Asset_MultiSign(b *testing.B) {
	benchInsertChain(b, 100, 1, "MultiSign")
}

func benchInsertChain(b *testing.B, blockTxNumber int, otherAssetNum int, txType string) {
	b.StopTimer()
	testNumber := b.N
	totalTxNumber := testNumber * blockTxNumber

	dirPath, err := ioutil.TempDir(".", "testDB")
	if err != nil {
		b.Fatal("create dirPath err:", err)
	}
	defer os.RemoveAll(dirPath)

	testDB := dbm.NewDB("testdb", "leveldb", dirPath)
	defer testDB.Close()

	// Generate a chain test data.
	chain, txs, txPool, err := GenerateChainData(dirPath, testDB, totalTxNumber, otherAssetNum, txType)
	if err != nil {
		b.Fatal("GenerateChainData err:", err)
	}

	b.ReportAllocs()
	b.StartTimer()

	for i := 0; i < b.N; i++ {
		testTxs := txs[blockTxNumber*i : blockTxNumber*(i+1)]
		if err := InsertChain(chain, txPool, testTxs); err != nil {
			b.Fatal("Failed to insert block into chain:", err)
		}
	}
}

func GenerateChainData(dirPath string, testDB dbm.DB, txNumber, otherAssetNum int, txType string) (*protocol.Chain, []*types.Tx, *protocol.TxPool, error) {
	var err error

	// generate transactions
	txs := []*types.Tx{}
	switch txType {
	case "P2PKH":
		txs, err = MockTxsP2PKH(dirPath, testDB, txNumber, otherAssetNum)
		if err != nil {
			return nil, nil, nil, err
		}
	case "P2SH":
		txs, err = MockTxsP2SH(dirPath, testDB, txNumber, otherAssetNum)
		if err != nil {
			return nil, nil, nil, err
		}
	case "MultiSign":
		txs, err = MockTxsMultiSign(dirPath, testDB, txNumber, otherAssetNum)
		if err != nil {
			return nil, nil, nil, err
		}
	default:
		txs, err = CreateTxbyNum(txNumber, otherAssetNum)
		if err != nil {
			return nil, nil, nil, err
		}
	}

	// init UtxoViewpoint
	utxoView := state.NewUtxoViewpoint()
	utxoEntry := storage.NewUtxoEntry(storage.NormalUTXOType, 1, false)
	for _, tx := range txs {
		for _, id := range tx.SpentOutputIDs {
			utxoView.Entries[id] = utxoEntry
		}
	}

	if err := SetUtxoView(testDB, utxoView); err != nil {
		return nil, nil, nil, err
	}

	config.CommonConfig = config.DefaultConfig()
	store := database.NewStore(testDB)
	dispatcher := event.NewDispatcher()
	txPool := protocol.NewTxPool(store, dispatcher)
	chain, err := protocol.NewChain(store, txPool, dispatcher)
	if err != nil {
		return nil, nil, nil, err
	}

	go processNewTxch(txPool)

	return chain, txs, txPool, nil
}

func InsertChain(chain *protocol.Chain, txPool *protocol.TxPool, txs []*types.Tx) error {
	for _, tx := range txs {
		if err := txbuilder.FinalizeTx(nil, chain, tx); err != nil {
			return err
		}
	}

	block, err := proposal.NewBlockTemplate(chain, txPool, nil, uint64(time.Now().UnixNano()/1e6))
	if err != nil {
		return err
	}

	blockSize, err := block.MarshalText()
	if err != nil {
		return err
	}

	fmt.Println("blocksize:", uint64(len(blockSize)))
	fmt.Println("block tx count:", uint64(len(block.Transactions)))
	fmt.Println("coinbase txsize:", uint64(block.Transactions[0].SerializedSize))
	if len(block.Transactions) > 1 {
		fmt.Println("txsize:", uint64(block.Transactions[1].SerializedSize))
	}

	if _, err := chain.ProcessBlock(block); err != nil {
		return err
	}

	return nil
}

func processNewTxch(txPool *protocol.TxPool) {
}

func MockSimpleUtxo(index uint64, assetID *bc.AssetID, amount uint64, ctrlProg *account.CtrlProgram) *account.UTXO {
	if ctrlProg == nil {
		ctrlProg = &account.CtrlProgram{
			AccountID:      "",
			Address:        "",
			KeyIndex:       uint64(0),
			ControlProgram: []byte{81},
			Change:         false,
		}
	}

	utxo := &account.UTXO{
		OutputID:            bc.Hash{V0: 1},
		SourceID:            bc.Hash{V0: 1},
		AssetID:             *assetID,
		Amount:              amount,
		SourcePos:           index,
		ControlProgram:      ctrlProg.ControlProgram,
		ControlProgramIndex: ctrlProg.KeyIndex,
		AccountID:           ctrlProg.AccountID,
		Address:             ctrlProg.Address,
		ValidHeight:         0,
	}

	return utxo
}

func GenerateBaseUtxos(num int, amount uint64, ctrlProg *account.CtrlProgram) []*account.UTXO {
	utxos := []*account.UTXO{}
	for i := 0; i < num; i++ {
		utxo := MockSimpleUtxo(uint64(i), consensus.BTMAssetID, amount, ctrlProg)
		utxos = append(utxos, utxo)
	}

	return utxos
}

func GenerateOtherUtxos(typeCount, num int, amount uint64, ctrlProg *account.CtrlProgram) []*account.UTXO {
	utxos := []*account.UTXO{}

	assetID := &bc.AssetID{
		V0: uint64(typeCount),
		V1: uint64(1),
		V2: uint64(0),
		V3: uint64(1),
	}

	for i := 0; i < num; i++ {
		utxo := MockSimpleUtxo(uint64(typeCount*num+i), assetID, amount, ctrlProg)
		utxos = append(utxos, utxo)
	}

	return utxos
}

func AddTxInputFromUtxo(utxo *account.UTXO, singer *signers.Signer) (*types.TxInput, *txbuilder.SigningInstruction, error) {
	txInput, signInst, err := account.UtxoToInputs(singer, utxo)
	if err != nil {
		return nil, nil, err
	}

	return txInput, signInst, nil
}

func AddTxOutput(assetID bc.AssetID, amount uint64, controlProgram []byte) *types.TxOutput {
	out := types.NewIntraChainOutput(assetID, amount, controlProgram)
	return out
}

func CreateTxBuilder(baseUtxo *account.UTXO, btmServiceFlag bool, signer *signers.Signer) (*txbuilder.TemplateBuilder, error) {
	tplBuilder := txbuilder.NewBuilder(time.Now())

	// add input
	txInput, signInst, err := AddTxInputFromUtxo(baseUtxo, signer)
	if err != nil {
		return nil, err
	}
	tplBuilder.AddInput(txInput, signInst)

	// if the btm is the service charge, didn't need to add the output
	if btmServiceFlag {
		txOutput := AddTxOutput(baseUtxo.AssetID, 100, baseUtxo.ControlProgram)
		tplBuilder.AddOutput(txOutput)
	}

	return tplBuilder, nil
}

func AddTxBuilder(tplBuilder *txbuilder.TemplateBuilder, utxo *account.UTXO, signer *signers.Signer) error {
	txInput, signInst, err := AddTxInputFromUtxo(utxo, signer)
	if err != nil {
		return err
	}
	tplBuilder.AddInput(txInput, signInst)

	txOutput := AddTxOutput(utxo.AssetID, utxo.Amount, utxo.ControlProgram)
	tplBuilder.AddOutput(txOutput)

	return nil
}

func BuildTx(baseUtxo *account.UTXO, otherUtxos []*account.UTXO, signer *signers.Signer) (*txbuilder.Template, error) {
	btmServiceFlag := false
	if otherUtxos == nil || len(otherUtxos) == 0 {
		btmServiceFlag = true
	}

	tplBuilder, err := CreateTxBuilder(baseUtxo, btmServiceFlag, signer)
	if err != nil {
		return nil, err
	}

	for _, u := range otherUtxos {
		if err := AddTxBuilder(tplBuilder, u, signer); err != nil {
			return nil, err
		}
	}

	tpl, _, err := tplBuilder.Build()
	if err != nil {
		return nil, err
	}

	return tpl, nil
}

func GenetrateTxbyUtxo(baseUtxo []*account.UTXO, otherUtxo [][]*account.UTXO) ([]*types.Tx, error) {
	tmpUtxo := []*account.UTXO{}
	txs := []*types.Tx{}
	otherUtxoFlag := true

	if len(otherUtxo) == 0 || len(otherUtxo) != len(baseUtxo) {
		otherUtxoFlag = false
	}

	for i := 0; i < len(baseUtxo); i++ {
		if otherUtxoFlag {
			tmpUtxo = otherUtxo[i]
		} else {
			tmpUtxo = nil
		}

		tpl, err := BuildTx(baseUtxo[i], tmpUtxo, nil)
		if err != nil {
			return nil, err
		}

		txs = append(txs, tpl.Transaction)
	}

	return txs, nil
}

func CreateTxbyNum(txNumber, otherAssetNum int) ([]*types.Tx, error) {
	baseUtxos := GenerateBaseUtxos(txNumber, 1000000000, nil)
	otherUtxos := make([][]*account.UTXO, 0, txNumber)
	if otherAssetNum != 0 {
		for i := 0; i < txNumber; i++ {
			utxos := GenerateOtherUtxos(i, otherAssetNum, 6000, nil)
			otherUtxos = append(otherUtxos, utxos)
		}
	}

	txs, err := GenetrateTxbyUtxo(baseUtxos, otherUtxos)
	if err != nil {
		return nil, err
	}

	return txs, nil
}

func SetUtxoView(db dbm.DB, view *state.UtxoViewpoint) error {
	batch := db.NewBatch()
	if err := database.SaveUtxoView(batch, view); err != nil {
		return err
	}
	batch.Write()
	return nil
}

//-------------------------Mock actual transaction----------------------------------
func MockTxsP2PKH(keyDirPath string, testDB dbm.DB, txNumber, otherAssetNum int) ([]*types.Tx, error) {
	accountStore := database.NewAccountStore(testDB)
	accountManager := account.NewManager(accountStore, nil)
	hsm, err := pseudohsm.New(keyDirPath)
	if err != nil {
		return nil, err
	}

	xpub, _, err := hsm.XCreate("TestP2PKH", "password", "en")
	if err != nil {
		return nil, err
	}

	txs := []*types.Tx{}
	for i := 0; i < txNumber; i++ {
		testAccountAlias := fmt.Sprintf("testAccount%d", i)
		testAccount, err := accountManager.Create([]chainkd.XPub{xpub.XPub}, 1, testAccountAlias, signers.BIP0044)
		if err != nil {
			return nil, err
		}

		controlProg, err := accountManager.CreateAddress(testAccount.ID, false)
		if err != nil {
			return nil, err
		}

		utxo := MockSimpleUtxo(0, consensus.BTMAssetID, 1000000000, controlProg)
		otherUtxos := GenerateOtherUtxos(i, otherAssetNum, 6000, controlProg)
		tpl, err := BuildTx(utxo, otherUtxos, testAccount.Signer)
		if err != nil {
			return nil, err
		}

		if _, err := MockSign(tpl, hsm, "password"); err != nil {
			return nil, err
		}

		txs = append(txs, tpl.Transaction)
	}

	return txs, nil
}

func MockTxsP2SH(keyDirPath string, testDB dbm.DB, txNumber, otherAssetNum int) ([]*types.Tx, error) {
	accountStore := database.NewAccountStore(testDB)
	accountManager := account.NewManager(accountStore, nil)
	hsm, err := pseudohsm.New(keyDirPath)
	if err != nil {
		return nil, err
	}

	xpub1, _, err := hsm.XCreate("TestP2SH1", "password", "en")
	if err != nil {
		return nil, err
	}

	xpub2, _, err := hsm.XCreate("TestP2SH2", "password", "en")
	if err != nil {
		return nil, err
	}

	txs := []*types.Tx{}
	for i := 0; i < txNumber; i++ {
		testAccountAlias := fmt.Sprintf("testAccount%d", i)
		testAccount, err := accountManager.Create([]chainkd.XPub{xpub1.XPub, xpub2.XPub}, 2, testAccountAlias, signers.BIP0044)
		if err != nil {
			return nil, err
		}

		controlProg, err := accountManager.CreateAddress(testAccount.ID, false)
		if err != nil {
			return nil, err
		}

		utxo := MockSimpleUtxo(0, consensus.BTMAssetID, 1000000000, controlProg)
		otherUtxos := GenerateOtherUtxos(i, otherAssetNum, 6000, controlProg)
		tpl, err := BuildTx(utxo, otherUtxos, testAccount.Signer)
		if err != nil {
			return nil, err
		}

		if _, err := MockSign(tpl, hsm, "password"); err != nil {
			return nil, err
		}

		txs = append(txs, tpl.Transaction)
	}

	return txs, nil
}

func MockTxsMultiSign(keyDirPath string, testDB dbm.DB, txNumber, otherAssetNum int) ([]*types.Tx, error) {
	accountStore := database.NewAccountStore(testDB)
	accountManager := account.NewManager(accountStore, nil)
	hsm, err := pseudohsm.New(keyDirPath)
	if err != nil {
		return nil, err
	}

	xpub1, _, err := hsm.XCreate("TestMultilNodeSign1", "password1", "en")
	if err != nil {
		return nil, err
	}

	xpub2, _, err := hsm.XCreate("TestMultilNodeSign2", "password2", "en")
	if err != nil {
		return nil, err
	}
	txs := []*types.Tx{}
	for i := 0; i < txNumber; i++ {
		testAccountAlias := fmt.Sprintf("testAccount%d", i)
		testAccount, err := accountManager.Create([]chainkd.XPub{xpub1.XPub, xpub2.XPub}, 2, testAccountAlias, signers.BIP0044)
		if err != nil {
			return nil, err
		}

		controlProg, err := accountManager.CreateAddress(testAccount.ID, false)
		if err != nil {
			return nil, err
		}

		utxo := MockSimpleUtxo(0, consensus.BTMAssetID, 1000000000, controlProg)
		otherUtxos := GenerateOtherUtxos(i, otherAssetNum, 6000, controlProg)
		tpl, err := BuildTx(utxo, otherUtxos, testAccount.Signer)
		if err != nil {
			return nil, err
		}

		if _, err := MockSign(tpl, hsm, "password1"); err != nil {
			return nil, err
		}

		if _, err := MockSign(tpl, hsm, "password2"); err != nil {
			return nil, err
		}

		txs = append(txs, tpl.Transaction)
	}

	return txs, nil
}
