package simulation_test

import (
	"math/big"
	"math/rand"
	"testing"
	"time"

	abci "git.cw.tr/mukan-network/mukan-consensus/abci/types"
	cmttypes "git.cw.tr/mukan-network/mukan-consensus/types"
	"github.com/cosmos/gogoproto/proto"
	"github.com/stretchr/testify/require"
	"github.com/stretchr/testify/suite"

	"cosmossdk.io/depinject"
	sdklog "cosmossdk.io/log"
	"cosmossdk.io/math"

	"git.cw.tr/mukan-network/mukan-sdk/client"
	"git.cw.tr/mukan-network/mukan-sdk/codec/address"
	cryptocodec "git.cw.tr/mukan-network/mukan-sdk/crypto/codec"
	"git.cw.tr/mukan-network/mukan-sdk/crypto/keys/secp256k1"
	"git.cw.tr/mukan-network/mukan-sdk/runtime"
	simtestutil "git.cw.tr/mukan-network/mukan-sdk/testutil/sims"
	sdk "git.cw.tr/mukan-network/mukan-sdk/types"
	moduletestutil "git.cw.tr/mukan-network/mukan-sdk/types/module/testutil"
	simtypes "git.cw.tr/mukan-network/mukan-sdk/types/simulation"
	authkeeper "git.cw.tr/mukan-network/mukan-sdk/x/auth/keeper"
	authtypes "git.cw.tr/mukan-network/mukan-sdk/x/auth/types"
	bankkeeper "git.cw.tr/mukan-network/mukan-sdk/x/bank/keeper"
	banktestutil "git.cw.tr/mukan-network/mukan-sdk/x/bank/testutil"
	distrkeeper "git.cw.tr/mukan-network/mukan-sdk/x/distribution/keeper"
	distrtypes "git.cw.tr/mukan-network/mukan-sdk/x/distribution/types"
	mintkeeper "git.cw.tr/mukan-network/mukan-sdk/x/mint/keeper"
	minttypes "git.cw.tr/mukan-network/mukan-sdk/x/mint/types"
	stakingkeeper "git.cw.tr/mukan-network/mukan-sdk/x/staking/keeper"
	"git.cw.tr/mukan-network/mukan-sdk/x/staking/simulation"
	"git.cw.tr/mukan-network/mukan-sdk/x/staking/testutil"
	"git.cw.tr/mukan-network/mukan-sdk/x/staking/types"
)

type SimTestSuite struct {
	suite.Suite

	r             *rand.Rand
	txConfig      client.TxConfig
	accounts      []simtypes.Account
	ctx           sdk.Context
	app           *runtime.App
	bankKeeper    bankkeeper.Keeper
	accountKeeper authkeeper.AccountKeeper
	distrKeeper   distrkeeper.Keeper
	stakingKeeper *stakingkeeper.Keeper

	encCfg moduletestutil.TestEncodingConfig
}

func (s *SimTestSuite) SetupTest() {
	sdk.DefaultPowerReduction = math.NewIntFromBigInt(new(big.Int).Exp(big.NewInt(10), big.NewInt(18), nil))

	s.r = rand.New(rand.NewSource(1))
	accounts := simtypes.RandomAccounts(s.r, 4)

	// create genesis accounts
	senderPrivKey := secp256k1.GenPrivKey()
	acc := authtypes.NewBaseAccount(senderPrivKey.PubKey().Address().Bytes(), senderPrivKey.PubKey(), 0, 0)
	accs := []simtestutil.GenesisAccount{
		{GenesisAccount: acc, Coins: sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, math.NewInt(100000000000000)))},
	}

	// create validator set with single validator
	account := accounts[0]
	cmtPk, err := cryptocodec.ToCmtPubKeyInterface(account.PubKey)
	require.NoError(s.T(), err)
	validator := cmttypes.NewValidator(cmtPk, 1)

	startupCfg := simtestutil.DefaultStartUpConfig()
	startupCfg.GenesisAccounts = accs
	startupCfg.ValidatorSet = func() (*cmttypes.ValidatorSet, error) {
		return cmttypes.NewValidatorSet([]*cmttypes.Validator{validator}), nil
	}

	var (
		accountKeeper authkeeper.AccountKeeper
		mintKeeper    mintkeeper.Keeper
		bankKeeper    bankkeeper.Keeper
		distrKeeper   distrkeeper.Keeper
		stakingKeeper *stakingkeeper.Keeper
	)

	cfg := depinject.Configs(
		testutil.AppConfig,
		depinject.Supply(sdklog.NewNopLogger()),
	)

	app, err := simtestutil.SetupWithConfiguration(cfg, startupCfg, &s.txConfig, &bankKeeper, &accountKeeper, &mintKeeper, &distrKeeper, &stakingKeeper)
	require.NoError(s.T(), err)

	ctx := app.NewContext(false)
	s.Require().NoError(mintKeeper.Params.Set(ctx, minttypes.DefaultParams()))
	s.Require().NoError(mintKeeper.Minter.Set(ctx, minttypes.DefaultInitialMinter()))

	initAmt := stakingKeeper.TokensFromConsensusPower(ctx, 200)
	initCoins := sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, initAmt))

	s.accounts = accounts
	// remove genesis validator account
	// add coins to the accounts
	for _, account := range accounts[1:] {
		acc := accountKeeper.NewAccountWithAddress(ctx, account.Address)
		accountKeeper.SetAccount(ctx, acc)
		s.Require().NoError(banktestutil.FundAccount(ctx, bankKeeper, account.Address, initCoins))
	}

	s.accountKeeper = accountKeeper
	s.bankKeeper = bankKeeper
	s.distrKeeper = distrKeeper
	s.stakingKeeper = stakingKeeper
	s.ctx = ctx
	s.app = app
}

