package ante_test

import (
	"errors"
	"testing"
	"time"

	"github.com/stretchr/testify/require"
	testifysuite "github.com/stretchr/testify/suite"

	codectypes "git.cw.tr/mukan-network/mukan-sdk/codec/types"
	sdk "git.cw.tr/mukan-network/mukan-sdk/types"

	clienttypes "git.cw.tr/mukan-network/mukan-ibc/modules/core/02-client/types"
	channeltypes "git.cw.tr/mukan-network/mukan-ibc/modules/core/04-channel/types"
	channeltypesv2 "git.cw.tr/mukan-network/mukan-ibc/modules/core/04-channel/v2/types"
	commitmenttypes "git.cw.tr/mukan-network/mukan-ibc/modules/core/23-commitment/types"
	host "git.cw.tr/mukan-network/mukan-ibc/modules/core/24-host"
	hostv2 "git.cw.tr/mukan-network/mukan-ibc/modules/core/24-host/v2"
	"git.cw.tr/mukan-network/mukan-ibc/modules/core/ante"
	"git.cw.tr/mukan-network/mukan-ibc/modules/core/exported"
	ibctm "git.cw.tr/mukan-network/mukan-ibc/modules/light-clients/07-tendermint"
	ibctesting "git.cw.tr/mukan-network/mukan-ibc/testing"
	"git.cw.tr/mukan-network/mukan-ibc/testing/mock/v2"
)

type AnteTestSuite struct {
	testifysuite.Suite

	coordinator *ibctesting.Coordinator

	// testing chains used for convenience and readability
	chainA *ibctesting.TestChain
	chainB *ibctesting.TestChain

	path *ibctesting.Path
}

// SetupTest creates a coordinator with 2 test chains.
func (suite *AnteTestSuite) SetupTest() {
	suite.coordinator = ibctesting.NewCoordinator(suite.T(), 2)
	suite.chainA = suite.coordinator.GetChain(ibctesting.GetChainID(1))
	suite.chainB = suite.coordinator.GetChain(ibctesting.GetChainID(2))
	// commit some blocks so that QueryProof returns valid proof (cannot return valid query if height <= 1)
	suite.coordinator.CommitNBlocks(suite.chainA, 2)
	suite.coordinator.CommitNBlocks(suite.chainB, 2)
	suite.path = ibctesting.NewPath(suite.chainA, suite.chainB)
	suite.path.Setup()
}

// TestAnteTestSuite runs all the tests within this package.
func TestAnteTestSuite(t *testing.T) {
	testifysuite.Run(t, new(AnteTestSuite))
}

// createRecvPacketMessage creates a RecvPacket message for a packet sent from chain A to chain B.
func (suite *AnteTestSuite) createRecvPacketMessage(isRedundant bool) *channeltypes.MsgRecvPacket {
	sequence, err := suite.path.EndpointA.SendPacket(clienttypes.NewHeight(2, 0), 0, ibctesting.MockPacketData)
	suite.Require().NoError(err)

	packet := channeltypes.NewPacket(ibctesting.MockPacketData, sequence,
		suite.path.EndpointA.ChannelConfig.PortID, suite.path.EndpointA.ChannelID,
		suite.path.EndpointB.ChannelConfig.PortID, suite.path.EndpointB.ChannelID,
		clienttypes.NewHeight(2, 0), 0)

	if isRedundant {
		err = suite.path.EndpointB.RecvPacket(packet)
		suite.Require().NoError(err)
	}

	err = suite.path.EndpointB.UpdateClient()
	suite.Require().NoError(err)

	packetKey := host.PacketCommitmentKey(packet.GetSourcePort(), packet.GetSourceChannel(), packet.GetSequence())
	proof, proofHeight := suite.chainA.QueryProof(packetKey)

	return channeltypes.NewMsgRecvPacket(packet, proof, proofHeight, suite.path.EndpointA.Chain.SenderAccount.GetAddress().String())
}

