package keeper_test

import (
	"fmt"
	"testing"

	"github.com/0glabs/0g-chain/app"
	"github.com/0glabs/0g-chain/x/precisebank/keeper"
	"github.com/0glabs/0g-chain/x/precisebank/testutil"
	"github.com/0glabs/0g-chain/x/precisebank/types"
	sdk "github.com/cosmos/cosmos-sdk/types"
	authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
	banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
	minttypes "github.com/cosmos/cosmos-sdk/x/mint/types"
	"github.com/stretchr/testify/suite"
)

type sendIntegrationTestSuite struct {
	testutil.Suite
}

func (suite *sendIntegrationTestSuite) SetupTest() {
	suite.Suite.SetupTest()
}

func TestSendIntegrationTestSuite(t *testing.T) {
	suite.Run(t, new(sendIntegrationTestSuite))
}

func (suite *sendIntegrationTestSuite) TestSendCoinsFromAccountToModule_MatchingErrors() {
	// No specific errors for SendCoinsFromAccountToModule, only 1 panic if
	// the module account does not exist

	tests := []struct {
		name            string
		sender          sdk.AccAddress
		recipientModule string
		sendAmount      sdk.Coins
		wantPanic       string
	}{
		// SendCoinsFromAccountToModule specific errors/panics
		{
			"missing module account - passthrough",
			sdk.AccAddress([]byte{2}),
			"cat",
			cs(c("usdc", 1000)),
			"module account cat does not exist: unknown address",
		},
		{
			"missing module account - extended",
			sdk.AccAddress([]byte{2}),
			"cat",
			cs(c(types.ExtendedCoinDenom, 1000)),
			"module account cat does not exist: unknown address",
		},
	}

	for _, tt := range tests {
		suite.Run(tt.name, func() {
			// Reset
			suite.SetupTest()

			suite.Require().NotEmpty(tt.wantPanic, "test case must have a wantPanic")

			suite.Require().PanicsWithError(tt.wantPanic, func() {
				suite.BankKeeper.SendCoinsFromAccountToModule(suite.Ctx, tt.sender, tt.recipientModule, tt.sendAmount)
			}, "wantPanic should match x/bank SendCoinsFromAccountToModule panic")

			suite.Require().PanicsWithError(tt.wantPanic, func() {
				suite.Keeper.SendCoinsFromAccountToModule(suite.Ctx, tt.sender, tt.recipientModule, tt.sendAmount)
			}, "x/precisebank panic should match x/bank SendCoinsFromAccountToModule panic")
		})
	}
}