// TestWeightedOperations tests the weights of the operations.
func (s *SimTestSuite) TestWeightedOperations() {
	require := s.Require()

	s.ctx.WithChainID("test-chain")

	cdc := s.encCfg.Codec
	appParams := make(simtypes.AppParams)

	weightedOps := simulation.WeightedOperations(appParams, cdc, s.txConfig, s.accountKeeper,
		s.bankKeeper, s.stakingKeeper,
	)

	expected := []struct {
		weight     int
		opMsgRoute string
		opMsgName  string
	}{
		{simulation.DefaultWeightMsgCreateValidator, types.ModuleName, sdk.MsgTypeURL(&types.MsgCreateValidator{})},
		{simulation.DefaultWeightMsgEditValidator, types.ModuleName, sdk.MsgTypeURL(&types.MsgEditValidator{})},
		{simulation.DefaultWeightMsgDelegate, types.ModuleName, sdk.MsgTypeURL(&types.MsgDelegate{})},
		{simulation.DefaultWeightMsgUndelegate, types.ModuleName, sdk.MsgTypeURL(&types.MsgUndelegate{})},
		{simulation.DefaultWeightMsgBeginRedelegate, types.ModuleName, sdk.MsgTypeURL(&types.MsgBeginRedelegate{})},
		{simulation.DefaultWeightMsgCancelUnbondingDelegation, types.ModuleName, sdk.MsgTypeURL(&types.MsgCancelUnbondingDelegation{})},
	}

	for i, w := range weightedOps {
		operationMsg, _, _ := w.Op()(s.r, s.app.BaseApp, s.ctx, s.accounts, s.ctx.ChainID())
		// require.NoError(t, err) // TODO check if it should be NoError

		// the following checks are very much dependent from the ordering of the output given
		// by WeightedOperations. if the ordering in WeightedOperations changes some tests
		// will fail
		require.Equal(expected[i].weight, w.Weight(), "weight should be the same")
		require.Equal(expected[i].opMsgRoute, operationMsg.Route, "route should be the same")
		require.Equal(expected[i].opMsgName, operationMsg.Name, "operation Msg name should be the same")
	}
}

// TestSimulateMsgCreateValidator tests the normal scenario of a valid message of type TypeMsgCreateValidator.
// Abonormal scenarios, where the message are created by an errors are not tested here.
func (s *SimTestSuite) TestSimulateMsgCreateValidator() {
	require := s.Require()
	_, err := s.app.FinalizeBlock(&abci.RequestFinalizeBlock{Height: s.app.LastBlockHeight() + 1, Hash: s.app.LastCommitID().Hash})
	require.NoError(err)

	// execute operation
	op := simulation.SimulateMsgCreateValidator(s.txConfig, s.accountKeeper, s.bankKeeper, s.stakingKeeper)
	operationMsg, futureOperations, err := op(s.r, s.app.BaseApp, s.ctx, s.accounts[1:], "")
	require.NoError(err)

	var msg types.MsgCreateValidator
	err = proto.Unmarshal(operationMsg.Msg, &msg)
	require.NoError(err)
	require.True(operationMsg.OK)
	require.Equal(sdk.MsgTypeURL(&types.MsgCreateValidator{}), sdk.MsgTypeURL(&msg))
	valaddr, err := sdk.ValAddressFromBech32(msg.ValidatorAddress)
	require.NoError(err)
	require.Equal("cosmos1p8wcgrjr4pjju90xg6u9cgq55dxwq8j7u4x9a0", sdk.AccAddress(valaddr).String())
	require.Equal("cosmosvaloper1p8wcgrjr4pjju90xg6u9cgq55dxwq8j7epjs3u", msg.ValidatorAddress)
	require.Len(futureOperations, 0)
}

