package autonity

import (
	"bytes"
	"crypto/ecdsa"
	"errors"
	"fmt"
	"math/big"
	"math/rand"
	"net"
	"sort"
	"testing"

	"github.com/autonity/autonity/autonity/bindings"

	"github.com/stretchr/testify/require"

	"github.com/autonity/autonity/accounts/abi"
	"github.com/autonity/autonity/common"
	"github.com/autonity/autonity/common/math"
	"github.com/autonity/autonity/core/rawdb"
	"github.com/autonity/autonity/core/state"
	"github.com/autonity/autonity/core/types"
	"github.com/autonity/autonity/core/vm"
	"github.com/autonity/autonity/crypto"
	"github.com/autonity/autonity/crypto/blst"
	"github.com/autonity/autonity/p2p/enode"
	"github.com/autonity/autonity/params"
	"github.com/autonity/autonity/params/generated"
)

func BenchmarkComputeCommittee(b *testing.B) {

	validatorCount := 100000
	validators, err := randomValidators(validatorCount, 30)
	require.NoError(b, err)
	contractAbi := &generated.AutonityAbi
	deployer := params.DeployerAddress
	committeeSize := 100

	b.Run("computeCommittee", func(b *testing.B) {
		stateDB, evmContract, contractAddress, err := deployAutonity(committeeSize, validators, deployer)
		require.NoError(b, err)
		var header *types.Header
		_, err = callContractFunction(evmContract, contractAddress, stateDB, header, contractAbi, "finalizeInitialization")
		require.NoError(b, err)
		packedArgs, err := contractAbi.Pack("computeCommittee")
		require.NoError(b, err)
		benchmarkWithGas(b, evmContract, stateDB, header, contractAddress, packedArgs)
	})
}

func TestComputeCommittee(t *testing.T) {

	t.Run("computeCommittee validators < committee", func(t *testing.T) {
		committeeSize := 100
		validatorCount := 10
		testComputeCommittee(committeeSize, validatorCount, t)
	})

	t.Run("computeCommittee validators > committee", func(t *testing.T) {
		committeeSize := 100
		validatorCount := 1000
		testComputeCommittee(committeeSize, validatorCount, t)
	})
}

