package tests

import (
	"bytes"
	"crypto/ecdsa"
	"encoding/json"
	"fmt"
	"math/big"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

	"github.com/autonity/autonity/common/math"
	"github.com/autonity/autonity/consensus/tendermint/core/message"
	"github.com/autonity/autonity/core"
	"github.com/autonity/autonity/core/types"
	"github.com/autonity/autonity/crypto/blst"
	"github.com/autonity/autonity/params/generated"

	"github.com/autonity/autonity/accounts/abi"
	"github.com/autonity/autonity/accounts/abi/bind"
	"github.com/autonity/autonity/autonity"
	"github.com/autonity/autonity/common"
	"github.com/autonity/autonity/consensus/tendermint/accountability"
	"github.com/autonity/autonity/core/rawdb"
	"github.com/autonity/autonity/core/state"
	"github.com/autonity/autonity/core/vm"
	"github.com/autonity/autonity/crypto"
	"github.com/autonity/autonity/eth/tracers"
	"github.com/autonity/autonity/params"
	"github.com/autonity/autonity/rlp"

	_ "github.com/autonity/autonity/eth/tracers/native" //nolint
)

var (
	FromAutonity = &runOptions{origin: params.AutonityContractAddress}
	User         = common.HexToAddress("0x99")
)

type runOptions struct {
	origin common.Address
	value  *big.Int
}

type contract struct {
	address common.Address
	abi     *abi.ABI
	r       *Runner
}

func (c *contract) Address() common.Address {
	return c.address
}

func (c *contract) call(opts *runOptions, method string, params ...any) ([]byte, uint64, error) {
	var tracer tracers.Tracer
	if c.r.Tracing {
		tracer, _ = tracers.New("callTracer", new(tracers.Context))
		c.r.Evm.Config = vm.Config{Debug: true, Tracer: tracer}
	}
	input, err := c.abi.Pack(method, params...)
	require.NoError(c.r.T, err)
	out, consumed, err := c.r.call(opts, c.address, input)
	if c.r.Tracing {
		traceResult, err := tracer.GetResult()
		require.NoError(c.r.T, err)
		pretty, _ := json.MarshalIndent(traceResult, "", "    ")
		fmt.Println(string(pretty))
	}
	if err != nil {
		return out, 0, err
	}
	return out, consumed, nil
}

// call a method that does not belong to the contract, `c`.
// instead the method can be found in the contract, `methodHouse`.
func (c *contract) CallMethod(methodHouse *contract, opts *runOptions, method string, params ...any) ([]any, uint64, error) {
	var tracer tracers.Tracer
	if c.r.Tracing {
		tracer, _ = tracers.New("callTracer", new(tracers.Context))
		c.r.Evm.Config = vm.Config{Debug: true, Tracer: tracer}
	}
	input, err := methodHouse.abi.Pack(method, params...)
	require.NoError(c.r.T, err)
	out, consumed, err := c.r.call(opts, c.address, input)
	if c.r.Tracing {
		traceResult, err := tracer.GetResult()
		require.NoError(c.r.T, err)
		pretty, _ := json.MarshalIndent(traceResult, "", "    ")
		fmt.Println(string(pretty))
	}
	if err != nil {
		reason, _ := abi.UnpackRevert(out)
		return nil, 0, fmt.Errorf("%w: %s", err, reason)
	}
	res, err := methodHouse.abi.Unpack(method, out)
	require.NoError(c.r.T, err)
	return res, consumed, nil
}

type Committee struct {
	Validators           []IAutonityValidator
	LiquidStateContracts []*ILiquid
}

type Runner struct {
	T       *testing.T
	Evm     *vm.EVM
	Origin  common.Address // session's sender, can be overridden via runOptions
	Tracing bool

	// protocol contracts
	// todo: see if genesis deployment flow can be abstracted somehow
	Autonity               *Autonity
	Accountability         *Accountability
	Oracle                 *Oracle
	Acu                    *ACU
	Auctioneer             *Auctioneer
	SupplyControl          *SupplyControl
	Stabilization          *Stabilization
	UpgradeManager         *UpgradeManager
	InflationController    *InflationController
	OmissionAccountability *OmissionAccountability

	Committee Committee   // genesis validators for easy access
	Operator  *runOptions // operator runOptions for easy access
}