// TestSimulateMsgCancelUnbondingDelegation tests the normal scenario of a valid message of type TypeMsgCancelUnbondingDelegation.
// Abonormal scenarios, where the message is
func (s *SimTestSuite) TestSimulateMsgCancelUnbondingDelegation() {
	require := s.Require()
	blockTime := time.Now().UTC()
	ctx := s.ctx.WithBlockTime(blockTime)

	// setup accounts[1] as validator
	validator0 := s.getTestingValidator0(ctx)

	// setup delegation
	delTokens := s.stakingKeeper.TokensFromConsensusPower(ctx, 2)
	validator0, issuedShares := validator0.AddTokensFromDel(delTokens)
	delegator := s.accounts[2]
	delegation := types.NewDelegation(delegator.Address.String(), validator0.GetOperator(), issuedShares)
	require.NoError(s.stakingKeeper.SetDelegation(ctx, delegation))
	val0bz, err := s.stakingKeeper.ValidatorAddressCodec().StringToBytes(validator0.GetOperator())
	s.Require().NoError(err)
	require.NoError(s.distrKeeper.SetDelegatorStartingInfo(ctx, val0bz, delegator.Address, distrtypes.NewDelegatorStartingInfo(2, math.LegacyOneDec(), 200)))

	s.setupValidatorRewards(ctx, val0bz)

	// unbonding delegation
	udb := types.NewUnbondingDelegation(delegator.Address, val0bz, s.app.LastBlockHeight()+1, blockTime.Add(2*time.Minute), delTokens, 0, address.NewBech32Codec("cosmosvaloper"), address.NewBech32Codec("cosmos"))
	require.NoError(s.stakingKeeper.SetUnbondingDelegation(ctx, udb))
	s.setupValidatorRewards(ctx, val0bz)

	_, err = s.app.FinalizeBlock(&abci.RequestFinalizeBlock{Height: s.app.LastBlockHeight() + 1, Hash: s.app.LastCommitID().Hash, Time: blockTime})
	require.NoError(err)

	// execute operation
	op := simulation.SimulateMsgCancelUnbondingDelegate(s.txConfig, s.accountKeeper, s.bankKeeper, s.stakingKeeper)
	accounts := []simtypes.Account{delegator}
	operationMsg, futureOperations, err := op(s.r, s.app.BaseApp, ctx, accounts, "")
	require.NoError(err)

	var msg types.MsgCancelUnbondingDelegation
	err = proto.Unmarshal(operationMsg.Msg, &msg)
	require.NoError(err)
	require.True(operationMsg.OK)
	require.Equal(sdk.MsgTypeURL(&types.MsgCancelUnbondingDelegation{}), sdk.MsgTypeURL(&msg))
	require.Equal(delegator.Address.String(), msg.DelegatorAddress)
	require.Equal(validator0.GetOperator(), msg.ValidatorAddress)
	require.Len(futureOperations, 0)
}

// TestSimulateMsgEditValidator tests the normal scenario of a valid message of type TypeMsgEditValidator.
// Abonormal scenarios, where the message is created by an errors are not tested here.
func (s *SimTestSuite) TestSimulateMsgEditValidator() {
	require := s.Require()
	blockTime := time.Now().UTC()
	ctx := s.ctx.WithBlockTime(blockTime)

	// setup accounts[0] as validator
	_ = s.getTestingValidator0(ctx)

	_, err := s.app.FinalizeBlock(&abci.RequestFinalizeBlock{Height: s.app.LastBlockHeight() + 1, Hash: s.app.LastCommitID().Hash, Time: blockTime})
	require.NoError(err)

	// execute operation
	op := simulation.SimulateMsgEditValidator(s.txConfig, s.accountKeeper, s.bankKeeper, s.stakingKeeper)
	operationMsg, futureOperations, err := op(s.r, s.app.BaseApp, ctx, s.accounts, "")
	require.NoError(err)

	var msg types.MsgEditValidator
	err = proto.Unmarshal(operationMsg.Msg, &msg)
	require.NoError(err)
	require.True(operationMsg.OK)
	require.Equal(sdk.MsgTypeURL(&types.MsgEditValidator{}), sdk.MsgTypeURL(&msg))
	require.Equal("cosmosvaloper1p8wcgrjr4pjju90xg6u9cgq55dxwq8j7epjs3u", msg.ValidatorAddress)
	require.Len(futureOperations, 0)
}