func TestUpdateEnode(t *testing.T) {

	contractAbi := &generated.AutonityTestAbi
	deployer := params.DeployerAddress
	validators, err := randomValidators(10, 100)
	require.NoError(t, err)

	t.Run("Success: update node ip of enode", func(t *testing.T) {
		stateDB, evmContract, contractAddress, _ := deployAutonity(5, validators, deployer)
		var header *types.Header
		val := validators[0]
		node, _ := enode.ParseV4(val.Enode)
		tempNode := enode.NewV4(node.Pubkey(), net.IP{127, 0, 0, 1}, 1234, 0)
		_, err = callContractFunctionAs(evmContract, contractAddress,
			stateDB, header, contractAbi, val.Treasury,
			"updateEnode", val.NodeAddress, tempNode.String())
		require.NoError(t, err)

		// make sure validator enode has taken new value
		res, err := callContractFunction(evmContract, contractAddress, stateDB, header, contractAbi, "getValidator", val.NodeAddress)
		require.NoError(t, err)
		av, err := contractAbi.Unpack("getValidator", res)
		require.NoError(t, err)
		out := abi.ConvertType(av[0], new(bindings.IAutonityValidator)).(*bindings.IAutonityValidator)
		require.Equal(t, tempNode.String(), out.Enode)
	})

	t.Run("Failure: update to invalid enode format", func(t *testing.T) {
		stateDB, evmContract, contractAddress, _ := deployAutonity(5, validators, deployer)
		var header *types.Header
		val := validators[0]
		testKey, _ := crypto.HexToECDSA("45a915e4d060149eb4365960e6a7a45f334393093061116b197e3240065ff2d8")
		tempNode := enode.NewV4(&testKey.PublicKey, net.IP{127, 0, 0, 1}, 1234, 0)
		res, err := callContractFunctionAs(evmContract, contractAddress,
			stateDB, header, contractAbi, val.Treasury,
			"updateEnode", val.NodeAddress, tempNode.String()+"junk")
		require.Error(t, err)

		revertReason, _ := abi.UnpackRevert(res)
		require.Equal(t, "enode error", revertReason)
	})

	t.Run("Failure: update node address of enode", func(t *testing.T) {
		stateDB, evmContract, contractAddress, _ := deployAutonity(5, validators, deployer)
		var header *types.Header
		val := validators[0]
		testKey, _ := crypto.HexToECDSA("45a915e4d060149eb4365960e6a7a45f334393093061116b197e3240065ff2d8")
		tempNode := enode.NewV4(&testKey.PublicKey, net.IP{127, 0, 0, 1}, 1234, 0)
		res, err := callContractFunctionAs(evmContract, contractAddress,
			stateDB, header, contractAbi, val.Treasury,
			"updateEnode", val.NodeAddress, tempNode.String())
		require.Error(t, err)

		revertReason, _ := abi.UnpackRevert(res)
		require.Equal(t, "validator node address can't be updated", revertReason)
	})

	t.Run("Failure: update ip of enode, unknown msg.sender", func(t *testing.T) {
		stateDB, evmContract, contractAddress, _ := deployAutonity(5, validators, deployer)
		var header *types.Header
		val := validators[0]
		node, _ := enode.ParseV4(val.Enode)
		res, err := callContractFunctionAs(evmContract, contractAddress,
			stateDB, header, contractAbi, validators[1].Treasury,
			"updateEnode", val.NodeAddress, node.String())
		require.Error(t, err)

		revertReason, _ := abi.UnpackRevert(res)
		require.Equal(t, "require caller to be validator treasury account", revertReason)
	})

	t.Run("Failure: update ip of enode, validator not registered", func(t *testing.T) {
		stateDB, evmContract, contractAddress, _ := deployAutonity(5, validators, deployer)
		var header *types.Header
		randVals, _ := randomValidators(1, 100)
		val := randVals[0]
		node, _ := enode.ParseV4(val.Enode)
		res, err := callContractFunctionAs(evmContract, contractAddress,
			stateDB, header, contractAbi, val.Treasury,
			"updateEnode", val.NodeAddress, node.String())
		require.Error(t, err)

		revertReason, _ := abi.UnpackRevert(res)
		require.Equal(t, "validator not registered", revertReason)
	})

	t.Run("Failure: update ip of enode, validator In Committee", func(t *testing.T) {
		// update committee size to 10, so all the validators will be in committeee
		stateDB, evmContract, contractAddress, _ := deployAutonity(10, validators, deployer)
		var header *types.Header
		_, err = callContractFunction(evmContract, contractAddress, stateDB, header, contractAbi, "applyStakingOperations")
		require.NoError(t, err)
		_, err = callContractFunction(evmContract, contractAddress, stateDB, header, contractAbi, "computeCommittee")
		require.NoError(t, err)

		val := validators[0]
		node, _ := enode.ParseV4(val.Enode)
		tempNode := enode.NewV4(node.Pubkey(), net.IP{127, 0, 0, 1}, 1234, 0)
		res, err := callContractFunctionAs(evmContract, contractAddress,
			stateDB, header, contractAbi, val.Treasury,
			"updateEnode", val.NodeAddress, tempNode.String())
		require.Error(t, err)
		revertReason, _ := abi.UnpackRevert(res)
		require.Equal(t, "validator must not be in committee", revertReason)

		// make sure validator enode remains unchanged
		res, err = callContractFunction(evmContract, contractAddress, stateDB, header, contractAbi, "getValidator", val.NodeAddress)
		require.NoError(t, err)
		av, err := contractAbi.Unpack("getValidator", res)
		require.NoError(t, err)
		out := abi.ConvertType(av[0], new(bindings.IAutonityValidator)).(*bindings.IAutonityValidator)
		require.Equal(t, node.String(), out.Enode)
	})
}

