package keeper_test

import (
	"context"
	"slices"
	"testing"

	"github.com/stretchr/testify/suite"
	"go.uber.org/mock/gomock"

	"cosmossdk.io/math"
	storetypes "cosmossdk.io/store/types"

	"git.cw.tr/mukan-network/mukan-sdk/runtime"
	"git.cw.tr/mukan-network/mukan-sdk/testutil"
	sdk "git.cw.tr/mukan-network/mukan-sdk/types"
	moduletestutil "git.cw.tr/mukan-network/mukan-sdk/types/module/testutil"
	authtypes "git.cw.tr/mukan-network/mukan-sdk/x/auth/types"
	govtypes "git.cw.tr/mukan-network/mukan-sdk/x/gov/types"
	"git.cw.tr/mukan-network/mukan-sdk/x/mint"
	"git.cw.tr/mukan-network/mukan-sdk/x/mint/keeper"
	minttestutil "git.cw.tr/mukan-network/mukan-sdk/x/mint/testutil"
	"git.cw.tr/mukan-network/mukan-sdk/x/mint/types"
)

// MintFnTestSuite defines the integration test suite for minting.
type MintFnTestSuite struct {
	suite.Suite

	mintKeeper    keeper.Keeper
	ctx           sdk.Context
	stakingKeeper *minttestutil.MockStakingKeeper
	bankKeeper    *minttestutil.MockBankKeeper
}

// TestMintFnTestSuite runs the mint test suite.
func TestMintFnTestSuite(t *testing.T) {
	suite.Run(t, new(MintFnTestSuite))
}

// SetupTest sets up the context, KV store, and mocks.
func (s *MintFnTestSuite) SetupTest() {
	encCfg := moduletestutil.MakeTestEncodingConfig(mint.AppModuleBasic{})
	key := storetypes.NewKVStoreKey(types.StoreKey)
	storeService := runtime.NewKVStoreService(key)
	testCtx := testutil.DefaultContextWithDB(s.T(), key, storetypes.NewTransientStoreKey("transient_test"))
	s.ctx = testCtx.Ctx

	ctrl := gomock.NewController(s.T())
	accountKeeper := minttestutil.NewMockAccountKeeper(ctrl)
	s.bankKeeper = minttestutil.NewMockBankKeeper(ctrl)
	s.stakingKeeper = minttestutil.NewMockStakingKeeper(ctrl)

	// Return a dummy module address for the mint module.
	accountKeeper.EXPECT().GetModuleAddress(types.ModuleName).Return(sdk.AccAddress{}).AnyTimes()

	// Override the default mint function with our dummy inflation calculator.
	s.mintKeeper = keeper.NewKeeper(
		encCfg.Codec,
		storeService,
		s.stakingKeeper,
		accountKeeper,
		s.bankKeeper,
		authtypes.FeeCollectorName,
		authtypes.NewModuleAddress(govtypes.ModuleName).String(),
	)

	// Set default parameters.
	err := s.mintKeeper.Params.Set(s.ctx, types.DefaultParams())
	s.Require().NoError(err)

	// Set a known dummy minter in the store for deterministic behavior.
	s.Require().NoError(s.mintKeeper.Minter.Set(s.ctx, types.DefaultInitialMinter()))
}

// TestDefaultMintFn_Success tests the successful execution of the default mint function.
func (s *MintFnTestSuite) TestDefaultMintFn_Success() {
	// Set the staking keeper expectations.
	stakingSupply := math.NewInt(1_000_000_000)
	bondedRatio := math.LegacyNewDecWithPrec(50, 2) // 0.50
	s.stakingKeeper.EXPECT().StakingTokenSupply(s.ctx).Return(stakingSupply, nil).Times(1)
	s.stakingKeeper.EXPECT().BondedRatio(s.ctx).Return(bondedRatio, nil).Times(1)

	expectedCoins := sdk.NewCoins(sdk.NewCoin("stake", math.NewInt(20)))

	minter, err := s.mintKeeper.Minter.Get(s.ctx)
	s.Require().NoError(err)
	expectedInflation := types.DefaultInflationCalculationFn(context.TODO(), minter, types.DefaultParams(), bondedRatio)

	// Set bank keeper expectations for minting and fee collection.
	s.bankKeeper.EXPECT().MintCoins(s.ctx, types.ModuleName, expectedCoins).Return(nil).Times(1)
	s.bankKeeper.EXPECT().SendCoinsFromModuleToModule(s.ctx, types.ModuleName, authtypes.FeeCollectorName, expectedCoins).Return(nil).Times(1)

	// Call the mint function.
	err = s.mintKeeper.MintFn(s.ctx)
	s.Require().NoError(err)

	// Retrieve the updated minter from storage.
	updatedMinter, err := s.mintKeeper.Minter.Get(s.ctx)
	s.Require().NoError(err)

	// check that minter values are updated as expected
	s.Require().Equal(expectedInflation, updatedMinter.Inflation)
	s.Require().Equal(expectedInflation.MulInt(stakingSupply), updatedMinter.AnnualProvisions)

	// Optionally, verify that a mint event has been emitted.
	events := s.ctx.EventManager().Events()
	s.Require().True(slices.ContainsFunc(events, func(event sdk.Event) bool {
		return event.Type == types.EventTypeMint
	}), "expected a mint event to be emitted")
}