func (r *Runner) NoError(gasConsumed uint64, err error) uint64 {
	require.NoError(r.T, err)
	return gasConsumed
}

func (r *Runner) LiquidStateContract(validatorAddress common.Address) *ILiquid {
	validator, _, err := r.Autonity.GetValidator(nil, validatorAddress)
	require.NoError(r.T, err)
	return &ILiquid{r.contractObject(ILiquidMetaData, validator.LiquidStateContract)}
}

func (r *Runner) slasherContract() *Slasher {
	slasherAddr, _, err := r.Autonity.GetSlasher(nil)
	require.NoError(r.T, err)
	abi, err := SlasherMetaData.GetAbi()
	require.NoError(r.T, err)
	return &Slasher{&contract{slasherAddr, abi, r}}
}

func (r *Runner) call(opts *runOptions, addr common.Address, input []byte) ([]byte, uint64, error) {
	r.Evm.Origin = r.Origin
	value := common.Big0
	if opts != nil {
		r.Evm.Origin = opts.origin
		if opts.value != nil {
			value = opts.value
		}
	}
	gas := uint64(math.MaxUint64)
	ret, leftOver, err := r.Evm.Call(vm.AccountRef(r.Evm.Origin), addr, input, gas, value)
	return ret, gas - leftOver, err
}

func (r *Runner) snapshot() int {
	return r.Evm.StateDB.Snapshot()
}

func (r *Runner) revertSnapshot(id int) {
	r.Evm.StateDB.RevertToSnapshot(id)
}

// helpful to run a code snippet without changing the state
func (r *Runner) RunAndRevert(f func(r *Runner)) {
	context := r.Evm.Context
	snap := r.snapshot()
	committee := r.Committee
	f(r)
	r.revertSnapshot(snap)
	r.Evm.Context = context
	r.Committee = committee
}

// run is a convenience wrapper against t.run with automated state snapshot
func (r *Runner) Run(name string, f func(r *Runner)) {
	r.T.Run(name, func(t2 *testing.T) {
		t := r.T
		r.T = t2
		// in the future avoid mutating for supporting parallel testing
		context := r.Evm.Context
		snap := r.snapshot()
		committee := r.Committee
		f(r)
		r.revertSnapshot(snap)
		r.Evm.Context = context
		r.Committee = committee
		r.T = t
	})
}

func RunWithSetup(name string, setup func() *Runner, run func(r *Runner)) {
	r := setup()
	r.T.Run(name, func(t2 *testing.T) {
		run(r)
	})
}
func (r *Runner) GiveMeSomeMoney(account common.Address, amount *big.Int) {
	r.Evm.StateDB.AddBalance(account, amount)
}

func (r *Runner) GetBalanceOf(account common.Address) *big.Int {
	return r.Evm.StateDB.GetBalance(account)
}

func (r *Runner) GetNewtonBalanceOf(account common.Address) *big.Int {
	balance, _, err := r.Autonity.BalanceOf(nil, account)
	require.NoError(r.T, err)
	return balance
}

func (r *Runner) deployContract(
	opts *runOptions,
	contractAbi *abi.ABI,
	bytecode []byte,
	params ...any,
) (common.Address, uint64, *contract, []byte, error) {
	args, err := contractAbi.Pack("", params...)
	require.NoError(r.T, err)
	data := append(bytecode, args...)
	gas := uint64(math.MaxUint64)
	r.Evm.Origin = r.Origin
	value := common.Big0
	if opts != nil {
		r.Evm.Origin = opts.origin
		if opts.value != nil {
			value = opts.value
		}
	}
	out, contractAddress, leftOverGas, err := r.Evm.Create(vm.AccountRef(r.Evm.Origin), data, gas, value)
	return contractAddress, gas - leftOverGas, &contract{contractAddress, contractAbi, r}, out, err
}

func makeCommitteeFromTestKeys() *types.Committee {
	members := make([]types.CommitteeMember, len(params.TestConsensusKeys))
	for i, key := range params.TestConsensusKeys {
		privateKey, _ := blst.SecretKeyFromHex(key)
		member := types.CommitteeMember{
			Index:        uint64(i), //nolint:gosec
			VotingPower:  common.Big1,
			ConsensusKey: privateKey.PublicKey(),
		}
		members[i] = member
	}
	return &types.Committee{Members: members}
}