// createRecvPacketMessageV2 creates a V2 RecvPacket message for a packet sent from chain A to chain B.
func (suite *AnteTestSuite) createRecvPacketMessageV2(isRedundant bool) *channeltypesv2.MsgRecvPacket {
	packet, err := suite.path.EndpointA.MsgSendPacket(suite.chainA.GetTimeoutTimestampSecs(), mock.NewMockPayload(mock.ModuleNameA, mock.ModuleNameB))
	suite.Require().NoError(err)

	if isRedundant {
		err = suite.path.EndpointB.MsgRecvPacket(packet)
		suite.Require().NoError(err)
	}

	err = suite.path.EndpointB.UpdateClient()
	suite.Require().NoError(err)

	packetKey := hostv2.PacketCommitmentKey(packet.SourceClient, packet.Sequence)
	proof, proofHeight := suite.chainA.QueryProof(packetKey)

	return channeltypesv2.NewMsgRecvPacket(packet, proof, proofHeight, suite.path.EndpointA.Chain.SenderAccount.GetAddress().String())
}

// createAcknowledgementMessage creates an Acknowledgement message for a packet sent from chain B to chain A.
func (suite *AnteTestSuite) createAcknowledgementMessage(isRedundant bool) sdk.Msg {
	sequence, err := suite.path.EndpointB.SendPacket(clienttypes.NewHeight(2, 0), 0, ibctesting.MockPacketData)
	suite.Require().NoError(err)

	packet := channeltypes.NewPacket(ibctesting.MockPacketData, sequence,
		suite.path.EndpointB.ChannelConfig.PortID, suite.path.EndpointB.ChannelID,
		suite.path.EndpointA.ChannelConfig.PortID, suite.path.EndpointA.ChannelID,
		clienttypes.NewHeight(2, 0), 0)
	err = suite.path.EndpointA.RecvPacket(packet)
	suite.Require().NoError(err)

	if isRedundant {
		err = suite.path.EndpointB.AcknowledgePacket(packet, ibctesting.MockAcknowledgement)
		suite.Require().NoError(err)
	}

	packetKey := host.PacketAcknowledgementKey(packet.GetDestPort(), packet.GetDestChannel(), packet.GetSequence())
	proof, proofHeight := suite.chainA.QueryProof(packetKey)

	return channeltypes.NewMsgAcknowledgement(packet, ibctesting.MockAcknowledgement, proof, proofHeight, suite.path.EndpointA.Chain.SenderAccount.GetAddress().String())
}

// createAcknowledgementMessageV2 creates a V2 Acknowledgement message for a packet sent from chain B to chain A.
func (suite *AnteTestSuite) createAcknowledgementMessageV2(isRedundant bool) *channeltypesv2.MsgAcknowledgement {
	packet, err := suite.path.EndpointB.MsgSendPacket(suite.chainB.GetTimeoutTimestampSecs(), mock.NewMockPayload(mock.ModuleNameA, mock.ModuleNameB))
	suite.Require().NoError(err)

	err = suite.path.EndpointA.MsgRecvPacket(packet)
	suite.Require().NoError(err)

	ack := channeltypesv2.Acknowledgement{AppAcknowledgements: [][]byte{mock.MockRecvPacketResult.Acknowledgement}}
	if isRedundant {
		err = suite.path.EndpointB.MsgAcknowledgePacket(packet, ack)
		suite.Require().NoError(err)
	}

	packetKey := hostv2.PacketAcknowledgementKey(packet.DestinationClient, packet.Sequence)
	proof, proofHeight := suite.chainA.QueryProof(packetKey)

	return channeltypesv2.NewMsgAcknowledgement(packet, ack, proof, proofHeight, suite.path.EndpointA.Chain.SenderAccount.GetAddress().String())
}