func TestGetters(t *testing.T) {
	contractAbi := &generated.AutonityAbi
	deployer := params.DeployerAddress
	validators, err := randomValidators(10, 100)
	require.NoError(t, err)
	var header *types.Header
	db, contract, contractAddress, err := deployAutonity(10, validators, deployer)
	require.NoError(t, err)

	_, err = callContractFunctionAs(contract, contractAddress, db, header, contractAbi, deployer, "finalizeInitialization", common.Big5)
	require.NoError(t, err)

	autonity := &AutonityContract{
		evmContract: evmContract{
			evmProvider: contract.evmProvider,
			contractABI: contractAbi,
			db:          contract.db,
			chainConfig: contract.chainConfig,
		},
	}

	t.Run("Test getEpochByHeight properly unmarshalls", func(t *testing.T) {
		info, err := autonity.CallEpochByHeight(db, header, big.NewInt(0))
		require.NoError(t, err)
		require.NotNil(t, info)

		require.Equal(t, uint64(0), info.EpochBlock.Uint64())
		require.Equal(t, uint64(50), info.NextEpochBlock.Uint64())
		require.Equal(t, uint64(5), info.OmissionDelta.Uint64())
		require.Equal(t, params.TestAutonityContractConfig.MinBaseFee, info.Eip1559.MinBaseFee.Uint64())
		require.Equal(t, uint64(8), info.Eip1559.BaseFeeChangeDenominator.Uint64())
		require.Equal(t, uint64(2), info.Eip1559.ElasticityMultiplier.Uint64())
		require.Equal(t, uint64(1024), info.Eip1559.GasLimitBoundDivisor.Uint64())
		require.Len(t, info.Committee.Members, 10)
	})

}

func deployAutonity(
	committeeSize int, validators []params.Validator, deployer common.Address,
) (*state.StateDB, *evmContract, common.Address, error) {
	abi := &generated.AutonityAbi
	stateDB, evm, evmContract, err := initializeEvm(abi)
	if err != nil {
		return stateDB, evmContract, common.Address{}, err
	}
	contractConfig := autonityTestConfig()
	contractConfig.Protocol.OperatorAccount = common.Address{}
	contractConfig.Protocol.CommitteeSize = big.NewInt(int64(committeeSize))
	args, err := abi.Pack("", validators, contractConfig)
	if err != nil {
		return stateDB, evmContract, common.Address{}, err
	}
	contractAddress, err := deployContract(generated.AutonityTestBytecode, args, deployer, evm)
	return stateDB, evmContract, contractAddress, err
}

func deployAutonityTest(
	committeeSize int, validators []params.Validator, deployer common.Address,
) (*state.StateDB, *evmContract, common.Address, error) {
	abi := &generated.AutonityTestAbi
	stateDB, evm, evmContract, err := initializeEvm(abi)
	if err != nil {
		return stateDB, evmContract, common.Address{}, err
	}
	contractConfig := autonityTestConfig()
	contractConfig.Protocol.OperatorAccount = common.Address{}
	contractConfig.Protocol.CommitteeSize = big.NewInt(int64(committeeSize))
	args, err := abi.Pack("", validators, contractConfig)
	if err != nil {
		return stateDB, evmContract, common.Address{}, err
	}
	contractAddress, err := deployContract(generated.AutonityTestBytecode, args, deployer, evm)
	return stateDB, evmContract, contractAddress, err
}

func initializeEvm(abi *abi.ABI) (*state.StateDB, *vm.EVM, *evmContract, error) {
	ethDb := rawdb.NewMemoryDatabase()
	db := state.NewDatabase(ethDb)
	stateDB, err := state.New(common.Hash{}, db, nil)
	if err != nil {
		return new(state.StateDB), new(vm.EVM), new(evmContract), err
	}
	evm := createTestVM(stateDB)
	evmContract := NewEVMContract(testEVMProvider(), abi, ethDb, params.TestChainConfig)
	return stateDB, evm, evmContract, nil
}