// generates an activity proof signed by all committee members, `absentees` excluded
// NOTE: if additional validators whose key is not in params.TestConsensusKey are registered in the tests,
// then this func needs to be modified to add their signatures as well.
func activityProof(committee []IAutonityValidator, headerSeal common.Hash, absentees map[common.Address]struct{}) *types.AggregateSignature {
	var signatures []blst.Signature //nolint
	consensusCommittee := makeCommitteeFromTestKeys()
	signers := types.NewSigners(consensusCommittee)
	numSigners := 0

	for _, keyHex := range params.TestConsensusKeys {
		// deserialize key
		key, err := blst.SecretKeyFromHex(keyHex)
		if err != nil {
			panic(err)
		}

		// find index in committee, and skip if not in it
		index := -1
		for j, member := range committee {
			if bytes.Equal(member.ConsensusKey, key.PublicKey().Marshal()) {
				index = j
			}
		}
		if index == -1 { // not in the committee
			continue
		}

		// skip if absent
		if _, isAbsent := absentees[committee[index].NodeAddress]; isAbsent {
			continue
		}
		signatures = append(signatures, key.Sign(headerSeal[:]))

		signers.AddSigner(uint64(index)) //nolint
		numSigners++
	}
	// if there are no signers, return an empty proof
	if numSigners == 0 {
		return nil
	}

	aggregateSig := blst.AggregateSignatures(signatures)

	return types.NewAggregateSignature(aggregateSig.(*blst.BlsSignature), signers)
}

// sets up an activity proof, `absentees` are excluded from it
func (r *Runner) setupActivityProofAndCoinbase(proposer common.Address, absentees map[common.Address]struct{}) {
	// if we are in TestMode, no need to set the activity proof, it will not be verified anyway
	if r.Evm.ChainConfig().TestMode {
		return
	}
	// initialize empty map if absentees was left nil
	if absentees == nil {
		absentees = make(map[common.Address]struct{})
	}
	epochInfo, _, err := r.Autonity.GetEpochInfo(nil)
	require.NoError(r.T, err)

	mustBeEmpty := r.Evm.Context.BlockNumber.Uint64() <= epochInfo.EpochBlock.Uint64()+epochInfo.OmissionDelta.Uint64()
	if !mustBeEmpty {
		r.Evm.Context.Coinbase = proposer
		targetHeight := r.Evm.Context.BlockNumber.Uint64() - epochInfo.OmissionDelta.Uint64()

		r.Evm.Context.ActivityProofRound = 0
		r.Evm.Context.ActivityProof = activityProof(r.Committee.Validators, sealFaker(targetHeight, r.Evm.Context.ActivityProofRound), absentees)
	}
}

func (r *Runner) lastMinedHeight() uint64 {
	return r.Evm.Context.BlockNumber.Uint64() - 1
}

// for omission
func (r *Runner) lastTargetHeight() uint64 {
	config, _, err := r.OmissionAccountability.GetConfig(nil)
	require.NoError(r.T, err)
	delta := config.Delta.Uint64()

	epochBlockBig, _, err := r.Autonity.GetLastEpochBlock(nil)
	require.NoError(r.T, err)
	epochBlock := epochBlockBig.Uint64()

	lastHeight := r.lastMinedHeight()

	require.Greater(r.T, lastHeight, epochBlock+delta)
	return lastHeight - delta
}

func (r *Runner) FinalizeBlock() {
	// Finalize is not the only block closing operation - fee redistribution is missing and prob
	// other stuff. Left as todo.
	epochID, _, err := r.Autonity.GetEpochID(nil)
	require.NoError(r.T, err)
	_, err = r.Autonity.Finalize(&runOptions{origin: common.Address{}})
	// consider monitoring gas cost here and fail if it's too much
	require.NoError(r.T, err, "finalize function error in block", r.Evm.Context.BlockNumber)
	r.Evm.Context.BlockNumber = new(big.Int).Add(r.Evm.Context.BlockNumber, common.Big1)
	r.Evm.Context.Time = new(big.Int).Add(r.Evm.Context.Time, common.Big1)
	// clean up activity proof related data
	r.Evm.Context.ActivityProof = nil
	r.Evm.Context.ActivityProofRound = 0
	r.Evm.Context.Coinbase = common.Address{}
	newEpochID, _, err := r.Autonity.GetEpochID(nil)
	require.NoError(r.T, err)
	if newEpochID.Cmp(epochID) != 0 {
		r.generateNewCommittee()
	}
}