// TestSimulateMsgDelegate tests the normal scenario of a valid message of type TypeMsgDelegate.
// Abonormal scenarios, where the message is created by an errors are not tested here.
func (s *SimTestSuite) TestSimulateMsgDelegate() {
	require := s.Require()
	blockTime := time.Now().UTC()
	ctx := s.ctx.WithBlockTime(blockTime)

	// execute operation
	op := simulation.SimulateMsgDelegate(s.txConfig, s.accountKeeper, s.bankKeeper, s.stakingKeeper)
	operationMsg, futureOperations, err := op(s.r, s.app.BaseApp, ctx, s.accounts[1:], "")
	require.NoError(err)

	var msg types.MsgDelegate
	err = proto.Unmarshal(operationMsg.Msg, &msg)
	require.NoError(err)
	require.True(operationMsg.OK)
	require.Equal("cosmos1p8wcgrjr4pjju90xg6u9cgq55dxwq8j7u4x9a0", msg.DelegatorAddress)
	require.Equal("stake", msg.Amount.Denom)
	require.Equal(sdk.MsgTypeURL(&types.MsgDelegate{}), sdk.MsgTypeURL(&msg))
	require.Equal("cosmosvaloper1tnh2q55v8wyygtt9srz5safamzdengsn9dsd7z", msg.ValidatorAddress)
	require.Len(futureOperations, 0)
}

// TestSimulateMsgUndelegate tests the normal scenario of a valid message of type TypeMsgUndelegate.
// Abonormal scenarios, where the message is created by an errors are not tested here.
func (s *SimTestSuite) TestSimulateMsgUndelegate() {
	require := s.Require()
	blockTime := time.Now().UTC()
	ctx := s.ctx.WithBlockTime(blockTime)

	// setup accounts[1] as validator
	validator0 := s.getTestingValidator0(ctx)

	// setup delegation
	delTokens := s.stakingKeeper.TokensFromConsensusPower(ctx, 2)
	validator0, issuedShares := validator0.AddTokensFromDel(delTokens)
	delegator := s.accounts[2]
	delegation := types.NewDelegation(delegator.Address.String(), validator0.GetOperator(), issuedShares)
	require.NoError(s.stakingKeeper.SetDelegation(ctx, delegation))
	val0bz, err := s.stakingKeeper.ValidatorAddressCodec().StringToBytes(validator0.GetOperator())
	s.Require().NoError(err)
	require.NoError(s.distrKeeper.SetDelegatorStartingInfo(ctx, val0bz, delegator.Address, distrtypes.NewDelegatorStartingInfo(2, math.LegacyOneDec(), 200)))

	s.setupValidatorRewards(ctx, val0bz)

	_, err = s.app.FinalizeBlock(&abci.RequestFinalizeBlock{Height: s.app.LastBlockHeight() + 1, Hash: s.app.LastCommitID().Hash, Time: blockTime})
	require.NoError(err)

	// execute operation
	op := simulation.SimulateMsgUndelegate(s.txConfig, s.accountKeeper, s.bankKeeper, s.stakingKeeper)
	operationMsg, futureOperations, err := op(s.r, s.app.BaseApp, ctx, s.accounts, "")
	require.NoError(err)

	var msg types.MsgUndelegate
	err = proto.Unmarshal(operationMsg.Msg, &msg)
	require.NoError(err)
	require.True(operationMsg.OK)
	require.Equal("cosmos1ghekyjucln7y67ntx7cf27m9dpuxxemn4c8g4r", msg.DelegatorAddress)
	require.Equal("1646627814093010272", msg.Amount.Amount.String())
	require.Equal("stake", msg.Amount.Denom)
	require.Equal(sdk.MsgTypeURL(&types.MsgUndelegate{}), sdk.MsgTypeURL(&msg))
	require.Equal("cosmosvaloper1p8wcgrjr4pjju90xg6u9cgq55dxwq8j7epjs3u", msg.ValidatorAddress)
	require.Len(futureOperations, 0)
}