// createTimeoutMessage creates an Timeout message for a packet sent from chain B to chain A.
func (suite *AnteTestSuite) createTimeoutMessage(isRedundant bool) sdk.Msg {
	height := suite.chainA.LatestCommittedHeader.GetHeight()
	timeoutHeight := clienttypes.NewHeight(height.GetRevisionNumber(), height.GetRevisionHeight()+1)

	sequence, err := suite.path.EndpointB.SendPacket(timeoutHeight, 0, ibctesting.MockPacketData)
	suite.Require().NoError(err)

	suite.coordinator.CommitNBlocks(suite.chainA, 3)

	err = suite.path.EndpointB.UpdateClient()
	suite.Require().NoError(err)

	packet := channeltypes.NewPacket(ibctesting.MockPacketData, sequence,
		suite.path.EndpointB.ChannelConfig.PortID, suite.path.EndpointB.ChannelID,
		suite.path.EndpointA.ChannelConfig.PortID, suite.path.EndpointA.ChannelID,
		timeoutHeight, 0)

	if isRedundant {
		err = suite.path.EndpointB.TimeoutPacket(packet)
		suite.Require().NoError(err)
	}

	packetKey := host.PacketReceiptKey(packet.GetSourcePort(), packet.GetSourceChannel(), packet.GetSequence())
	proof, proofHeight := suite.chainA.QueryProof(packetKey)

	return channeltypes.NewMsgTimeout(packet, sequence, proof, proofHeight, suite.path.EndpointA.Chain.SenderAccount.GetAddress().String())
}

// createTimeoutMessageV2 creates a V2 Timeout message for a packet sent from chain B to chain A.
func (suite *AnteTestSuite) createTimeoutMessageV2(isRedundant bool) *channeltypesv2.MsgTimeout {
	timeoutTimestamp := uint64(suite.chainB.GetContext().BlockTime().Add(time.Second).Unix())
	packet, err := suite.path.EndpointB.MsgSendPacket(timeoutTimestamp, mock.NewMockPayload(mock.ModuleNameA, mock.ModuleNameB))
	suite.Require().NoError(err)

	suite.coordinator.IncrementTimeBy(time.Hour)
	err = suite.path.EndpointB.UpdateClient()
	suite.Require().NoError(err)

	if isRedundant {
		err = suite.path.EndpointB.MsgTimeoutPacket(packet)
		suite.Require().NoError(err)
	}

	packetKey := hostv2.PacketReceiptKey(packet.SourceClient, packet.Sequence)
	proof, proofHeight := suite.chainA.QueryProof(packetKey)

	return channeltypesv2.NewMsgTimeout(packet, proof, proofHeight, suite.path.EndpointA.Chain.SenderAccount.GetAddress().String())
}

// createTimeoutOnCloseMessage creates an TimeoutOnClose message for a packet sent from chain B to chain A.
func (suite *AnteTestSuite) createTimeoutOnCloseMessage(isRedundant bool) sdk.Msg {
	height := suite.chainA.LatestCommittedHeader.GetHeight()
	timeoutHeight := clienttypes.NewHeight(height.GetRevisionNumber(), height.GetRevisionHeight()+1)

	sequence, err := suite.path.EndpointB.SendPacket(timeoutHeight, 0, ibctesting.MockPacketData)
	suite.Require().NoError(err)
	suite.path.EndpointA.UpdateChannel(func(channel *channeltypes.Channel) { channel.State = channeltypes.CLOSED })

	packet := channeltypes.NewPacket(ibctesting.MockPacketData, sequence,
		suite.path.EndpointB.ChannelConfig.PortID, suite.path.EndpointB.ChannelID,
		suite.path.EndpointA.ChannelConfig.PortID, suite.path.EndpointA.ChannelID,
		timeoutHeight, 0)

	if isRedundant {
		err = suite.path.EndpointB.TimeoutOnClose(packet)
		suite.Require().NoError(err)
	}

	packetKey := host.PacketReceiptKey(packet.GetDestPort(), packet.GetDestChannel(), packet.GetSequence())
	proof, proofHeight := suite.chainA.QueryProof(packetKey)

	channelKey := host.ChannelKey(packet.GetDestPort(), packet.GetDestChannel())
	closedProof, _ := suite.chainA.QueryProof(channelKey)

	return channeltypes.NewMsgTimeoutOnClose(packet, 1, proof, closedProof, proofHeight, suite.path.EndpointA.Chain.SenderAccount.GetAddress().String())
}