func (r *Runner) WaitNBlocks(n int) {
	for i := 0; i < n; i++ {
		// set validator 0 as proposer always
		r.setupActivityProofAndCoinbase(r.Committee.Validators[0].NodeAddress, nil)
		r.FinalizeBlock()
	}
}

func (r *Runner) WaitNextEpoch() {
	info, _, err := r.Autonity.GetEpochInfo(nil)
	require.NoError(r.T, err)

	diff := new(big.Int).Sub(info.NextEpochBlock, r.Evm.Context.BlockNumber)
	r.WaitNBlocks(int(diff.Uint64() + 1))
}

func (r *Runner) contractObject(metadata *bind.MetaData, address common.Address) *contract {
	parsed, err := metadata.GetAbi()
	require.NoError(r.T, err)
	return &contract{address, parsed, r}
}

func (r *Runner) generateNewCommittee() {
	committeeMembers, _, err := r.Autonity.GetCommittee(nil)
	require.NoError(r.T, err)
	r.Committee.Validators = make([]IAutonityValidator, len(committeeMembers))
	r.Committee.LiquidStateContracts = make([]*ILiquid, len(committeeMembers))
	for i, member := range committeeMembers {
		validator, _, err := r.Autonity.GetValidator(nil, member.Addr)
		require.NoError(r.T, err)
		r.Committee.Validators[i] = validator
		r.Committee.LiquidStateContracts[i] = r.LiquidStateContract(validator.NodeAddress)
	}
}

func (r *Runner) WaitForBlocksUntil(endTime int64) int64 {
	// bcause we have 1 block/s
	r.WaitNBlocks(int(endTime) - int(r.Evm.Context.Time.Int64()))
	return r.Evm.Context.Time.Int64()
}

func (r *Runner) WaitForEpochsUntil(endTime int64) int64 {
	currentTime := r.Evm.Context.Time.Int64()
	for currentTime < endTime {
		r.WaitNextEpoch()
		currentTime = r.Evm.Context.Time.Int64()
	}
	return currentTime
}

func (r *Runner) SendAUT(sender, recipient common.Address, value *big.Int) {
	require.True(r.T, r.Evm.StateDB.GetBalance(sender).Cmp(value) >= 0, "not enough balance to transfer")
	r.Evm.StateDB.SubBalance(sender, value)
	r.Evm.StateDB.AddBalance(recipient, value)
}

func (r *Runner) CheckErrorAndGetData(data interface{}, _ uint64, err error) interface{} {
	require.NoError(r.T, err)
	return data
}

type EpochReward struct {
	RewardATN *big.Int
	RewardNTN *big.Int
}

func (r *Runner) RewardsForValidatorAfterOneEpoch(validator common.Address) EpochReward {
	committee, _, err := r.Autonity.GetCommittee(nil)
	require.NoError(r.T, err)
	found := false
	var votingPower *big.Int
	for _, member := range committee {
		if validator == member.Addr {
			found = true
			votingPower = member.VotingPower
			break
		}
	}
	if !found {
		return EpochReward{common.Big0, common.Big0}
	}

	reward := r.RewardsAfterOneEpoch()
	config, _, err := r.Autonity.GetConfig(nil)
	require.NoError(r.T, err)
	treasuryReward := new(big.Int).Div(new(big.Int).Mul(reward.RewardATN, config.Policy.TreasuryFee), params.DecimalFactor)

	totalStake, _, err := r.Autonity.GetEpochTotalBondedStake(nil)
	require.NoError(r.T, err)
	atnReward := new(big.Int).Sub(reward.RewardATN, treasuryReward)
	atnReward = new(big.Int).Div(new(big.Int).Mul(atnReward, votingPower), totalStake)
	ntnReward := new(big.Int).Div(new(big.Int).Mul(reward.RewardNTN, votingPower), totalStake)
	return EpochReward{atnReward, ntnReward}
}

