package testutil

import (
	"time"

	abci "github.com/cometbft/cometbft/abci/types"
	"github.com/cometbft/cometbft/crypto/tmhash"
	tmproto "github.com/cometbft/cometbft/proto/tendermint/types"
	tmversion "github.com/cometbft/cometbft/proto/tendermint/version"
	tmtime "github.com/cometbft/cometbft/types/time"
	"github.com/cometbft/cometbft/version"
	sdk "github.com/cosmos/cosmos-sdk/types"
	authkeeper "github.com/cosmos/cosmos-sdk/x/auth/keeper"
	bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper"
	minttypes "github.com/cosmos/cosmos-sdk/x/mint/types"
	"github.com/evmos/ethermint/crypto/ethsecp256k1"
	"github.com/stretchr/testify/suite"

	"github.com/kava-labs/kava/app"
	"github.com/kava-labs/kava/x/precisebank/keeper"
	"github.com/kava-labs/kava/x/precisebank/types"
)

type Suite struct {
	suite.Suite

	App           app.TestApp
	Ctx           sdk.Context
	BankKeeper    bankkeeper.Keeper
	AccountKeeper authkeeper.AccountKeeper
	Keeper        keeper.Keeper
}

func (suite *Suite) SetupTest() {
	tApp := app.NewTestApp()

	suite.Ctx = tApp.NewContext(true, tmproto.Header{Height: 1, Time: tmtime.Now()})
	suite.App = tApp
	suite.BankKeeper = tApp.GetBankKeeper()
	suite.AccountKeeper = tApp.GetAccountKeeper()
	suite.Keeper = tApp.GetPrecisebankKeeper()

	cdc := suite.App.AppCodec()
	coins := sdk.NewCoins(sdk.NewInt64Coin("ukava", 1000_000_000_000_000_000))
	authGS := app.NewFundedGenStateWithSameCoins(cdc, coins, []sdk.AccAddress{})

	gs := app.GenesisState{}
	suite.App.InitializeFromGenesisStates(authGS, gs)

	// consensus key - needed to set up evm module
	consPriv, err := ethsecp256k1.GenerateKey()
	suite.Require().NoError(err)
	consAddress := sdk.ConsAddress(consPriv.PubKey().Address())

	// InitializeFromGenesisStates commits first block so we start at 2 here
	suite.Ctx = suite.App.NewContext(false, tmproto.Header{
		Height:          suite.App.LastBlockHeight() + 1,
		ChainID:         app.TestChainId,
		Time:            time.Now().UTC(),
		ProposerAddress: consAddress.Bytes(),
		Version: tmversion.Consensus{
			Block: version.BlockProtocol,
		},
		LastBlockId: tmproto.BlockID{
			Hash: tmhash.Sum([]byte("block_id")),
			PartSetHeader: tmproto.PartSetHeader{
				Total: 11,
				Hash:  tmhash.Sum([]byte("partset_header")),
			},
		},
		AppHash:            tmhash.Sum([]byte("app")),
		DataHash:           tmhash.Sum([]byte("data")),
		EvidenceHash:       tmhash.Sum([]byte("evidence")),
		ValidatorsHash:     tmhash.Sum([]byte("validators")),
		NextValidatorsHash: tmhash.Sum([]byte("next_validators")),
		ConsensusHash:      tmhash.Sum([]byte("consensus")),
		LastResultsHash:    tmhash.Sum([]byte("last_result")),
	})
}

func (suite *Suite) Commit() {
	_ = suite.App.Commit()
	header := suite.Ctx.BlockHeader()
	header.Height += 1
	suite.App.BeginBlock(abci.RequestBeginBlock{
		Header: header,
	})

	// update ctx
	suite.Ctx = suite.App.NewContext(false, header)
}

// MintToAccount mints coins to an account with the x/precisebank methods. This
// must be used when minting extended coins, ie. akava coins. This depends on
// the methods to be properly tested to be implemented correctly.
func (suite *Suite) MintToAccount(addr sdk.AccAddress, amt sdk.Coins) {
	accBalancesBefore := suite.GetAllBalances(addr)

	err := suite.Keeper.MintCoins(suite.Ctx, minttypes.ModuleName, amt)
	suite.Require().NoError(err)

	err = suite.Keeper.SendCoinsFromModuleToAccount(suite.Ctx, minttypes.ModuleName, addr, amt)
	suite.Require().NoError(err)

	// Double check balances are correctly minted and sent to account
	accBalancesAfter := suite.GetAllBalances(addr)

	netIncrease := accBalancesAfter.Sub(accBalancesBefore...)
	suite.Require().Equal(ConvertCoinsToExtendedCoinDenom(amt), netIncrease)

	suite.T().Logf("minted %s to %s", amt, addr)
}

// GetAllBalances returns all the account balances for the given account address.
// This returns the extended coin balance if the account has a non-zero balance,
// WITHOUT the integer coin balance.
func (suite *Suite) GetAllBalances(addr sdk.AccAddress) sdk.Coins {
	// Get all balances for an account
	bankBalances := suite.BankKeeper.GetAllBalances(suite.Ctx, addr)

	// Remove integer coins from the balance
	for _, coin := range bankBalances {
		if coin.Denom == types.IntegerCoinDenom {
			bankBalances = bankBalances.Sub(coin)
		}
	}

	// Replace the integer coin with the extended coin, from x/precisebank
	extendedBal := suite.Keeper.GetBalance(suite.Ctx, addr, types.ExtendedCoinDenom)

	return bankBalances.Add(extendedBal)
}

// ConvertCoinsToExtendedCoinDenom converts sdk.Coins that includes Integer denoms
// to sdk.Coins that includes Extended denoms of the same amount. This is useful
// for testing to make sure only extended amounts are compared instead of double
// counting balances.
func ConvertCoinsToExtendedCoinDenom(coins sdk.Coins) sdk.Coins {
	integerCoinAmt := coins.AmountOf(types.IntegerCoinDenom)
	if integerCoinAmt.IsZero() {
		return coins
	}

	// Remove the integer coin from the coins
	integerCoin := sdk.NewCoin(types.IntegerCoinDenom, integerCoinAmt)

	// Add the equivalent extended coin to the coins
	extendedCoin := sdk.NewCoin(types.ExtendedCoinDenom, integerCoinAmt.Mul(types.ConversionFactor()))

	return coins.Sub(integerCoin).Add(extendedCoin)
}