package keeper_test

import (
	"testing"

	"github.com/stretchr/testify/require"

	sdkmath "cosmossdk.io/math"
	sdk "github.com/cosmos/cosmos-sdk/types"
	authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
	minttypes "github.com/cosmos/cosmos-sdk/x/mint/types"

	"github.com/0glabs/0g-chain/x/precisebank/types"
)

func TestMintCoins_PanicValidations(t *testing.T) {
	// panic tests for invalid inputs

	tests := []struct {
		name            string
		recipientModule string
		setupFn         func(td testData)
		mintAmount      sdk.Coins
		wantPanic       string
	}{
		{
			"invalid module",
			"notamodule",
			func(td testData) {
				// Make module not found
				td.ak.EXPECT().
					GetModuleAccount(td.ctx, "notamodule").
					Return(nil).
					Once()
			},
			cs(c("ukava", 1000)),
			"module account notamodule does not exist: unknown address",
		},
		{
			"no permission",
			minttypes.ModuleName,
			func(td testData) {
				td.ak.EXPECT().
					GetModuleAccount(td.ctx, minttypes.ModuleName).
					Return(authtypes.NewModuleAccount(
						authtypes.NewBaseAccountWithAddress(sdk.AccAddress{1}),
						minttypes.ModuleName,
						// no mint permission
					)).
					Once()
			},
			cs(c("ukava", 1000)),
			"module account mint does not have permissions to mint tokens: unauthorized",
		},
		{
			"has mint permission",
			minttypes.ModuleName,
			func(td testData) {
				td.ak.EXPECT().
					GetModuleAccount(td.ctx, minttypes.ModuleName).
					Return(authtypes.NewModuleAccount(
						authtypes.NewBaseAccountWithAddress(sdk.AccAddress{1}),
						minttypes.ModuleName,
						// includes minter permission
						authtypes.Minter,
					)).
					Once()

				// Will call x/bank MintCoins coins
				td.bk.EXPECT().
					MintCoins(td.ctx, minttypes.ModuleName, cs(c("ukava", 1000))).
					Return(nil).
					Once()
			},
			cs(c("ukava", 1000)),
			"",
		},
		{
			"disallow minting to x/precisebank",
			types.ModuleName,
			func(td testData) {
				// No mock setup needed since this is checked before module
				// account checks
			},
			cs(c("ukava", 1000)),
			"module account precisebank cannot be minted to: unauthorized",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			td := NewMockedTestData(t)
			tt.setupFn(td)

			if tt.wantPanic != "" {
				require.PanicsWithError(t, tt.wantPanic, func() {
					_ = td.keeper.MintCoins(td.ctx, tt.recipientModule, tt.mintAmount)
				})
				return
			}

			require.NotPanics(t, func() {
				// Not testing errors, only panics for this test
				_ = td.keeper.MintCoins(td.ctx, tt.recipientModule, tt.mintAmount)
			})
		})
	}
}

func TestMintCoins_Errors(t *testing.T) {
	// returned errors, not panics

	tests := []struct {
		name            string
		recipientModule string
		setupFn         func(td testData)
		mintAmount      sdk.Coins
		wantError       string
	}{
		{
			"invalid coins",
			minttypes.ModuleName,
			func(td testData) {
				// Valid module account minter
				td.ak.EXPECT().
					GetModuleAccount(td.ctx, minttypes.ModuleName).
					Return(authtypes.NewModuleAccount(
						authtypes.NewBaseAccountWithAddress(sdk.AccAddress{1}),
						minttypes.ModuleName,
						// includes minter permission
						authtypes.Minter,
					)).
					Once()
			},
			sdk.Coins{sdk.Coin{
				Denom:  "ukava",
				Amount: sdk.NewInt(-1000),
			}},
			"-1000ukava: invalid coins",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			td := NewMockedTestData(t)
			tt.setupFn(td)

			require.NotPanics(t, func() {
				err := td.keeper.MintCoins(td.ctx, tt.recipientModule, tt.mintAmount)

				if tt.wantError != "" {
					require.Error(t, err)
					require.EqualError(t, err, tt.wantError)
					return
				}

				require.NoError(t, err)
			})
		})
	}
}

