package core_test

import (
	"fmt"
	"math/big"
	"path/filepath"
	"testing"

	"github.com/nspcc-dev/neo-go/internal/random"
	"github.com/nspcc-dev/neo-go/pkg/config/netmode"
	"github.com/nspcc-dev/neo-go/pkg/core/native/nativenames"
	"github.com/nspcc-dev/neo-go/pkg/core/state"
	"github.com/nspcc-dev/neo-go/pkg/core/storage"
	"github.com/nspcc-dev/neo-go/pkg/core/storage/dbconfig"
	"github.com/nspcc-dev/neo-go/pkg/core/transaction"
	"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
	"github.com/nspcc-dev/neo-go/pkg/neotest"
	"github.com/nspcc-dev/neo-go/pkg/neotest/chain"
	"github.com/nspcc-dev/neo-go/pkg/smartcontract"
	"github.com/nspcc-dev/neo-go/pkg/wallet"
	"github.com/stretchr/testify/require"
)

func BenchmarkBlockchain_VerifyWitness(t *testing.B) {
	bc, acc := chain.NewSingle(t)
	e := neotest.NewExecutor(t, bc, acc, acc)
	tx := e.NewTx(t, []neotest.Signer{acc}, e.NativeHash(t, nativenames.Gas), "transfer", acc.ScriptHash(), acc.Script(), 1, nil)

	t.ResetTimer()
	for n := 0; n < t.N; n++ {
		_, err := bc.VerifyWitness(tx.Signers[0].Account, tx, &tx.Scripts[0], 100000000)
		require.NoError(t, err)
	}
}

func BenchmarkBlockchain_ForEachNEP17Transfer(t *testing.B) {
	var stores = map[string]func(testing.TB) storage.Store{
		"MemPS": func(t testing.TB) storage.Store {
			return storage.NewMemoryStore()
		},
		"BoltPS":  newBoltStoreForTesting,
		"LevelPS": newLevelDBForTesting,
	}
	startFrom := []int{1, 100, 1000}
	blocksToTake := []int{100, 1000}
	for psName, newPS := range stores {
		for _, startFromBlock := range startFrom {
			for _, nBlocksToTake := range blocksToTake {
				t.Run(fmt.Sprintf("%s_StartFromBlockN-%d_Take%dBlocks", psName, startFromBlock, nBlocksToTake), func(t *testing.B) {
					ps := newPS(t)
					t.Cleanup(func() { ps.Close() })
					benchmarkForEachNEP17Transfer(t, ps, startFromBlock, nBlocksToTake)
				})
			}
		}
	}
}

func BenchmarkNEO_GetGASPerVote(t *testing.B) {
	var stores = map[string]func(testing.TB) storage.Store{
		"MemPS": func(t testing.TB) storage.Store {
			return storage.NewMemoryStore()
		},
		"BoltPS":  newBoltStoreForTesting,
		"LevelPS": newLevelDBForTesting,
	}
	for psName, newPS := range stores {
		for nRewardRecords := 10; nRewardRecords <= 1000; nRewardRecords *= 10 {
			for rewardDistance := 1; rewardDistance <= 1000; rewardDistance *= 10 {
				t.Run(fmt.Sprintf("%s_%dRewardRecords_%dRewardDistance", psName, nRewardRecords, rewardDistance), func(t *testing.B) {
					ps := newPS(t)
					t.Cleanup(func() { ps.Close() })
					benchmarkGasPerVote(t, ps, nRewardRecords, rewardDistance)
				})
			}
		}
	}
}

func benchmarkForEachNEP17Transfer(t *testing.B, ps storage.Store, startFromBlock, nBlocksToTake int) {
	var (
		chainHeight       = 2_100                            // constant chain height to be able to compare paging results
		transfersPerBlock = state.TokenTransferBatchSize/4 + // 4 blocks per batch
			state.TokenTransferBatchSize/32 // shift
	)

	bc, validators, committee := chain.NewMultiWithCustomConfigAndStore(t, nil, ps, true)

	e := neotest.NewExecutor(t, bc, validators, committee)
	gasHash := e.NativeHash(t, nativenames.Gas)

	acc := random.Uint160()
	from := e.Validator.ScriptHash()

	for j := 0; j < chainHeight; j++ {
		b := smartcontract.NewBuilder()
		for i := 0; i < transfersPerBlock; i++ {
			b.InvokeWithAssert(gasHash, "transfer", from, acc, 1, nil)
		}
		script, err := b.Script()
		require.NoError(t, err)
		tx := transaction.New(script, int64(1100_0000*transfersPerBlock))
		tx.NetworkFee = 1_0000_000
		tx.ValidUntilBlock = bc.BlockHeight() + 1
		tx.Nonce = neotest.Nonce()
		tx.Signers = []transaction.Signer{{Account: from, Scopes: transaction.CalledByEntry}}
		require.NoError(t, validators.SignTx(netmode.UnitTestNet, tx))
		e.AddNewBlock(t, tx)
		e.CheckHalt(t, tx.Hash())
	}

	newestB, err := bc.GetBlock(bc.GetHeaderHash(bc.BlockHeight() - uint32(startFromBlock) + 1))
	require.NoError(t, err)
	newestTimestamp := newestB.Timestamp
	oldestB, err := bc.GetBlock(bc.GetHeaderHash(newestB.Index - uint32(nBlocksToTake)))
	require.NoError(t, err)
	oldestTimestamp := oldestB.Timestamp

	t.ResetTimer()
	t.ReportAllocs()
	t.StartTimer()
	for i := 0; i < t.N; i++ {
		require.NoError(t, bc.ForEachNEP17Transfer(acc, newestTimestamp, func(t *state.NEP17Transfer) (bool, error) {
			if t.Timestamp < oldestTimestamp {
				// iterating from newest to oldest, already have reached the needed height
				return false, nil
			}
			return true, nil
		}))
	}
	t.StopTimer()
}