func (r *Runner) RewardsAfterOneEpoch() (rewardsToDistribute EpochReward) {
	// get supply and inflationReserve to calculate inflation reward
	supply, _, err := r.Autonity.CirculatingSupply(nil)
	require.NoError(r.T, err)
	inflationReserve, _, err := r.Autonity.GetInflationReserve(nil)
	require.NoError(r.T, err)
	info, _, err := r.Autonity.GetEpochInfo(nil)
	require.NoError(r.T, err)
	// get inflation reward
	lastEpochTime, _, err := r.Autonity.GetLastEpochTime(nil)
	require.NoError(r.T, err)
	currentEpochTime := new(big.Int).Add(lastEpochTime, new(big.Int).Sub(info.NextEpochBlock, info.EpochBlock))
	inflationReward, _, err := r.InflationController.CalculateSupplyDelta(nil, supply, inflationReserve, lastEpochTime, currentEpochTime)
	require.NoError(r.T, err)
	if inflationReward.Cmp(inflationReserve) == 1 {
		inflationReward = inflationReserve
	}
	// get atn reward
	rewardsToDistribute.RewardATN = r.GetBalanceOf(r.Autonity.Address())
	rewardsToDistribute.RewardNTN = new(big.Int).Add(inflationReward, r.GetNewtonBalanceOf(r.Autonity.Address()))
	return rewardsToDistribute
}

func hashFaker(h uint64) common.Hash {
	return common.BytesToHash(new(big.Int).SetUint64(h).Bytes())
}

func sealFaker(targetHeight uint64, round uint64) common.Hash {
	return message.PrepareCommittedSeal(hashFaker(targetHeight), int64(round), new(big.Int).SetUint64(targetHeight))
}

func initializeEVM() (*vm.EVM, error) {
	ethDb := rawdb.NewMemoryDatabase()
	db := state.NewDatabase(ethDb)
	stateDB, err := state.New(common.Hash{}, db, nil)
	if err != nil {
		return nil, err
	}
	vmBlockContext := vm.BlockContext{
		Transfer: func(db vm.StateDB, sender, recipient common.Address, amount *big.Int) {
			db.SubBalance(sender, amount)
			db.AddBalance(recipient, amount)
		},
		CanTransfer: func(db vm.StateDB, addr common.Address, amount *big.Int) bool {
			return db.GetBalance(addr).Cmp(amount) >= 0
		},
		BlockNumber: common.Big0,
		Time:        big.NewInt(time.Now().Unix()),
		// used by the AbsenteeComputer precompile to verify activity proofs
		GetHash: hashFaker,
	}
	txContext := vm.TxContext{
		Origin:   common.Address{},
		GasPrice: common.Big0,
	}
	evm := vm.NewEVM(vmBlockContext, txContext, stateDB, params.TestChainConfig, vm.Config{})
	return evm, nil
}

func copyConfig(original *params.AutonityContractGenesis) *params.AutonityContractGenesis {
	jsonBytes, err := json.Marshal(original)
	if err != nil {
		panic("cannot marshal autonity genesis config: " + err.Error())
	}
	genesisCopy := &params.AutonityContractGenesis{}
	err = json.Unmarshal(jsonBytes, genesisCopy)
	if err != nil {
		panic("cannot unmarshal autonity genesis config: " + err.Error())
	}
	return genesisCopy
}

func SetInflationReserveZero(config *params.AutonityContractGenesis) *params.AutonityContractGenesis {
	config.InitialInflationReserve = (*math.HexOrDecimal256)(big.NewInt(0))
	return config
}