func (suite *AnteTestSuite) createUpdateClientMessage() sdk.Msg {
	endpoint := suite.path.EndpointB

	// ensure counterparty has committed state
	endpoint.Chain.Coordinator.CommitBlock(endpoint.Counterparty.Chain)

	var header exported.ClientMessage

	switch endpoint.ClientConfig.GetClientType() {
	case exported.Tendermint:
		trustedHeight := endpoint.GetClientLatestHeight()
		header, _ = endpoint.Counterparty.Chain.IBCClientHeader(endpoint.Counterparty.Chain.LatestCommittedHeader, trustedHeight.(clienttypes.Height))

	default:
	}

	msg, err := clienttypes.NewMsgUpdateClient(
		endpoint.ClientID, header,
		endpoint.Chain.SenderAccount.GetAddress().String(),
	)
	require.NoError(endpoint.Chain.TB, err)

	return msg
}

func (suite *AnteTestSuite) TestAnteDecoratorCheckTx() {
	testCases := []struct {
		name     string
		malleate func(suite *AnteTestSuite) []sdk.Msg
		expError error
	}{
		{
			"success on one new RecvPacket message",
			func(suite *AnteTestSuite) []sdk.Msg {
				// the RecvPacket message has not been submitted to the chain yet, so it will succeed
				return []sdk.Msg{suite.createRecvPacketMessage(false)}
			},
			nil,
		},
		{
			"success on one new V2 RecvPacket message",
			func(suite *AnteTestSuite) []sdk.Msg {
				suite.path.SetupV2()
				// the RecvPacket message has not been submitted to the chain yet, so it will succeed
				return []sdk.Msg{suite.createRecvPacketMessageV2(false)}
			},
			nil,
		},
		{
			"success on one new Acknowledgement message",
			func(suite *AnteTestSuite) []sdk.Msg {
				// the Acknowledgement message has not been submitted to the chain yet, so it will succeed
				return []sdk.Msg{suite.createAcknowledgementMessage(false)}
			},
			nil,
		},
		{
			"success on one new V2 Acknowledgement message",
			func(suite *AnteTestSuite) []sdk.Msg {
				suite.path.SetupV2()
				// the Acknowledgement message has not been submitted to the chain yet, so it will succeed
				return []sdk.Msg{suite.createAcknowledgementMessageV2(false)}
			},
			nil,
		},
		{
			"success on one new Timeout message",
			func(suite *AnteTestSuite) []sdk.Msg {
				// the Timeout message has not been submitted to the chain yet, so it will succeed
				return []sdk.Msg{suite.createTimeoutMessage(false)}
			},
			nil,
		},
		{
			"success on one new Timeout V2 message",
			func(suite *AnteTestSuite) []sdk.Msg {
				suite.path.SetupV2()
				// the Timeout message has not been submitted to the chain yet, so it will succeed
				return []sdk.Msg{suite.createTimeoutMessageV2(false)}
			},
			nil,
		},
		{
			"success on one new TimeoutOnClose message",
			func(suite *AnteTestSuite) []sdk.Msg {
				// the TimeoutOnClose message has not been submitted to the chain yet, so it will succeed
				return []sdk.Msg{suite.createTimeoutOnCloseMessage(false)}
			},
			nil,
		},
		{
			"success on three new messages of each type",
			func(suite *AnteTestSuite) []sdk.Msg {
				var msgs []sdk.Msg

				// none of the messages of each type has been submitted to the chain yet,
				// the first message is succeed and the next two of each type will be rejected
				// because they are redundant.

				// from A to B
				for i := 1; i <= 3; i++ {
					msgs = append(msgs, suite.createRecvPacketMessage(false))
				}

				// from B to A
				for i := 1; i <= 9; i++ {
					switch {
					case i >= 1 && i <= 3:
						msgs = append(msgs, suite.createAcknowledgementMessage(false))
					case i >= 4 && i <= 6:
						msgs = append(msgs, suite.createTimeoutMessage(false))
					case i >= 7 && i <= 9:
						msgs = append(msgs, suite.createTimeoutOnCloseMessage(false))
					}
				}
				return msgs
			},
			nil,
		},
		{
			"success on three redundant messages of RecvPacket, Acknowledgement and TimeoutOnClose, and one new Timeout message",
			func(suite *AnteTestSuite) []sdk.Msg {
				var msgs []sdk.Msg

				// we pass three messages of RecvPacket, Acknowledgement and TimeoutOnClose that
				// are all redundant (i.e. those messages have already been submitted and
				// processed by the chain). But these messages will not be rejected because the
				// Timeout message is new.

				// from A to B
				for i := 1; i <= 3; i++ {
					msgs = append(msgs, suite.createRecvPacketMessage(true))
				}

				// from B to A
				for i := 1; i <= 7; i++ {
					switch {
					case i >= 1 && i <= 3:
						msgs = append(msgs, suite.createAcknowledgementMessage(true))
					case i == 4:
						msgs = append(msgs, suite.createTimeoutMessage(false))
					case i >= 5 && i <= 7:
						msgs = append(msgs, suite.createTimeoutOnCloseMessage(true))
					}
				}
				return msgs
			},
			nil,
		},
		{
			"success on one new message and two redundant messages of each type",
			func(suite *AnteTestSuite) []sdk.Msg {
				var msgs []sdk.Msg

				// For each type there is a new message and two messages that are redundant
				// (i.e. they have been already submitted and processed by the chain). But all
				// the redundant messages will not be rejected because there is a new message
				// of each type.

				// from A to B
				for i := 1; i <= 3; i++ {
					msgs = append(msgs, suite.createRecvPacketMessage(i != 2))
				}

				// from B to A
				for i := 1; i <= 9; i++ {
					switch {
					case i >= 1 && i <= 3:
						msgs = append(msgs, suite.createAcknowledgementMessage(i != 2))
					case i >= 4 && i <= 6:
						msgs = append(msgs, suite.createTimeoutMessage(i != 5))
					case i >= 7 && i <= 9:
						msgs = append(msgs, suite.createTimeoutOnCloseMessage(i != 8))
					}
				}
				return msgs
			},
			nil,
		},
		{
			"success on one new UpdateClient message",
			func(suite *AnteTestSuite) []sdk.Msg {
				return []sdk.Msg{suite.createUpdateClientMessage()}
			},
			nil,
		},
		{
			"success on three new UpdateClient messages",
			func(suite *AnteTestSuite) []sdk.Msg {
				return []sdk.Msg{suite.createUpdateClientMessage(), suite.createUpdateClientMessage(), suite.createUpdateClientMessage()}
			},
			nil,
		},
		{
			"success on three new Updateclient messages and one new RecvPacket message",
			func(suite *AnteTestSuite) []sdk.Msg {
				return []sdk.Msg{
					suite.createUpdateClientMessage(),
					suite.createUpdateClientMessage(),
					suite.createUpdateClientMessage(),
					suite.createRecvPacketMessage(false),
				}
			},
			nil,
		},
		{
			"success on three redundant RecvPacket messages and one SubmitMisbehaviour message",
			func(suite *AnteTestSuite) []sdk.Msg {
				msgs := []sdk.Msg{suite.createUpdateClientMessage()}

				for i := 1; i <= 3; i++ {
					msgs = append(msgs, suite.createRecvPacketMessage(true))
				}

				// append non packet and update message to msgs to ensure multimsg tx should pass
				msgs = append(msgs, &clienttypes.MsgSubmitMisbehaviour{}) //nolint:staticcheck // we're using the deprecated message for testing
				return msgs
			},
			nil,
		},
		{
			"success on app callback error, app callbacks are skipped for performance",
			func(suite *AnteTestSuite) []sdk.Msg {
				suite.chainB.GetSimApp().IBCMockModule.IBCApp.OnRecvPacket = func(
					ctx sdk.Context, channelVersion string, packet channeltypes.Packet, relayer sdk.AccAddress,
				) exported.Acknowledgement {
					panic(errors.New("failed OnRecvPacket mock callback"))
				}

				// the RecvPacket message has not been submitted to the chain yet, so it will succeed
				return []sdk.Msg{suite.createRecvPacketMessage(false)}
			},
			nil,
		},
		{
			"no success on one redundant RecvPacket message",
			func(suite *AnteTestSuite) []sdk.Msg {
				return []sdk.Msg{suite.createRecvPacketMessage(true)}
			},
			channeltypes.ErrRedundantTx,
		},
		{
			"no success on one redundant V2 RecvPacket message",
			func(suite *AnteTestSuite) []sdk.Msg {
				suite.path.SetupV2()
				return []sdk.Msg{suite.createRecvPacketMessageV2(true)}
			},
			channeltypes.ErrRedundantTx,
		},
		{
			"no success on three redundant messages of each type",
			func(suite *AnteTestSuite) []sdk.Msg {
				var msgs []sdk.Msg

				// from A to B
				for i := 1; i <= 3; i++ {
					msgs = append(msgs, suite.createRecvPacketMessage(true))
				}

				// from B to A
				for i := 1; i <= 9; i++ {
					switch {
					case i >= 1 && i <= 3:
						msgs = append(msgs, suite.createAcknowledgementMessage(true))
					case i >= 4 && i <= 6:
						msgs = append(msgs, suite.createTimeoutMessage(true))
					case i >= 7 && i <= 9:
						msgs = append(msgs, suite.createTimeoutOnCloseMessage(true))
					}
				}
				return msgs
			},
			channeltypes.ErrRedundantTx,
		},
		{
			"no success on one new UpdateClient message and three redundant RecvPacket messages",
			func(suite *AnteTestSuite) []sdk.Msg {
				msgs := []sdk.Msg{suite.createUpdateClientMessage()}

				for i := 1; i <= 3; i++ {
					msgs = append(msgs, suite.createRecvPacketMessage(true))
				}

				return msgs
			},
			channeltypes.ErrRedundantTx,
		},
		{
			"no success on one new UpdateClient message: invalid client identifier",
			func(suite *AnteTestSuite) []sdk.Msg {
				clientMsg, err := codectypes.NewAnyWithValue(&ibctm.Header{})
				suite.Require().NoError(err)

				msgs := []sdk.Msg{&clienttypes.MsgUpdateClient{ClientId: ibctesting.InvalidID, ClientMessage: clientMsg}}
				return msgs
			},
			clienttypes.ErrClientNotActive,
		},
		{
			"no success on one new UpdateClient message: client module not found",
			func(suite *AnteTestSuite) []sdk.Msg {
				clientMsg, err := codectypes.NewAnyWithValue(&ibctm.Header{})
				suite.Require().NoError(err)

				msgs := []sdk.Msg{&clienttypes.MsgUpdateClient{ClientId: clienttypes.FormatClientIdentifier("08-wasm", 1), ClientMessage: clientMsg}}
				return msgs
			},
			clienttypes.ErrClientNotActive,
		},
		{
			"no success on one new UpdateClient message: no consensus state for trusted height",
			func(suite *AnteTestSuite) []sdk.Msg {
				clientMsg, err := codectypes.NewAnyWithValue(&ibctm.Header{TrustedHeight: clienttypes.NewHeight(1, 10000)})
				suite.Require().NoError(err)

				msgs := []sdk.Msg{&clienttypes.MsgUpdateClient{ClientId: suite.path.EndpointA.ClientID, ClientMessage: clientMsg}}
				return msgs
			},
			clienttypes.ErrConsensusStateNotFound,
		},
		{
			"no success on three new UpdateClient messages and three redundant messages of each type",
			func(suite *AnteTestSuite) []sdk.Msg {
				msgs := []sdk.Msg{suite.createUpdateClientMessage(), suite.createUpdateClientMessage(), suite.createUpdateClientMessage()}

				// from A to B
				for i := 1; i <= 3; i++ {
					msgs = append(msgs, suite.createRecvPacketMessage(true))
				}

				// from B to A
				for i := 1; i <= 9; i++ {
					switch {
					case i >= 1 && i <= 3:
						msgs = append(msgs, suite.createAcknowledgementMessage(true))
					case i >= 4 && i <= 6:
						msgs = append(msgs, suite.createTimeoutMessage(true))
					case i >= 7 && i <= 9:
						msgs = append(msgs, suite.createTimeoutOnCloseMessage(true))
					}
				}
				return msgs
			},
			channeltypes.ErrRedundantTx,
		},
		{
			"no success on one new message and one invalid message",
			func(suite *AnteTestSuite) []sdk.Msg {
				packet := channeltypes.NewPacket(ibctesting.MockPacketData, 2,
					suite.path.EndpointA.ChannelConfig.PortID, suite.path.EndpointA.ChannelID,
					suite.path.EndpointB.ChannelConfig.PortID, suite.path.EndpointB.ChannelID,
					clienttypes.NewHeight(2, 0), 0)

				return []sdk.Msg{
					suite.createRecvPacketMessage(false),
					channeltypes.NewMsgRecvPacket(packet, []byte("proof"), clienttypes.NewHeight(1, 1), "signer"),
				}
			},
			commitmenttypes.ErrInvalidProof,
		},
		{
			"no success on one new message and one redundant message in the same block",
			func(suite *AnteTestSuite) []sdk.Msg {
				msg := suite.createRecvPacketMessage(false)

				// We want to be able to run check tx with the non-redundant message without
				// committing it to a block, so that the when check tx runs with the redundant
				// message they are both in the same block
				k := suite.chainB.App.GetIBCKeeper()
				decorator := ante.NewRedundantRelayDecorator(k)
				checkCtx := suite.chainB.GetContext().WithIsCheckTx(true)
				next := func(ctx sdk.Context, tx sdk.Tx, simulate bool) (newCtx sdk.Context, err error) { return ctx, nil }
				txBuilder := suite.chainB.TxConfig.NewTxBuilder()
				err := txBuilder.SetMsgs([]sdk.Msg{msg}...)
				suite.Require().NoError(err)
				tx := txBuilder.GetTx()

				_, err = decorator.AnteHandle(checkCtx, tx, false, next)
				suite.Require().NoError(err)

				return []sdk.Msg{msg}
			},
			channeltypes.ErrRedundantTx,
		},
	}

	for _, tc := range testCases {
		suite.Run(tc.name, func() {
			// reset suite
			suite.SetupTest()

			k := suite.chainB.App.GetIBCKeeper()
			decorator := ante.NewRedundantRelayDecorator(k)

			msgs := tc.malleate(suite)

			deliverCtx := suite.chainB.GetContext().WithIsCheckTx(false)
			checkCtx := suite.chainB.GetContext().WithIsCheckTx(true)

			// create multimsg tx
			txBuilder := suite.chainB.TxConfig.NewTxBuilder()
			err := txBuilder.SetMsgs(msgs...)
			suite.Require().NoError(err)
			tx := txBuilder.GetTx()

			next := func(ctx sdk.Context, tx sdk.Tx, simulate bool) (newCtx sdk.Context, err error) { return ctx, nil }

			_, err = decorator.AnteHandle(deliverCtx, tx, false, next)
			suite.Require().NoError(err, "antedecorator should not error on DeliverTx")

			_, err = decorator.AnteHandle(checkCtx, tx, false, next)
			if tc.expError == nil {
				suite.Require().NoError(err, "non-strict decorator did not pass as expected")
			} else {
				suite.Require().ErrorIs(err, tc.expError, "non-strict antehandler did not return error as expected")
			}
		})
	}
}