func (suite *sendIntegrationTestSuite) TestSendCoinsFromModuleToAccount_MatchingErrors() {
	// Ensure errors match x/bank errors AND panics. This needs to be well
	// tested before SendCoins as all send tests rely on this to initialize
	// account balances.
	// No unit test with mock x/bank for SendCoinsFromModuleToAccount since
	// we only are testing the errors/panics specific to the method and
	// remaining logic is the same as SendCoins.

	blockedMacAddrs := suite.App.GetBlockedMaccAddrs()
	precisebankAddr := suite.AccountKeeper.GetModuleAddress(types.ModuleName)

	var blockedAddr sdk.AccAddress
	// Get the first blocked address
	for addr, isBlocked := range blockedMacAddrs {
		// Skip x/precisebank module account
		if addr == precisebankAddr.String() {
			continue
		}

		if isBlocked {
			blockedAddr = sdk.MustAccAddressFromBech32(addr)
			break
		}
	}

	// We need a ModuleName of another module account to send funds from.
	// x/precisebank is blocked from use with SendCoinsFromModuleToAccount as we
	// don't want external modules to modify x/precisebank balances.
	var senderModuleName string
	macPerms := app.GetMaccPerms()
	for moduleName := range macPerms {
		if moduleName != types.ModuleName {
			senderModuleName = moduleName
		}
	}

	suite.Require().NotEmpty(blockedAddr, "no blocked addresses found")
	suite.Require().NotEmpty(senderModuleName, "no sender module name found")

	tests := []struct {
		name         string
		senderModule string
		recipient    sdk.AccAddress
		sendAmount   sdk.Coins
		wantErr      string
		wantPanic    string
	}{
		// SendCoinsFromModuleToAccount specific errors/panics
		{
			"missing module account - passthrough",
			"cat",
			sdk.AccAddress([]byte{2}),
			cs(c("usdc", 1000)),
			"",
			"module account cat does not exist: unknown address",
		},
		{
			"missing module account - extended",
			"cat",
			sdk.AccAddress([]byte{2}),
			cs(c(types.ExtendedCoinDenom, 1000)),
			"",
			"module account cat does not exist: unknown address",
		},
		{
			"blocked recipient address - passthrough",
			senderModuleName,
			blockedAddr,
			cs(c("usdc", 1000)),
			fmt.Sprintf("%s is not allowed to receive funds: unauthorized", blockedAddr.String()),
			"",
		},
		{
			"blocked recipient address - extended",
			senderModuleName,
			blockedAddr,
			cs(c(types.ExtendedCoinDenom, 1000)),
			fmt.Sprintf("%s is not allowed to receive funds: unauthorized", blockedAddr.String()),
			"",
		},
		// SendCoins specific errors/panics
		{
			"invalid coins",
			senderModuleName,
			sdk.AccAddress([]byte{2}),
			sdk.Coins{sdk.Coin{Denom: "ukava", Amount: sdk.NewInt(-1)}},
			"-1ukava: invalid coins",
			"",
		},
		{
			"insufficient balance - passthrough",
			senderModuleName,
			sdk.AccAddress([]byte{2}),
			cs(c(types.IntegerCoinDenom, 1000)),
			"spendable balance  is smaller than 1000ukava: insufficient funds",
			"",
		},
		{
			"insufficient balance - extended",
			senderModuleName,
			sdk.AccAddress([]byte{2}),
			// We can still test insufficient bal errors with "akava" since
			// we also expect it to not exist in x/bank
			cs(c(types.ExtendedCoinDenom, 1000)),
			"spendable balance  is smaller than 1000akava: insufficient funds",
			"",
		},
	}

	for _, tt := range tests {
		suite.Run(tt.name, func() {
			// Reset
			suite.SetupTest()

			if tt.wantPanic == "" && tt.wantErr == "" {
				suite.FailNow("test case must have a wantErr or wantPanic")
			}

			if tt.wantPanic != "" {
				suite.Require().Empty(tt.wantErr, "test case must not have a wantErr if wantPanic is set")

				suite.Require().PanicsWithError(tt.wantPanic, func() {
					suite.BankKeeper.SendCoinsFromModuleToAccount(suite.Ctx, tt.senderModule, tt.recipient, tt.sendAmount)
				}, "wantPanic should match x/bank SendCoinsFromModuleToAccount panic")

				suite.Require().PanicsWithError(tt.wantPanic, func() {
					suite.Keeper.SendCoinsFromModuleToAccount(suite.Ctx, tt.senderModule, tt.recipient, tt.sendAmount)
				}, "x/precisebank panic should match x/bank SendCoinsFromModuleToAccount panic")
			}

			if tt.wantErr != "" {
				bankErr := suite.BankKeeper.SendCoinsFromModuleToAccount(suite.Ctx, tt.senderModule, tt.recipient, tt.sendAmount)
				suite.Require().Error(bankErr)
				suite.Require().EqualError(bankErr, tt.wantErr, "expected error should match x/bank SendCoins error")

				pbankErr := suite.Keeper.SendCoinsFromModuleToAccount(suite.Ctx, tt.senderModule, tt.recipient, tt.sendAmount)
				suite.Require().Error(pbankErr)
				// Compare strings instead of errors, as error stack is still different
				suite.Require().Equal(
					bankErr.Error(),
					pbankErr.Error(),
					"x/precisebank error should match x/bank SendCoins error",
				)
			}
		})
	}
}