func Setup(t *testing.T, configOverride func(*params.AutonityContractGenesis) *params.AutonityContractGenesis) *Runner {
	evm, err := initializeEVM()
	require.NoError(t, err)
	r := &Runner{T: t, Evm: evm}

	//TODO: implement override also for the other contracts
	var autonityGenesis *params.AutonityContractGenesis
	if configOverride != nil {
		autonityGenesis = configOverride(copyConfig(params.TestAutonityContractConfig))
	} else {
		autonityGenesis = params.TestAutonityContractConfig
	}
	genesisConfig := &core.Genesis{
		Config: params.TestChainConfig,
	}
	genesisConfig.Config.AutonityContractConfig = autonityGenesis

	genesisConfig.Config.SetDefaults()

	//
	// Step 1: Execute test genesis sequence
	//

	err = autonity.ExecuteTestGenesisSequence(genesisConfig.Config, genesisConfig.Alloc.ToGenesisBonds(), evm)
	require.NoError(t, err)

	//
	// Step 2: Setup internal bindings
	//

	r.Autonity = &Autonity{&contract{
		params.AutonityContractAddress,
		&generated.AutonityTestAbi,
		r,
	}}
	r.Accountability = &Accountability{&contract{
		params.AccountabilityContractAddress,
		&generated.AccountabilityAbi,
		r,
	}}
	r.Oracle = &Oracle{&contract{
		params.OracleContractAddress,
		&generated.OracleAbi,
		r,
	}}
	r.Acu = &ACU{&contract{
		params.ACUContractAddress,
		&generated.ACUAbi,
		r,
	}}
	r.SupplyControl = &SupplyControl{&contract{
		params.SupplyControlContractAddress,
		&generated.SupplyControlAbi,
		r,
	}}
	r.Stabilization = &Stabilization{&contract{
		params.StabilizationContractAddress,
		&generated.StabilizationAbi,
		r,
	}}
	r.UpgradeManager = &UpgradeManager{&contract{
		params.UpgradeManagerContractAddress,
		&generated.UpgradeManagerAbi,
		r,
	}}
	r.InflationController = &InflationController{&contract{
		params.InflationControllerContractAddress,
		&generated.InflationControllerAbi,
		r,
	}}

	r.OmissionAccountability = &OmissionAccountability{&contract{
		params.OmissionAccountabilityContractAddress,
		&generated.OmissionAccountabilityAbi,
		r,
	}}

	r.Auctioneer = &Auctioneer{&contract{
		params.AuctioneerContractAddress,
		&generated.AuctioneerAbi,
		r,
	}}

	// TODO: replicate truffle tests default config.

	r.Operator = &runOptions{origin: genesisConfig.Config.AutonityContractConfig.Operator}

	r.Committee.Validators = make([]IAutonityValidator, 0, len(autonityGenesis.Validators))
	for _, v := range autonityGenesis.Validators {
		validator := genesisToAutonityVal(v)
		r.Committee.Validators = append(r.Committee.Validators, validator)
	}

	r.Committee.LiquidStateContracts = make([]*ILiquid, 0, len(autonityGenesis.Validators))
	for i, v := range autonityGenesis.Validators {
		validator, _, err := r.Autonity.GetValidator(nil, *v.NodeAddress)
		require.NoError(r.T, err)
		r.Committee.Validators[i] = validator
		r.Committee.LiquidStateContracts = append(r.Committee.LiquidStateContracts, r.LiquidStateContract(validator.NodeAddress))
	}

	r.Evm.Context.BlockNumber = common.Big1
	r.Evm.Context.Time = new(big.Int).Add(r.Evm.Context.Time, common.Big1)
	return r
}

// temporary until we find a better solution
func genesisToAutonityVal(v *params.Validator) IAutonityValidator {
	return IAutonityValidator{
		Treasury:                 v.Treasury,
		NodeAddress:              *v.NodeAddress,
		OracleAddress:            v.OracleAddress,
		Enode:                    v.Enode,
		CommissionRate:           v.CommissionRate,
		BondedStake:              v.BondedStake,
		UnbondingStake:           v.UnbondingStake,
		UnbondingShares:          v.UnbondingShares,
		SelfBondedStake:          v.SelfBondedStake,
		SelfUnbondingStake:       v.SelfUnbondingStake,
		SelfUnbondingShares:      v.SelfUnbondingShares,
		SelfUnbondingStakeLocked: v.SelfUnbondingStakeLocked,
		LiquidStateContract:      *v.LiquidStateContract,
		LiquidSupply:             v.LiquidSupply,
		RegistrationBlock:        v.RegistrationBlock,
		TotalSlashed:             v.TotalSlashed,
		JailReleaseBlock:         v.JailReleaseBlock,
		ConsensusKey:             v.ConsensusKey,
		State:                    *v.State,
		ConversionRatio:          v.ConversionRatio,
	}
}

func FromSender(sender common.Address, value *big.Int) *runOptions {
	return &runOptions{origin: sender, value: value}
}