// TestSimulateMsgBeginRedelegate tests the normal scenario of a valid message of type TypeMsgBeginRedelegate.
// Abonormal scenarios, where the message is created by an errors, are not tested here.
func (s *SimTestSuite) TestSimulateMsgBeginRedelegate() {
	require := s.Require()
	blockTime := time.Now().UTC()
	ctx := s.ctx.WithBlockTime(blockTime)

	// setup accounts[1] as validator0 and accounts[2] as validator1
	validator0 := s.getTestingValidator0(ctx)
	validator1 := s.getTestingValidator1(ctx)

	delTokens := s.stakingKeeper.TokensFromConsensusPower(ctx, 2)
	validator1, issuedShares := validator1.AddTokensFromDel(delTokens)

	// setup accounts[3] as delegator
	delegator := s.accounts[3]
	delegation := types.NewDelegation(delegator.Address.String(), validator0.GetOperator(), issuedShares)
	require.NoError(s.stakingKeeper.SetDelegation(ctx, delegation))
	val0bz, err := s.stakingKeeper.ValidatorAddressCodec().StringToBytes(validator0.GetOperator())
	s.Require().NoError(err)
	val1bz, err := s.stakingKeeper.ValidatorAddressCodec().StringToBytes(validator1.GetOperator())
	s.Require().NoError(err)
	require.NoError(s.distrKeeper.SetDelegatorStartingInfo(ctx, val0bz, delegator.Address, distrtypes.NewDelegatorStartingInfo(2, math.LegacyOneDec(), 200)))

	s.setupValidatorRewards(ctx, val0bz)
	s.setupValidatorRewards(ctx, val1bz)

	_, err = s.app.FinalizeBlock(&abci.RequestFinalizeBlock{Height: s.app.LastBlockHeight() + 1, Hash: s.app.LastCommitID().Hash, Time: blockTime})
	require.NoError(err)

	// execute operation
	op := simulation.SimulateMsgBeginRedelegate(s.txConfig, s.accountKeeper, s.bankKeeper, s.stakingKeeper)
	operationMsg, futureOperations, err := op(s.r, s.app.BaseApp, ctx, s.accounts, "")
	s.T().Logf("operation message: %v", operationMsg)
	require.NoError(err)

	var msg types.MsgBeginRedelegate
	err = proto.Unmarshal(operationMsg.Msg, &msg)
	require.NoError(err)
	require.True(operationMsg.OK)
	require.Equal("cosmos1ua0fwyws7vzjrry3pqkklvf8mny93l9s9zg0h4", msg.DelegatorAddress)
	require.Equal("stake", msg.Amount.Denom)
	require.Equal(sdk.MsgTypeURL(&types.MsgBeginRedelegate{}), sdk.MsgTypeURL(&msg))
	require.Equal("cosmosvaloper1ghekyjucln7y67ntx7cf27m9dpuxxemnsvnaes", msg.ValidatorDstAddress)
	require.Equal("cosmosvaloper1p8wcgrjr4pjju90xg6u9cgq55dxwq8j7epjs3u", msg.ValidatorSrcAddress)
	require.Len(futureOperations, 0)
}

func (s *SimTestSuite) getTestingValidator0(ctx sdk.Context) types.Validator {
	commission0 := types.NewCommission(math.LegacyZeroDec(), math.LegacyOneDec(), math.LegacyOneDec())
	return s.getTestingValidator(ctx, commission0, 1)
}

func (s *SimTestSuite) getTestingValidator1(ctx sdk.Context) types.Validator {
	commission1 := types.NewCommission(math.LegacyZeroDec(), math.LegacyZeroDec(), math.LegacyZeroDec())
	return s.getTestingValidator(ctx, commission1, 2)
}

func (s *SimTestSuite) getTestingValidator(ctx sdk.Context, commission types.Commission, n int) types.Validator {
	account := s.accounts[n]
	valPubKey := account.PubKey
	valAddr := sdk.ValAddress(account.PubKey.Address().Bytes())
	validator := testutil.NewValidator(s.T(), valAddr, valPubKey)
	validator, err := validator.SetInitialCommission(commission)
	s.Require().NoError(err)

	validator.DelegatorShares = math.LegacyNewDec(100)
	validator.Tokens = s.stakingKeeper.TokensFromConsensusPower(ctx, 100)

	s.Require().NoError(s.stakingKeeper.SetValidator(ctx, validator))

	return validator
}

func (s *SimTestSuite) setupValidatorRewards(ctx sdk.Context, valAddress sdk.ValAddress) {
	s.T().Helper()

	decCoins := sdk.DecCoins{sdk.NewDecCoinFromDec(sdk.DefaultBondDenom, math.LegacyOneDec())}
	historicalRewards := distrtypes.NewValidatorHistoricalRewards(decCoins, 2)
	s.Require().NoError(s.distrKeeper.SetValidatorHistoricalRewards(ctx, valAddress, 2, historicalRewards))
	// setup current revards
	currentRewards := distrtypes.NewValidatorCurrentRewards(decCoins, 3)
	s.Require().NoError(s.distrKeeper.SetValidatorCurrentRewards(ctx, valAddress, currentRewards))
}

func TestSimTestSuite(t *testing.T) {
	suite.Run(t, new(SimTestSuite))
}