func (suite *sendIntegrationTestSuite) TestSendCoins_MatchingErrors() {
	// Ensure errors match x/bank errors

	tests := []struct {
		name          string
		initialAmount sdk.Coins
		sendAmount    sdk.Coins
		wantErr       string
	}{
		{
			"invalid coins",
			cs(),
			sdk.Coins{sdk.Coin{Denom: "ukava", Amount: sdk.NewInt(-1)}},
			"-1ukava: invalid coins",
		},
		{
			"insufficient empty balance - passthrough",
			cs(),
			cs(c(types.IntegerCoinDenom, 1000)),
			"spendable balance  is smaller than 1000ukava: insufficient funds",
		},
		{
			"insufficient empty balance - extended",
			cs(),
			// We can still test insufficient bal errors with "akava" since
			// we also expect it to not exist in x/bank
			cs(c(types.ExtendedCoinDenom, 1000)),
			"spendable balance  is smaller than 1000akava: insufficient funds",
		},
		{
			"insufficient non-empty balance - passthrough",
			cs(c(types.IntegerCoinDenom, 100), c("usdc", 1000)),
			cs(c(types.IntegerCoinDenom, 1000)),
			"spendable balance 100ukava is smaller than 1000ukava: insufficient funds",
		},
		// non-empty akava transfer error is tested in SendCoins, not here since
		// x/bank doesn't hold akava
	}

	for _, tt := range tests {
		suite.Run(tt.name, func() {
			// Reset
			suite.SetupTest()
			sender := sdk.AccAddress([]byte{1})
			recipient := sdk.AccAddress([]byte{2})

			suite.Require().NotEmpty(tt.wantErr, "test case must have a wantErr")

			suite.MintToAccount(sender, tt.initialAmount)

			bankErr := suite.BankKeeper.SendCoins(suite.Ctx, sender, recipient, tt.sendAmount)
			suite.Require().Error(bankErr)
			suite.Require().EqualError(bankErr, tt.wantErr, "expected error should match x/bank SendCoins error")

			pbankErr := suite.Keeper.SendCoins(suite.Ctx, sender, recipient, tt.sendAmount)
			suite.Require().Error(pbankErr)
			// Compare strings instead of errors, as error stack is still different
			suite.Require().Equal(
				bankErr.Error(),
				pbankErr.Error(),
				"x/precisebank error should match x/bank SendCoins error",
			)
		})
	}
}