// customMintFn defines a custom minting function that overrides minter behavior.
func customMintFn(ctx sdk.Context, k *keeper.Keeper) error {
	// Retrieve the current minter and parameters.
	minter, err := k.Minter.Get(ctx)
	if err != nil {
		return err
	}
	_, err = k.Params.Get(ctx)
	if err != nil {
		return err
	}

	// Custom logic: override minter values.
	minter.Inflation = math.LegacyMustNewDecFromStr("0.1")
	minter.AnnualProvisions = math.LegacyMustNewDecFromStr("200")
	if err := k.Minter.Set(ctx, minter); err != nil {
		return err
	}

	// Instead of the default block provision, mint a custom coin.
	mintedCoin := sdk.NewCoin("custom", math.NewInt(50))
	mintedCoins := sdk.NewCoins(mintedCoin)

	// Execute bank keeper methods.
	if err := k.MintCoins(ctx, mintedCoins); err != nil {
		return err
	}
	if err := k.AddCollectedFees(ctx, mintedCoins); err != nil {
		return err
	}

	// Emit a custom event.
	sdkCtx := sdk.UnwrapSDKContext(ctx)
	sdkCtx.EventManager().EmitEvent(sdk.NewEvent(
		"custom_mint",
		sdk.NewAttribute("custom_attribute", "true"),
	))

	return nil
}

// TestCustomMintFn tests the custom mint function.
func (s *MintFnTestSuite) TestCustomMintFn() {
	// Reinitialize the keeper with the custom mint function.
	encCfg := moduletestutil.MakeTestEncodingConfig(mint.AppModuleBasic{})
	key := storetypes.NewKVStoreKey(types.StoreKey)
	storeService := runtime.NewKVStoreService(key)
	s.ctx = testutil.DefaultContextWithDB(s.T(), key, storetypes.NewTransientStoreKey("transient_test")).Ctx

	ctrl := gomock.NewController(s.T())
	accountKeeper := minttestutil.NewMockAccountKeeper(ctrl)
	// Use fresh mocks for account keeper if needed.
	accountKeeper.EXPECT().GetModuleAddress(types.ModuleName).Return(sdk.AccAddress{}).AnyTimes()

	// Reuse the existing stakingKeeper and bankKeeper from the suite.
	s.mintKeeper = keeper.NewKeeper(
		encCfg.Codec,
		storeService,
		s.stakingKeeper,
		accountKeeper,
		s.bankKeeper,
		authtypes.FeeCollectorName,
		authtypes.NewModuleAddress(govtypes.ModuleName).String(),
		keeper.WithMintFn(customMintFn),
	)

	// Set default parameters and initial minter.
	err := s.mintKeeper.Params.Set(s.ctx, types.DefaultParams())
	s.Require().NoError(err)
	s.Require().NoError(s.mintKeeper.Minter.Set(s.ctx, types.DefaultInitialMinter()))

	// Expect bank keeper calls to be made for the custom minted coin.
	expectedCoins := sdk.NewCoins(sdk.NewCoin("custom", math.NewInt(50)))
	s.bankKeeper.EXPECT().MintCoins(s.ctx, types.ModuleName, expectedCoins).Return(nil).Times(1)
	s.bankKeeper.EXPECT().SendCoinsFromModuleToModule(s.ctx, types.ModuleName, authtypes.FeeCollectorName, expectedCoins).Return(nil).Times(1)

	// Call the custom mint function.
	err = s.mintKeeper.MintFn(s.ctx)
	s.Require().NoError(err)

	// Retrieve and verify the updated minter values.
	storedMinter, err := s.mintKeeper.Minter.Get(s.ctx)
	s.Require().NoError(err)
	s.Require().Equal(math.LegacyMustNewDecFromStr("0.1"), storedMinter.Inflation)
	s.Require().Equal(math.LegacyMustNewDecFromStr("200"), storedMinter.AnnualProvisions)

	// Check that the custom mint event was emitted.
	events := s.ctx.EventManager().Events()
	s.Require().True(slices.ContainsFunc(events, func(event sdk.Event) bool {
		return event.Type == "custom_mint"
	}), "expected custom_mint event to be emitted")
}