func newLevelDBForTesting(t testing.TB) storage.Store {
	dbPath := t.TempDir()
	dbOptions := dbconfig.LevelDBOptions{
		DataDirectoryPath: dbPath,
	}
	newLevelStore, err := storage.NewLevelDBStore(dbOptions)
	require.Nil(t, err, "NewLevelDBStore error")
	return newLevelStore
}

func newBoltStoreForTesting(t testing.TB) storage.Store {
	d := t.TempDir()
	dbPath := filepath.Join(d, "test_bolt_db")
	boltDBStore, err := storage.NewBoltDBStore(dbconfig.BoltDBOptions{FilePath: dbPath})
	require.NoError(t, err)
	return boltDBStore
}

func benchmarkGasPerVote(t *testing.B, ps storage.Store, nRewardRecords int, rewardDistance int) {
	bc, validators, committee := chain.NewMultiWithCustomConfigAndStore(t, nil, ps, true)
	cfg := bc.GetConfig()

	e := neotest.NewExecutor(t, bc, validators, committee)
	neoHash := e.NativeHash(t, nativenames.Neo)
	gasHash := e.NativeHash(t, nativenames.Gas)
	neoSuperInvoker := e.NewInvoker(neoHash, validators, committee)
	neoValidatorsInvoker := e.ValidatorInvoker(neoHash)
	gasValidatorsInvoker := e.ValidatorInvoker(gasHash)

	// Vote for new committee.
	sz := len(cfg.StandbyCommittee)
	voters := make([]*wallet.Account, sz)
	candidates := make(keys.PublicKeys, sz)
	txs := make([]*transaction.Transaction, 0, len(voters)*3)
	for i := 0; i < sz; i++ {
		priv, err := keys.NewPrivateKey()
		require.NoError(t, err)
		candidates[i] = priv.PublicKey()
		voters[i], err = wallet.NewAccount()
		require.NoError(t, err)
		registerTx := neoSuperInvoker.PrepareInvoke(t, "registerCandidate", candidates[i].Bytes())
		txs = append(txs, registerTx)

		to := voters[i].Contract.ScriptHash()
		transferNeoTx := neoValidatorsInvoker.PrepareInvoke(t, "transfer", e.Validator.ScriptHash(), to, big.NewInt(int64(sz-i)*1000000).Int64(), nil)
		txs = append(txs, transferNeoTx)

		transferGasTx := gasValidatorsInvoker.PrepareInvoke(t, "transfer", e.Validator.ScriptHash(), to, int64(1_000_000_000), nil)
		txs = append(txs, transferGasTx)
	}
	e.AddNewBlock(t, txs...)
	for _, tx := range txs {
		e.CheckHalt(t, tx.Hash())
	}
	voteTxs := make([]*transaction.Transaction, 0, sz)
	for i := 0; i < sz; i++ {
		priv := voters[i].PrivateKey()
		h := priv.GetScriptHash()
		voteTx := e.NewTx(t, []neotest.Signer{neotest.NewSingleSigner(voters[i])}, neoHash, "vote", h, candidates[i].Bytes())
		voteTxs = append(voteTxs, voteTx)
	}
	e.AddNewBlock(t, voteTxs...)
	for _, tx := range voteTxs {
		e.CheckHalt(t, tx.Hash())
	}

	// Collect set of nRewardRecords reward records for each voter.
	e.GenerateNewBlocks(t, len(cfg.StandbyCommittee))

	// Transfer some more NEO to first voter to update his balance height.
	to := voters[0].Contract.ScriptHash()
	neoValidatorsInvoker.Invoke(t, true, "transfer", e.Validator.ScriptHash(), to, int64(1), nil)

	// Advance chain one more time to avoid same start/end rewarding bounds.
	e.GenerateNewBlocks(t, rewardDistance)
	end := bc.BlockHeight()

	t.ResetTimer()
	t.ReportAllocs()
	t.StartTimer()
	for i := 0; i < t.N; i++ {
		_, err := bc.CalculateClaimable(to, end)
		require.NoError(t, err)
	}
	t.StopTimer()
}