func (suite *sendIntegrationTestSuite) TestSendCoins() {
	// SendCoins is tested mostly in this integration test, as a unit test with
	// mocked BankKeeper overcomplicates expected keepers and makes initializing
	// balances very complex.

	tests := []struct {
		name                  string
		giveStartBalSender    sdk.Coins
		giveStartBalRecipient sdk.Coins
		giveAmt               sdk.Coins
		wantErr               string
	}{
		{
			"insufficient balance error denom matches",
			cs(c(types.ExtendedCoinDenom, 10), c("usdc", 1000)),
			cs(),
			cs(c(types.ExtendedCoinDenom, 1000)),
			"spendable balance 10akava is smaller than 1000akava: insufficient funds",
		},
		{
			"passthrough - unrelated",
			cs(c("cats", 1000)),
			cs(),
			cs(c("cats", 1000)),
			"",
		},
		{
			"passthrough - integer denom",
			cs(c(types.IntegerCoinDenom, 1000)),
			cs(),
			cs(c(types.IntegerCoinDenom, 1000)),
			"",
		},
		{
			"passthrough & extended",
			cs(c(types.IntegerCoinDenom, 1000)),
			cs(),
			cs(c(types.IntegerCoinDenom, 10), c(types.ExtendedCoinDenom, 1)),
			"",
		},
		{
			"akava send - 1akava to 0 balance",
			// Starting balances
			cs(ci(types.ExtendedCoinDenom, types.ConversionFactor().MulRaw(5))),
			cs(),
			// Send amount
			cs(c(types.ExtendedCoinDenom, 1)), // akava
			"",
		},
		{
			"sender borrow from integer",
			// 1ukava, 0 fractional
			cs(ci(types.ExtendedCoinDenom, types.ConversionFactor())),
			cs(),
			// Send 1 with 0 fractional balance
			cs(c(types.ExtendedCoinDenom, 1)),
			"",
		},
		{
			"sender borrow from integer - max fractional amount",
			// 1ukava, 0 fractional
			cs(ci(types.ExtendedCoinDenom, types.ConversionFactor())),
			cs(),
			// Max fractional amount
			cs(ci(types.ExtendedCoinDenom, types.ConversionFactor().SubRaw(1))),
			"",
		},
		{
			"receiver carry",
			cs(c(types.ExtendedCoinDenom, 1000)),
			// max fractional amount, carries over to integer
			cs(ci(types.ExtendedCoinDenom, types.ConversionFactor().SubRaw(1))),
			cs(c(types.ExtendedCoinDenom, 1)),
			"",
		},
		{
			"receiver carry - max fractional amount",
			cs(ci(types.ExtendedCoinDenom, types.ConversionFactor().MulRaw(5))),
			// max fractional amount, carries over to integer
			cs(ci(types.ExtendedCoinDenom, types.ConversionFactor().SubRaw(1))),
			cs(ci(types.ExtendedCoinDenom, types.ConversionFactor().SubRaw(1))),
			"",
		},
	}

	for _, tt := range tests {
		suite.Run(tt.name, func() {
			suite.SetupTest()

			sender := sdk.AccAddress([]byte{1})
			recipient := sdk.AccAddress([]byte{2})

			// Initialize balances
			suite.MintToAccount(sender, tt.giveStartBalSender)
			suite.MintToAccount(recipient, tt.giveStartBalRecipient)

			senderBalBefore := suite.GetAllBalances(sender)
			recipientBalBefore := suite.GetAllBalances(recipient)

			err := suite.Keeper.SendCoins(suite.Ctx, sender, recipient, tt.giveAmt)
			if tt.wantErr != "" {
				suite.Require().Error(err)
				suite.Require().EqualError(err, tt.wantErr)
				return
			}

			suite.Require().NoError(err)

			// Check balances
			senderBalAfter := suite.GetAllBalances(sender)
			recipientBalAfter := suite.GetAllBalances(recipient)

			// Convert send amount coins to extended coins. i.e. if send coins
			// includes ukava, convert it so that its the equivalent akava
			// amount so its easier to compare. Compare extended coins only.
			sendAmountFullExtended := tt.giveAmt
			sendAmountInteger := tt.giveAmt.AmountOf(types.IntegerCoinDenom)
			if !sendAmountInteger.IsZero() {
				integerCoin := sdk.NewCoin(types.IntegerCoinDenom, sendAmountInteger)
				sendAmountFullExtended = sendAmountFullExtended.Sub(integerCoin)

				// Add equivalent extended coin
				extendedCoinAmount := sendAmountInteger.Mul(types.ConversionFactor())
				extendedCoin := sdk.NewCoin(types.ExtendedCoinDenom, extendedCoinAmount)
				sendAmountFullExtended = sendAmountFullExtended.Add(extendedCoin)
			}

			suite.Require().Equal(
				senderBalBefore.Sub(sendAmountFullExtended...),
				senderBalAfter,
			)

			suite.Require().Equal(
				recipientBalBefore.Add(sendAmountFullExtended...),
				recipientBalAfter,
			)

			invariantFn := keeper.AllInvariants(suite.Keeper)
			res, stop := invariantFn(suite.Ctx)
			suite.Require().False(stop, "invariants should not stop")
			suite.Require().Empty(res, "invariants should not return any messages")

			// Check events

			// FULL akava equivalent, including ukava only/mixed sends
			sendExtendedAmount := sdk.NewCoin(
				types.ExtendedCoinDenom,
				sendAmountFullExtended.AmountOf(types.ExtendedCoinDenom),
			)
			extCoins := sdk.NewCoins(sendExtendedAmount)

			// No extra events if not sending akava
			if sendExtendedAmount.IsZero() {
				return
			}

			extendedEvent := sdk.NewEvent(
				banktypes.EventTypeTransfer,
				sdk.NewAttribute(banktypes.AttributeKeyRecipient, recipient.String()),
				sdk.NewAttribute(banktypes.AttributeKeySender, sender.String()),
				sdk.NewAttribute(sdk.AttributeKeyAmount, sendExtendedAmount.String()),
			)

			expReceivedEvent := banktypes.NewCoinReceivedEvent(
				recipient,
				extCoins,
			)

			expSentEvent := banktypes.NewCoinSpentEvent(
				sender,
				extCoins,
			)

			events := suite.Ctx.EventManager().Events()

			suite.Require().Contains(events, extendedEvent)
			suite.Require().Contains(events, expReceivedEvent)
			suite.Require().Contains(events, expSentEvent)
		})
	}
}