func (suite *AnteTestSuite) TestAnteDecoratorReCheckTx() {
	testCases := []struct {
		name     string
		malleate func(suite *AnteTestSuite) []sdk.Msg
		expError error
	}{
		{
			"success on one new RecvPacket message",
			func(suite *AnteTestSuite) []sdk.Msg {
				// the RecvPacket message has not been submitted to the chain yet, so it will succeed
				return []sdk.Msg{suite.createRecvPacketMessage(false)}
			},
			nil,
		},
		{
			"success on one new V2 RecvPacket message",
			func(suite *AnteTestSuite) []sdk.Msg {
				suite.path.SetupV2()
				// the RecvPacket message has not been submitted to the chain yet, so it will succeed
				return []sdk.Msg{suite.createRecvPacketMessageV2(false)}
			},
			nil,
		},
		{
			"success on one redundant and one new RecvPacket message",
			func(suite *AnteTestSuite) []sdk.Msg {
				return []sdk.Msg{
					suite.createRecvPacketMessage(true),
					suite.createRecvPacketMessage(false),
				}
			},
			nil,
		},
		{
			"success on invalid proof (proof checks occur in checkTx)",
			func(suite *AnteTestSuite) []sdk.Msg {
				msg := suite.createRecvPacketMessage(false)
				msg.ProofCommitment = []byte("invalid-proof")
				return []sdk.Msg{msg}
			},
			nil,
		},
		{
			"success on app callback error, app callbacks are skipped for performance",
			func(suite *AnteTestSuite) []sdk.Msg {
				suite.chainB.GetSimApp().IBCMockModule.IBCApp.OnRecvPacket = func(
					ctx sdk.Context, channelVersion string, packet channeltypes.Packet, relayer sdk.AccAddress,
				) exported.Acknowledgement {
					panic(errors.New("failed OnRecvPacket mock callback"))
				}

				// the RecvPacket message has not been submitted to the chain yet, so it will succeed
				return []sdk.Msg{suite.createRecvPacketMessage(false)}
			},
			nil,
		},
		{
			"no success on one redundant RecvPacket message",
			func(suite *AnteTestSuite) []sdk.Msg {
				return []sdk.Msg{suite.createRecvPacketMessage(true)}
			},
			channeltypes.ErrRedundantTx,
		},
		{
			"no success on one redundant V2 RecvPacket message",
			func(suite *AnteTestSuite) []sdk.Msg {
				suite.path.SetupV2()
				return []sdk.Msg{suite.createRecvPacketMessageV2(true)}
			},
			channeltypes.ErrRedundantTx,
		},
	}

	for _, tc := range testCases {
		suite.Run(tc.name, func() {
			// reset suite
			suite.SetupTest()

			k := suite.chainB.App.GetIBCKeeper()
			decorator := ante.NewRedundantRelayDecorator(k)

			msgs := tc.malleate(suite)

			deliverCtx := suite.chainB.GetContext().WithIsCheckTx(false)
			reCheckCtx := suite.chainB.GetContext().WithIsReCheckTx(true)

			// create multimsg tx
			txBuilder := suite.chainB.TxConfig.NewTxBuilder()
			err := txBuilder.SetMsgs(msgs...)
			suite.Require().NoError(err)
			tx := txBuilder.GetTx()

			next := func(ctx sdk.Context, tx sdk.Tx, simulate bool) (newCtx sdk.Context, err error) { return ctx, nil }

			_, err = decorator.AnteHandle(deliverCtx, tx, false, next)
			suite.Require().NoError(err, "antedecorator should not error on DeliverTx")

			_, err = decorator.AnteHandle(reCheckCtx, tx, false, next)
			if tc.expError == nil {
				suite.Require().NoError(err, "non-strict decorator did not pass as expected")
			} else {
				suite.Require().ErrorIs(err, tc.expError, "non-strict antehandler did not return error as expected")
			}
		})
	}
}