func deployContract(byteCode []byte, args []byte, deployer common.Address, evm *vm.EVM) (common.Address, error) {
	gas := uint64(math.MaxUint64)
	value := common.Big0
	data := append(byteCode, args...)
	_, contractAddress, _, err := evm.Create(vm.AccountRef(deployer), data, gas, value)
	return contractAddress, err
}

// Packs the args and then calls the function and returns result
func callContractFunction(
	evmContract *evmContract, contractAddress common.Address, stateDB *state.StateDB, header *types.Header, abi *abi.ABI,
	methodName string, args ...interface{},
) ([]byte, error) {
	argsPacked, err := abi.Pack(methodName, args...)
	if err != nil {
		return make([]byte, 0), err
	}
	res, _, err := evmContract.callContractFunc(stateDB, header, contractAddress, argsPacked)
	return res, err
}

// Packs the args and then calls the function and returns result
func callContractFunctionAs(
	evmContract *evmContract, contractAddress common.Address, stateDB *state.StateDB, header *types.Header, abi *abi.ABI,
	origin common.Address, methodName string, args ...interface{}, //nolint:unparam
) ([]byte, error) {
	argsPacked, err := abi.Pack(methodName, args...)
	if err != nil {
		return make([]byte, 0), err
	}
	return evmContract.callContractFuncAs(stateDB, header, contractAddress, origin, argsPacked)
}

func randomValidators(count int, randomPercentage int) ([]params.Validator, error) {
	if count == 0 {
		return []params.Validator{}, nil
	}

	bondedStake := make([]int64, count)
	for i := 0; i < count; i++ {
		bondedStake[i] = int64(rand.Uint64() >> 1)
	}
	if randomPercentage < 100 {
		sort.SliceStable(bondedStake, func(i, j int) bool {
			return bondedStake[i] > bondedStake[j]
		})
		if count > 1 && bondedStake[0] < bondedStake[1] {
			return []params.Validator{}, errors.New("Not sorted")
		}
	}

	validatorList := make([]params.Validator, count)
	// ecdsaSecretKeyList := make([]*ecdsa.PrivateKey, count)
	// blsSecretKeyList := make([]*blst.SecretKey, count)
	for i := 0; i < count; i++ {
		var privateKey *ecdsa.PrivateKey
		var secretKey blst.SecretKey
		var err error
		for {
			privateKey, err = crypto.GenerateKey()
			if err == nil {
				break
			}
		}
		for {
			secretKey, err = blst.RandKey()
			if err == nil {
				break
			}
		}
		// ecdsaSecretKeyList[i] = privateKey
		// blsSecretKeyList[i] = &secretKey
		consensusKey := secretKey.PublicKey()
		publicKey := privateKey.PublicKey
		enode := "enode://" + string(crypto.PubECDSAToHex(&publicKey)[2:]) + "@3.209.45.79:30303"
		address := crypto.PubkeyToAddress(publicKey)
		validatorList[i] = params.Validator{
			Treasury:      address,
			Enode:         enode,
			BondedStake:   big.NewInt(bondedStake[i]),
			OracleAddress: address,
			ConsensusKey:  consensusKey.Marshal(),
		}
		err = validatorList[i].Validate()
		if err != nil {
			return []params.Validator{}, err
		}
	}

	if randomPercentage == 0 || randomPercentage == 100 {
		return validatorList, nil
	}

	randomValidatorCount := count * randomPercentage / 100
	randomIndex := make(map[uint32]bool)
	randomIndex[0] = true
	for i := 0; i < randomValidatorCount; i++ {
		var idx uint32
		for {
			idx = rand.Uint32() % uint32(count)
			_, ok := randomIndex[idx]
			if !ok {
				break
			}
		}

		stake := validatorList[idx-1].BondedStake
		validatorList[idx].BondedStake = new(big.Int).Add(stake, big.NewInt(int64(rand.Uint64()>>1)))
		randomIndex[idx] = true
	}
	return validatorList, nil
}