func (suite *sendIntegrationTestSuite) TestSendCoins_Matrix() {
	// SendCoins is tested mostly in this integration test, as a unit test with
	// mocked BankKeeper overcomplicates expected keepers and makes initializing
	// balances very complex.

	type startBalance struct {
		name string
		bal  sdk.Coins
	}

	// Run through each combination of start sender/recipient balance & send amt
	// Test matrix fields:
	startBalances := []startBalance{
		{"empty", cs()},
		{"integer only", cs(c(types.IntegerCoinDenom, 1000))},
		{"extended only", cs(c(types.ExtendedCoinDenom, 1000))},
		{"integer & extended", cs(c(types.IntegerCoinDenom, 1000), c(types.ExtendedCoinDenom, 1000))},
		{"integer & extended - max fractional", cs(c(types.IntegerCoinDenom, 1000), ci(types.ExtendedCoinDenom, types.ConversionFactor().SubRaw(1)))},
		{"integer & extended - min fractional", cs(c(types.IntegerCoinDenom, 1000), c(types.ExtendedCoinDenom, 1))},
	}

	sendAmts := []struct {
		name string
		amt  sdk.Coins
	}{
		{
			"empty",
			cs(),
		},
		{
			"integer only",
			cs(c(types.IntegerCoinDenom, 10)),
		},
		{
			"extended only",
			cs(c(types.ExtendedCoinDenom, 10)),
		},
		{
			"integer & extended",
			cs(c(types.IntegerCoinDenom, 10), c(types.ExtendedCoinDenom, 1000)),
		},
		{
			"integer & extended - max fractional",
			cs(c(types.IntegerCoinDenom, 10), ci(types.ExtendedCoinDenom, types.ConversionFactor().SubRaw(1))),
		},
		{
			"integer & extended - min fractional",
			cs(c(types.IntegerCoinDenom, 10), c(types.ExtendedCoinDenom, 1)),
		},
	}

	for _, senderStartBal := range startBalances {
		for _, recipientStartBal := range startBalances {
			for _, sendAmt := range sendAmts {
				testName := fmt.Sprintf(
					"%s -> %s (%s -> %s), send %s (%s)",
					senderStartBal.name, senderStartBal.bal,
					recipientStartBal.name, recipientStartBal.bal,
					sendAmt.name, sendAmt.amt,
				)

				suite.Run(testName, func() {
					suite.SetupTest()

					sender := sdk.AccAddress([]byte{1})
					recipient := sdk.AccAddress([]byte{2})

					// Initialize balances
					suite.MintToAccount(sender, senderStartBal.bal)
					suite.MintToAccount(recipient, recipientStartBal.bal)

					// balances & send amount will only contain total equivalent
					// extended coins and no integer coins so its easier to compare
					senderBalBefore := suite.GetAllBalances(sender)
					recipientBalBefore := suite.GetAllBalances(recipient)

					sendAmtNormalized := testutil.ConvertCoinsToExtendedCoinDenom(sendAmt.amt)

					err := suite.Keeper.SendCoins(suite.Ctx, sender, recipient, sendAmt.amt)

					hasSufficientBal := senderBalBefore.IsAllGTE(sendAmtNormalized)

					if hasSufficientBal {
						suite.Require().NoError(err)
					} else {
						suite.Require().Error(err, "expected insufficient funds error")
						// No balance checks if insufficient funds
						return
					}

					// Check balances
					senderBalAfter := suite.GetAllBalances(sender)
					recipientBalAfter := suite.GetAllBalances(recipient)

					// Convert send amount coins to extended coins. i.e. if send coins
					// includes ukava, convert it so that its the equivalent akava
					// amount so its easier to compare. Compare extended coins only.

					suite.Require().Equal(
						senderBalBefore.Sub(sendAmtNormalized...),
						senderBalAfter,
					)

					suite.Require().Equal(
						recipientBalBefore.Add(sendAmtNormalized...),
						recipientBalAfter,
					)

					invariantFn := keeper.AllInvariants(suite.Keeper)
					res, stop := invariantFn(suite.Ctx)
					suite.Require().False(stop, "invariants should not stop")
					suite.Require().Empty(res, "invariants should not return any messages")
				})
			}
		}
	}
}