func TestMintCoins_ExpectedCalls(t *testing.T) {
	// Tests the expected calls to the bank keeper when minting coins

	tests := []struct {
		name string
		// Only care about starting fractional balance.
		// MintCoins() doesn't care about the previous integer balance.
		startFractionalBalance sdkmath.Int
		mintAmount             sdk.Coins
		// account x/precisebank balance (fractional amount)
		wantPreciseBalance sdkmath.Int
	}{
		{
			"passthrough mint - integer denom",
			sdkmath.ZeroInt(),
			cs(c("ukava", 1000)),
			sdkmath.ZeroInt(),
		},

		{
			"passthrough mint - unrelated denom",
			sdkmath.ZeroInt(),
			cs(c("meow", 1000)),
			sdkmath.ZeroInt(),
		},
		{
			"no carry - 0 starting fractional",
			sdkmath.ZeroInt(),
			cs(c(types.ExtendedCoinDenom, 1000)),
			sdkmath.NewInt(1000),
		},
		{
			"no carry - non-zero fractional",
			sdkmath.NewInt(1_000_000),
			cs(c(types.ExtendedCoinDenom, 1000)),
			sdkmath.NewInt(1_001_000),
		},
		{
			"fractional carry",
			// max fractional amount
			types.ConversionFactor().SubRaw(1),
			cs(c(types.ExtendedCoinDenom, 1)), // +1 to carry
			sdkmath.ZeroInt(),
		},
		{
			"fractional carry max",
			// max fractional amount + max fractional amount
			types.ConversionFactor().SubRaw(1),
			cs(ci(types.ExtendedCoinDenom, types.ConversionFactor().SubRaw(1))),
			types.ConversionFactor().SubRaw(2),
		},
		{
			"integer with fractional no carry",
			sdkmath.NewInt(1234),
			// mint 100 fractional
			cs(c(types.ExtendedCoinDenom, 100)),
			sdkmath.NewInt(1234 + 100),
		},
		{
			"integer with fractional carry",
			types.ConversionFactor().SubRaw(100),
			// mint 105 fractional to carry
			cs(c(types.ExtendedCoinDenom, 105)),
			sdkmath.NewInt(5),
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			td := NewMockedTestData(t)

			// Set initial fractional balance
			// Initial integer balance doesn't matter for this test
			moduleAddr := sdk.AccAddress{1}
			td.keeper.SetFractionalBalance(
				td.ctx,
				moduleAddr,
				tt.startFractionalBalance,
			)
			fBal := td.keeper.GetFractionalBalance(td.ctx, moduleAddr)
			require.Equal(t, tt.startFractionalBalance, fBal)

			// Always calls GetModuleAccount() to check if module exists &
			// has permission
			td.ak.EXPECT().
				GetModuleAccount(td.ctx, minttypes.ModuleName).
				Return(authtypes.NewModuleAccount(
					authtypes.NewBaseAccountWithAddress(
						moduleAddr,
					),
					minttypes.ModuleName,
					// Include minter permissions - not testing permission in
					// this test
					authtypes.Minter,
				)).
				Once()

			// ----------------------------------------
			// Separate passthrough and extended coins
			// Determine how much is passed through to x/bank
			passthroughCoins := tt.mintAmount

			found, extCoins := tt.mintAmount.Find(types.ExtendedCoinDenom)
			if found {
				// Remove extended coin from passthrough coins
				passthroughCoins = passthroughCoins.Sub(extCoins)
			} else {
				extCoins = sdk.NewCoin(types.ExtendedCoinDenom, sdkmath.ZeroInt())
			}

			require.Equalf(
				t,
				sdkmath.ZeroInt(),
				passthroughCoins.AmountOf(types.ExtendedCoinDenom),
				"expected pass through coins should not include %v",
				types.ExtendedCoinDenom,
			)

			// ----------------------------------------
			// Set expectations for minting passthrough coins
			// Only expect MintCoins to be called with passthrough coins with non-zero amount
			if !passthroughCoins.Empty() {
				t.Logf("Expecting MintCoins(%v)", passthroughCoins)

				td.bk.EXPECT().
					MintCoins(td.ctx, minttypes.ModuleName, passthroughCoins).
					Return(nil).
					Once()
			}

			// ----------------------------------------
			// Set expectations for reserve minting when fractional amounts
			// are minted & remainder is insufficient
			mintFractionalAmount := extCoins.Amount.Mod(types.ConversionFactor())
			currentRemainder := td.keeper.GetRemainderAmount(td.ctx)

			causesIntegerCarry := fBal.Add(mintFractionalAmount).GTE(types.ConversionFactor())
			remainderEnough := currentRemainder.GTE(mintFractionalAmount)

			// Optimization: Carry & insufficient remainder is directly minted
			if causesIntegerCarry && !remainderEnough {
				extCoins = extCoins.AddAmount(types.ConversionFactor())
			}

			// ----------------------------------------
			// Set expectations for minting fractional coins
			if !extCoins.IsNil() && extCoins.IsPositive() {
				td.ak.EXPECT().
					GetModuleAddress(minttypes.ModuleName).
					Return(moduleAddr).
					Once()

				// Initial integer balance is always 0 for this test
				mintIntegerAmount := extCoins.Amount.Quo(types.ConversionFactor())

				// Minted coins does NOT include roll-over, simply excludes
				mintCoins := cs(ci(types.IntegerCoinDenom, mintIntegerAmount))

				// Only expect MintCoins to be called with mint coins with
				// non-zero amount.
				// Will fail if x/bank MintCoins is called with empty coins
				if !mintCoins.Empty() {
					t.Logf("Expecting MintCoins(%v)", mintCoins)

					td.bk.EXPECT().
						MintCoins(td.ctx, minttypes.ModuleName, mintCoins).
						Return(nil).
						Once()
				}
			}

			if causesIntegerCarry && remainderEnough {
				td.bk.EXPECT().
					SendCoinsFromModuleToModule(
						td.ctx,
						types.ModuleName,
						minttypes.ModuleName,
						cs(c(types.IntegerCoinDenom, 1)),
					).
					Return(nil).
					Once()
			}

			if !remainderEnough && !causesIntegerCarry {
				reserveMintCoins := cs(c(types.IntegerCoinDenom, 1))
				td.bk.EXPECT().
					// Mints to x/precisebank
					MintCoins(td.ctx, types.ModuleName, reserveMintCoins).
					Return(nil).
					Once()
			}

			// ----------------------------------------
			// Actual call after all setup and expectations
			require.NotPanics(t, func() {
				err := td.keeper.MintCoins(td.ctx, minttypes.ModuleName, tt.mintAmount)
				require.NoError(t, err)
			})

			// Check final fractional balance
			fBal = td.keeper.GetFractionalBalance(td.ctx, moduleAddr)
			require.Equal(t, tt.wantPreciseBalance, fBal)
		})
	}
}