func autonityTestConfig() bindings.IAutonityConfig {
	// shallow copy is enough for this modifications
	var testConfigCopy params.AutonityContractGenesis = *params.TestAutonityContractConfig
	testConfigCopy.WithheldRewardsPool = params.TestAutonityContractConfig.Operator
	testConfigCopy.Treasury = params.TestAutonityContractConfig.Operator
	return toContractConfig(&testConfigCopy)
}

func createTestVM(state vm.StateDB) *vm.EVM {
	vmBlockContext := vm.BlockContext{
		// some calls fail when time is nil
		Time:        common.Big1,
		Transfer:    func(vm.StateDB, common.Address, common.Address, *big.Int) {},
		CanTransfer: func(vm.StateDB, common.Address, *big.Int) bool { return true },
		BlockNumber: common.Big0,
	}
	txContext := vm.TxContext{
		Origin:   common.Address{},
		GasPrice: common.Big0,
	}
	evm := vm.NewEVM(vmBlockContext, txContext, state, params.TestChainConfig, vm.Config{})
	return evm
}

func testEVMProvider() func(header *types.Header, origin common.Address, stateDB vm.StateDB) *vm.EVM {
	return func(header *types.Header, origin common.Address, stateDB vm.StateDB) *vm.EVM {
		vmBlockContext := vm.BlockContext{
			Transfer:    func(vm.StateDB, common.Address, common.Address, *big.Int) {},
			CanTransfer: func(vm.StateDB, common.Address, *big.Int) bool { return true },
			BlockNumber: common.Big0,
			// some calls fail when time is nil
			Time: common.Big1,
		}
		txContext := vm.TxContext{
			Origin:   common.Address{},
			GasPrice: common.Big0,
		}
		evm := vm.NewEVM(vmBlockContext, txContext, stateDB, params.TestChainConfig, vm.Config{})
		return evm
	}
}

// to properly benchmark a contract call, it is expected that the state is same everytime the contract function is run
func benchmarkWithGas(
	b *testing.B, evmContract *evmContract, stateDB vm.StateDB, header *types.Header,
	contractAddress common.Address, packedArgs []byte,
) {
	var totalGasUsed uint64

	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		_, usedGas, err := evmContract.callContractFunc(stateDB, header, contractAddress, packedArgs)
		require.NoError(b, err)
		totalGasUsed += usedGas
	}
	b.Log(1.0 * totalGasUsed / uint64(b.N))
}

func isSorted(voters []common.Address, reporters []common.Address, treasuries []common.Address, committeeMembers []types.CommitteeMember, validators []params.Validator, enodes []string, totalStake *big.Int) error {
	if len(voters) != len(treasuries) {
		return fmt.Errorf("mismatch between addresses length")
	}
	if len(voters) != len(reporters) {
		return fmt.Errorf("mismatch between addresses length")
	}
	if len(voters) > len(validators) {
		return fmt.Errorf("more voters than validators")
	}
	if len(voters) != len(committeeMembers) {
		return fmt.Errorf("committee size not equal to voter size")
	}
	if len(enodes) != len(committeeMembers) {
		return fmt.Errorf("not enough enodes")
	}
	positions := make(map[common.Address]int)
	for i, validator := range validators {
		if _, ok := positions[*validator.NodeAddress]; ok {
			return fmt.Errorf("duplicate validator")
		}
		positions[*validator.NodeAddress] = i
	}
	lastStake := big.NewInt(0)
	totalStakeCalculated := big.NewInt(0)
	for i, member := range committeeMembers {
		idx, ok := positions[member.Address]
		if !ok {
			return fmt.Errorf("committee member not found")
		}
		if i > 0 && lastStake.Cmp(validators[idx].BondedStake) < 0 {
			return fmt.Errorf("not sorted")
		}
		lastStake = validators[idx].BondedStake
		totalStakeCalculated = totalStakeCalculated.Add(totalStakeCalculated, lastStake)

		if !bytes.Equal(member.Address.Bytes(), validators[idx].NodeAddress.Bytes()) {
			return fmt.Errorf("committee member address mismatch")
		}
		if member.VotingPower.Cmp(validators[idx].BondedStake) != 0 {
			return fmt.Errorf("committee member stake mismatch")
		}
		if !bytes.Equal(member.ConsensusKeyBytes, validators[idx].ConsensusKey) {
			return fmt.Errorf("committee member consensus key mismatch")
		}
		if enodes[i] != validators[idx].Enode {
			return fmt.Errorf("committee member enode mismatch")
		}
		if voters[i] != validators[idx].OracleAddress {
			return fmt.Errorf("oracle address mismatch")
		}
		if treasuries[i] != validators[idx].Treasury {
			return fmt.Errorf("treasury account mismatch")
		}
		if reporters[i] != *validators[idx].NodeAddress {
			return fmt.Errorf("reporter address mismatch")
		}
		if *validators[idx].State != uint8(0) {
			return fmt.Errorf("committee member not active")
		}
		delete(positions, member.Address)
	}
	nonVoter := 0
	for _, validator := range validators {
		if _, ok := positions[*validator.NodeAddress]; ok {
			nonVoter++
			if validator.BondedStake.Cmp(lastStake) == 1 {
				return fmt.Errorf("non voter has more stake than voter")
			}
		}
	}
	if nonVoter+len(voters) != len(validators) {
		return fmt.Errorf("not all are accounted")
	}
	if totalStakeCalculated.Cmp(totalStake) != 0 {
		return fmt.Errorf("epochTotalStake mismatch")
	}
	return nil
}