func (suite *sendIntegrationTestSuite) TestSendCoinsFromAccountToModule() {
	// Ensure recipient correctly matches the specified module account. Specific
	// send amount and cases are handled by SendCoins() tests, so we are only
	// checking SendCoinsFromAccountToModule specific behavior here.

	sender := sdk.AccAddress([]byte{1})
	recipientModule := minttypes.ModuleName
	recipientAddr := suite.AccountKeeper.GetModuleAddress(recipientModule)

	sendAmt := cs(c(types.ExtendedCoinDenom, 1000))

	suite.MintToAccount(sender, sendAmt)

	err := suite.Keeper.SendCoinsFromAccountToModule(
		suite.Ctx,
		sender,
		recipientModule,
		sendAmt,
	)
	suite.Require().NoError(err)

	// Check balances
	senderBalAfter := suite.GetAllBalances(sender)
	recipientBalAfter := suite.GetAllBalances(recipientAddr)

	suite.Require().Equal(
		cs(),
		senderBalAfter,
	)

	suite.Require().Equal(
		sendAmt,
		recipientBalAfter,
	)
}

func (suite *sendIntegrationTestSuite) TestSendCoinsFromAccountToModule_BlockedRecipientCarry() {
	// Carrying to module account balance. This tests that SendCoinsFromAccountToModule
	// does not fail when sending to a blocked module account.

	sender := sdk.AccAddress([]byte{1})

	sendAmt := cs(c(types.ExtendedCoinDenom, 1000))
	sendAmt2 := cs(ci(types.ExtendedCoinDenom, types.ConversionFactor().SubRaw(10)))

	suite.MintToAccount(sender, sendAmt.Add(sendAmt2...))

	err := suite.Keeper.SendCoinsFromAccountToModule(
		suite.Ctx,
		sender,
		authtypes.FeeCollectorName,
		sendAmt,
	)
	suite.Require().NoError(err)

	// Trigger carry for fee_collector module account
	err = suite.Keeper.SendCoinsFromAccountToModule(
		suite.Ctx,
		sender,
		authtypes.FeeCollectorName,
		sendAmt2,
	)
	suite.Require().NoError(err)
}