func NewAccusationEvent(height uint64, value common.Hash, reporter common.Address, offenderIndex int, rule autonity.Rule) IAccountabilityEvent {
	if offenderIndex >= len(params.TestNodeKeys) {
		panic("offenderIndex out of range")
	}
	offenderNodeKey, _ := crypto.HexToECDSA(params.TestNodeKeys[offenderIndex])
	offender := crypto.PubkeyToAddress(offenderNodeKey.PublicKey)
	offenderConsensusKey, _ := blst.SecretKeyFromHex(params.TestConsensusKeys[offenderIndex])
	cm := types.CommitteeMember{Address: offender, VotingPower: common.Big1, ConsensusKey: offenderConsensusKey.PublicKey(), ConsensusKeyBytes: offenderConsensusKey.PublicKey().Marshal(), Index: uint64(offenderIndex)}
	signer := func(hash common.Hash) blst.Signature {
		return offenderConsensusKey.Sign(hash[:])
	}
	committee := makeCommitteeFromTestKeys()

	var msg message.Msg
	var evidences []message.Msg
	switch rule {
	case autonity.PVN:
		msg = message.NewPrevote(0, height, value, signer, &cm, committee)
	case autonity.PVO:
		r := uint64(2)
		msg = message.NewPrevote(int64(r), height, value, signer, &cm, committee)

		// attach also old light proposal as evidence
		vr := uint64(1)
		signaturePayload, err := rlp.EncodeToBytes([]any{message.ProposalCode, r, height, vr, false, value})
		if err != nil {
			panic("failed to generate light proposal as evidence: " + err.Error())
		}
		signatureInput := crypto.Hash(signaturePayload)
		signature := signer(signatureInput)

		evidences = []message.Msg{message.NewFakeLightPropose(message.Fake{
			FakeRound:          r,
			FakeHeight:         height,
			FakeValue:          value,
			FakeSignature:      signature,
			FakeSignatureInput: signatureInput,
			FakeSignerKey:      offenderConsensusKey.PublicKey(),
			FakeValidRound:     vr,
			FakeValidRoundNil:  false,
			FakeSigner:         offender,
			FakeSignerIndex:    uint64(offenderIndex), //nolint:gosec
			FakeVerified:       true,
		}, true)}
	case autonity.C1:
		msg = message.NewPrecommit(0, height, value, signer, &cm, committee)
	default:
		panic("unsupported rule")
	}

	p := &accountability.Proof{
		Type:          autonity.Accusation,
		Rule:          rule,
		Message:       msg,
		Evidences:     evidences,
		OffenderIndex: offenderIndex,
	}
	rawProof, err := rlp.EncodeToBytes(p)
	if err != nil {
		panic(err)
	}

	return IAccountabilityEvent{
		EventType:      uint8(p.Type),
		Rule:           uint8(p.Rule),
		Reporter:       reporter,
		Offender:       offender,
		RawProof:       rawProof,
		Id:             common.Big0,                           // assigned contract-side
		Block:          new(big.Int).SetUint64(p.Message.H()), // assigned contract-side
		ReportingBlock: common.Big0,                           // assigned contract-side
		Epoch:          common.Big0,                           // assigned contract-side
		MessageHash:    common.Big0,                           // assigned contract-side
	}
}

func RandomValidator() (params.Validator, []byte, *ecdsa.PrivateKey, blst.SecretKey, error) {
	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)
	validator := params.Validator{
		Treasury:      address,
		Enode:         enode,
		OracleAddress: address,
		ConsensusKey:  consensusKey.Marshal(),
	}
	pop, err := crypto.AutonityPOPProof(privateKey, privateKey, address.Hex(), secretKey)
	return validator, pop, privateKey, secretKey, err
}

// abi.encode(_reports, _salt, msg.sender) follows below encoding schema of the eth ABI specification.
var ReportABIEncodeSchema = []byte("[{\"components\":[{\"internalType\":\"uint120\",\"name\":\"price\",\"type\":\"uint120\"},{\"internalType\":\"uint8\",\"name\":\"confidence\",\"type\":\"uint8\"}],\"internalType\":\"struct Report[]\",\"name\":\"_reports\",\"type\":\"tuple[]\"},{\"internalType\":\"uint256\",\"name\":\"_salt\",\"type\":\"uint256\"},{\"internalType\":\"address\",\"name\":\"sender\",\"type\":\"address\"}]")

func MakeOracleCommit(t *testing.T, salt *big.Int, sender common.Address, reports []IOracleReport) *big.Int {
	var args abi.Arguments
	err := json.Unmarshal(ReportABIEncodeSchema, &args)
	require.NoError(t, err)

	var hash common.Hash
	bytes, err := args.Pack(reports, salt, sender)
	require.NoError(t, err)

	hash = crypto.Keccak256Hash(bytes)
	return new(big.Int).SetBytes(hash[:])

}