func testComputeCommittee(committeeSize int, validatorCount int, t *testing.T) {
	contractAbi := &generated.AutonityTestAbi
	deployer := params.DeployerAddress

	validators, err := randomValidators(validatorCount, 30)
	require.NoError(t, err)

	stateDB, evmContract, contractAddress, err := deployAutonityTest(committeeSize, validators, deployer)
	require.NoError(t, err)
	var header *types.Header
	_, err = callContractFunction(evmContract, contractAddress, stateDB, header, contractAbi, "applyStakingOperations")
	require.NoError(t, err)

	res, err := callContractFunction(evmContract, contractAddress, stateDB, header, contractAbi, "computeCommittee")
	require.NoError(t, err)

	addresses, err := contractAbi.Unpack("computeCommittee", res)
	require.NoError(t, err)
	require.Equal(t, 3, len(addresses))
	voters := addresses[0].([]common.Address)
	reporters := addresses[1].([]common.Address)
	treasuries := addresses[2].([]common.Address)

	res, err = callContractFunction(evmContract, contractAddress, stateDB, header, contractAbi, "getCommittee")
	require.NoError(t, err)
	members := make([]types.CommitteeMember, committeeSize)
	err = contractAbi.UnpackIntoInterface(&members, "getCommittee", res)
	require.NoError(t, err)

	res, err = callContractFunction(evmContract, contractAddress, stateDB, header, contractAbi, "getMaxCommitteeSize")
	require.NoError(t, err)
	var maxCommitteeSize *big.Int
	err = contractAbi.UnpackIntoInterface(&maxCommitteeSize, "getMaxCommitteeSize", res)
	require.NoError(t, err)

	require.Equal(t, true, maxCommitteeSize.Cmp(big.NewInt(int64(len(members)))) >= 0, "committee size exceeds MaxCommitteeSize")

	res, err = callContractFunction(evmContract, contractAddress, stateDB, header, contractAbi, "getCommitteeEnodes")
	require.NoError(t, err)
	enodes := make([]string, committeeSize)
	err = contractAbi.UnpackIntoInterface(&enodes, "getCommitteeEnodes", res)
	require.NoError(t, err)
	res, err = callContractFunction(evmContract, contractAddress, stateDB, header, contractAbi, "getEpochTotalBondedStake")
	require.NoError(t, err)
	totalStake := big.NewInt(0)
	err = contractAbi.UnpackIntoInterface(&totalStake, "getEpochTotalBondedStake", res)
	require.NoError(t, err)

	err = isSorted(voters, reporters, treasuries, members, validators, enodes, totalStake)
	require.NoError(t, err)
}