func (suite *sendIntegrationTestSuite) TestSendCoins_BlockedRecipientCarry() {
	// Same test as TestSendCoinsFromModuleToAccount_Blocked, but with SendCoins
	// which also should not fail when sending to a blocked module account.
	sender := sdk.AccAddress([]byte{1})

	sendAmt := cs(c(types.ExtendedCoinDenom, 1000))
	sendAmt2 := cs(ci(types.ExtendedCoinDenom, types.ConversionFactor().SubRaw(10)))

	suite.MintToAccount(sender, sendAmt.Add(sendAmt2...))

	recipient := suite.App.GetAccountKeeper().GetModuleAddress(authtypes.FeeCollectorName)

	err := suite.Keeper.SendCoins(
		suite.Ctx,
		sender,
		recipient,
		sendAmt,
	)
	suite.Require().NoError(err)

	// Trigger carry for fee_collector module account
	err = suite.Keeper.SendCoins(
		suite.Ctx,
		sender,
		recipient,
		sendAmt2,
	)
	suite.Require().NoError(err)
}

func (suite *sendIntegrationTestSuite) TestSendCoinsFromModuleToAccount() {
	// Ensure sender correctly matches the specified module account. Opposite
	// of SendCoinsFromAccountToModule, so we are only checking the correct
	// addresses are being used.

	senderModule := "community"
	senderAddr := suite.AccountKeeper.GetModuleAddress(senderModule)

	recipient := sdk.AccAddress([]byte{1})

	sendAmt := cs(c(types.ExtendedCoinDenom, 1000))

	suite.MintToAccount(senderAddr, sendAmt)

	err := suite.Keeper.SendCoinsFromModuleToAccount(
		suite.Ctx,
		senderModule,
		recipient,
		sendAmt,
	)
	suite.Require().NoError(err)

	// Check balances
	senderBalAfter := suite.GetAllBalances(senderAddr)
	recipientBalAfter := suite.GetAllBalances(recipient)

	suite.Require().Equal(
		cs(),
		senderBalAfter,
	)

	suite.Require().Equal(
		sendAmt,
		recipientBalAfter,
	)
}

func FuzzSendCoins(f *testing.F) {
	f.Add(uint64(100), uint64(0), uint64(2))
	f.Add(uint64(100), uint64(100), uint64(5))
	f.Add(types.ConversionFactor().Uint64(), uint64(0), uint64(500))
	f.Add(
		types.ConversionFactor().MulRaw(2).AddRaw(123948723).Uint64(),
		types.ConversionFactor().MulRaw(2).Uint64(),
		types.ConversionFactor().Uint64(),
	)

	f.Fuzz(func(
		t *testing.T,
		startBalSender uint64,
		startBalReceiver uint64,
		sendAmount uint64,
	) {
		// Manually setup test suite since no direct Fuzz support in test suites
		suite := new(sendIntegrationTestSuite)
		suite.SetT(t)
		suite.SetS(suite)
		suite.SetupTest()

		sender := sdk.AccAddress([]byte{1})
		recipient := sdk.AccAddress([]byte{2})

		// Initial balances
		suite.MintToAccount(sender, cs(c(types.ExtendedCoinDenom, int64(startBalSender))))
		suite.MintToAccount(recipient, cs(c(types.ExtendedCoinDenom, int64(startBalReceiver))))

		// Send amount
		sendCoins := cs(c(types.ExtendedCoinDenom, int64(sendAmount)))
		err := suite.Keeper.SendCoins(suite.Ctx, sender, recipient, sendCoins)
		if startBalSender < sendAmount {
			suite.Require().Error(err, "expected insufficient funds error")
			return
		}

		suite.Require().NoError(err)

		// Check FULL balances
		balSender := suite.GetAllBalances(sender)
		balReceiver := suite.GetAllBalances(recipient)

		suite.Require().Equal(
			startBalSender-sendAmount,
			balSender.AmountOf(types.ExtendedCoinDenom).Uint64(),
		)
		suite.Require().Equal(
			startBalReceiver+sendAmount,
			balReceiver.AmountOf(types.ExtendedCoinDenom).Uint64(),
		)

		// Run Invariants to ensure remainder is backing all minted fractions
		// and in a valid state
		allInvariantsFn := keeper.AllInvariants(suite.Keeper)
		res, stop := allInvariantsFn(suite.Ctx)
		suite.Require().False(stop, "invariant should not be broken")
		suite.Require().Empty(res, "unexpected invariant message: %s", res)
	})
}