package controller_test

import (
	"errors"
	"strconv"
	"testing"

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

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

	"git.cw.tr/mukan-network/mukan-ibc/modules/apps/27-interchain-accounts/controller"
	controllerkeeper "git.cw.tr/mukan-network/mukan-ibc/modules/apps/27-interchain-accounts/controller/keeper"
	"git.cw.tr/mukan-network/mukan-ibc/modules/apps/27-interchain-accounts/controller/types"
	icatypes "git.cw.tr/mukan-network/mukan-ibc/modules/apps/27-interchain-accounts/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"
	porttypes "git.cw.tr/mukan-network/mukan-ibc/modules/core/05-port/types"
	host "git.cw.tr/mukan-network/mukan-ibc/modules/core/24-host"
	ibcerrors "git.cw.tr/mukan-network/mukan-ibc/modules/core/errors"
	ibctesting "git.cw.tr/mukan-network/mukan-ibc/testing"
)

const invalidVersion = "invalid|version"

var (
	// TestOwnerAddress defines a reusable bech32 address for testing purposes
	TestOwnerAddress = "cosmos17dtl0mjt3t77kpuhg2edqzjpszulwhgzuj9ljs"

	// TestPortID defines a reusable port identifier for testing purposes
	TestPortID, _ = icatypes.NewControllerPortID(TestOwnerAddress)

	// TestVersion defines a reusable interchainaccounts version string for testing purposes
	TestVersion = icatypes.NewDefaultMetadataString(ibctesting.FirstConnectionID, ibctesting.FirstConnectionID)
)

type InterchainAccountsTestSuite struct {
	testifysuite.Suite

	coordinator *ibctesting.Coordinator

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

func TestICATestSuite(t *testing.T) {
	testifysuite.Run(t, new(InterchainAccountsTestSuite))
}

func (suite *InterchainAccountsTestSuite) SetupTest() {
	suite.coordinator = ibctesting.NewCoordinator(suite.T(), 3)
	suite.chainA = suite.coordinator.GetChain(ibctesting.GetChainID(1))
	suite.chainB = suite.coordinator.GetChain(ibctesting.GetChainID(2))
	suite.chainC = suite.coordinator.GetChain(ibctesting.GetChainID(3))
}

func NewICAPath(chainA, chainB *ibctesting.TestChain, ordering channeltypes.Order) *ibctesting.Path {
	path := ibctesting.NewPath(chainA, chainB)
	path.EndpointA.ChannelConfig.PortID = icatypes.HostPortID
	path.EndpointB.ChannelConfig.PortID = icatypes.HostPortID
	path.EndpointA.ChannelConfig.Order = ordering
	path.EndpointB.ChannelConfig.Order = ordering
	path.EndpointA.ChannelConfig.Version = TestVersion
	path.EndpointB.ChannelConfig.Version = TestVersion

	return path
}

func RegisterInterchainAccount(endpoint *ibctesting.Endpoint, owner string) error {
	portID, err := icatypes.NewControllerPortID(owner)
	if err != nil {
		return err
	}

	channelSequence := endpoint.Chain.App.GetIBCKeeper().ChannelKeeper.GetNextChannelSequence(endpoint.Chain.GetContext())

	if err := endpoint.Chain.GetSimApp().ICAControllerKeeper.RegisterInterchainAccount(endpoint.Chain.GetContext(), endpoint.ConnectionID, owner, TestVersion, endpoint.ChannelConfig.Order); err != nil {
		return err
	}

	// commit state changes for proof verification
	endpoint.Chain.NextBlock()

	// update port/channel ids
	endpoint.ChannelID = channeltypes.FormatChannelIdentifier(channelSequence)
	endpoint.ChannelConfig.PortID = portID
	endpoint.ChannelConfig.Version = TestVersion

	return nil
}

// SetupICAPath invokes the InterchainAccounts entrypoint and subsequent channel handshake handlers
func SetupICAPath(path *ibctesting.Path, owner string) error {
	if err := RegisterInterchainAccount(path.EndpointA, owner); err != nil {
		return err
	}

	if err := path.EndpointB.ChanOpenTry(); err != nil {
		return err
	}

	if err := path.EndpointA.ChanOpenAck(); err != nil {
		return err
	}

	return path.EndpointB.ChanOpenConfirm()
}

func (suite *InterchainAccountsTestSuite) TestOnChanOpenInit() {
	var (
		channel  *channeltypes.Channel
		isNilApp bool
		path     *ibctesting.Path
	)

	testCases := []struct {
		name     string
		malleate func()
		expErr   error
	}{
		{
			"success", func() {}, nil,
		},
		{
			"ICA auth module modification of channel version is ignored", func() {
				// NOTE: explicitly modify the channel version via the auth module callback,
				// ensuring the expected JSON encoded metadata is not modified upon return
				suite.chainA.GetSimApp().ICAAuthModule.IBCApp.OnChanOpenInit = func(ctx sdk.Context, order channeltypes.Order, connectionHops []string,
					portID, channelID string,
					counterparty channeltypes.Counterparty, version string,
				) (string, error) {
					return "invalid-version", nil
				}
			}, nil,
		},
		{
			"controller submodule disabled", func() {
				suite.chainA.GetSimApp().ICAControllerKeeper.SetParams(suite.chainA.GetContext(), types.NewParams(false))
			}, types.ErrControllerSubModuleDisabled,
		},
		{
			"ICA auth module callback fails", func() {
				suite.chainA.GetSimApp().ICAAuthModule.IBCApp.OnChanOpenInit = func(ctx sdk.Context, order channeltypes.Order, connectionHops []string,
					portID, channelID string,
					counterparty channeltypes.Counterparty, version string,
				) (string, error) {
					return "", errors.New("mock ica auth fails")
				}
			}, errors.New("mock ica auth fails"),
		},
		{
			"nil underlying app", func() {
				isNilApp = true
			}, nil,
		},
		{
			"middleware disabled", func() {
				suite.chainA.GetSimApp().ICAControllerKeeper.DeleteMiddlewareEnabled(suite.chainA.GetContext(), path.EndpointA.ChannelConfig.PortID, path.EndpointA.ConnectionID)

				suite.chainA.GetSimApp().ICAAuthModule.IBCApp.OnChanOpenInit = func(ctx sdk.Context, order channeltypes.Order, connectionHops []string,
					portID, channelID string,
					counterparty channeltypes.Counterparty, version string,
				) (string, error) {
					return "", errors.New("error should be unreachable")
				}
			}, nil,
		},
	}

	for _, ordering := range []channeltypes.Order{channeltypes.UNORDERED, channeltypes.ORDERED} {
		for _, tc := range testCases {
			suite.Run(tc.name, func() {
				suite.SetupTest() // reset
				isNilApp = false

				path = NewICAPath(suite.chainA, suite.chainB, ordering)
				path.SetupConnections()

				// mock init interchain account
				portID, err := icatypes.NewControllerPortID(TestOwnerAddress)
				suite.Require().NoError(err)

				path.EndpointA.ChannelConfig.PortID = portID
				path.EndpointA.ChannelID = ibctesting.FirstChannelID

				suite.chainA.GetSimApp().ICAControllerKeeper.SetMiddlewareEnabled(suite.chainA.GetContext(), path.EndpointA.ChannelConfig.PortID, path.EndpointA.ConnectionID)

				// default values
				counterparty := channeltypes.NewCounterparty(path.EndpointB.ChannelConfig.PortID, path.EndpointB.ChannelID)
				channel = &channeltypes.Channel{
					State:          channeltypes.INIT,
					Ordering:       ordering,
					Counterparty:   counterparty,
					ConnectionHops: []string{path.EndpointA.ConnectionID},
					Version:        path.EndpointA.ChannelConfig.Version,
				}

				tc.malleate() // malleate mutates test data

				// ensure channel on chainA is set in state
				suite.chainA.GetSimApp().IBCKeeper.ChannelKeeper.SetChannel(suite.chainA.GetContext(), path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID, *channel)

				cbs, ok := suite.chainA.App.GetIBCKeeper().PortKeeper.Route(path.EndpointA.ChannelConfig.PortID)
				suite.Require().True(ok)

				if isNilApp {
					cbs = controller.NewIBCMiddleware(suite.chainA.GetSimApp().ICAControllerKeeper)
				}

				version, err := cbs.OnChanOpenInit(suite.chainA.GetContext(), channel.Ordering, channel.ConnectionHops,
					path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID, channel.Counterparty, channel.Version,
				)

				if tc.expErr == nil {
					suite.Require().Equal(TestVersion, version)
					suite.Require().NoError(err)
				} else {
					suite.Require().ErrorContains(err, tc.expErr.Error())
				}
			})
		}
	}
}

// Test initiating a ChanOpenTry using the controller chain instead of the host chain
// ChainA is the controller chain. ChainB creates a controller port as well,
// attempting to trick chainA.
// Sending a MsgChanOpenTry will never reach the application callback due to
// core IBC checks not passing, so a call to the application callback is also
// done directly.
func (suite *InterchainAccountsTestSuite) TestChanOpenTry() {
	for _, ordering := range []channeltypes.Order{channeltypes.UNORDERED, channeltypes.ORDERED} {
		suite.SetupTest() // reset
		path := NewICAPath(suite.chainA, suite.chainB, ordering)
		path.SetupConnections()

		err := RegisterInterchainAccount(path.EndpointA, TestOwnerAddress)
		suite.Require().NoError(err)

		// chainB also creates a controller port
		err = RegisterInterchainAccount(path.EndpointB, TestOwnerAddress)
		suite.Require().NoError(err)

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

		channelKey := host.ChannelKey(path.EndpointB.ChannelConfig.PortID, path.EndpointB.ChannelID)
		initProof, proofHeight := path.EndpointB.Chain.QueryProof(channelKey)

		// use chainA (controller) for ChanOpenTry
		msg := channeltypes.NewMsgChannelOpenTry(path.EndpointA.ChannelConfig.PortID, TestVersion, ordering, []string{path.EndpointA.ConnectionID}, path.EndpointB.ChannelConfig.PortID, path.EndpointB.ChannelID, TestVersion, initProof, proofHeight, icatypes.ModuleName)
		handler := suite.chainA.GetSimApp().MsgServiceRouter().Handler(msg)
		_, err = handler(suite.chainA.GetContext(), msg)

		suite.Require().Error(err)

		cbs, ok := suite.chainA.App.GetIBCKeeper().PortKeeper.Route(path.EndpointB.ChannelConfig.PortID)
		suite.Require().True(ok)

		counterparty := channeltypes.NewCounterparty(path.EndpointB.ChannelConfig.PortID, path.EndpointB.ChannelID)

		version, err := cbs.OnChanOpenTry(
			suite.chainA.GetContext(), path.EndpointA.ChannelConfig.Order, []string{path.EndpointA.ConnectionID},
			path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID,
			counterparty, path.EndpointB.ChannelConfig.Version,
		)
		suite.Require().Error(err)
		suite.Require().Equal("", version)
	}
}

func (suite *InterchainAccountsTestSuite) TestOnChanOpenAck() {
	var (
		path     *ibctesting.Path
		isNilApp bool
	)

	testCases := []struct {
		name     string
		malleate func()
		expErr   error
	}{
		{
			"success", func() {}, nil,
		},
		{
			"controller submodule disabled", func() {
				suite.chainA.GetSimApp().ICAControllerKeeper.SetParams(suite.chainA.GetContext(), types.NewParams(false))
			}, types.ErrControllerSubModuleDisabled,
		},
		{
			"ICA OnChanOpenACK fails - invalid version", func() {
				path.EndpointB.ChannelConfig.Version = invalidVersion
			}, ibcerrors.ErrInvalidType,
		},
		{
			"ICA auth module callback fails", func() {
				suite.chainA.GetSimApp().ICAAuthModule.IBCApp.OnChanOpenAck = func(
					ctx sdk.Context, portID, channelID string, counterpartyChannelID string, counterpartyVersion string,
				) error {
					return errors.New("mock ica auth fails")
				}
			}, errors.New("mock ica auth fails"),
		},
		{
			"nil underlying app", func() {
				isNilApp = true
			}, nil,
		},
		{
			"middleware disabled", func() {
				suite.chainA.GetSimApp().ICAControllerKeeper.DeleteMiddlewareEnabled(suite.chainA.GetContext(), path.EndpointA.ChannelConfig.PortID, path.EndpointA.ConnectionID)

				suite.chainA.GetSimApp().ICAAuthModule.IBCApp.OnChanOpenAck = func(
					ctx sdk.Context, portID, channelID string, counterpartyChannelID string, counterpartyVersion string,
				) error {
					return errors.New("error should be unreachable")
				}
			}, nil,
		},
	}

	for _, ordering := range []channeltypes.Order{channeltypes.UNORDERED, channeltypes.ORDERED} {
		for _, tc := range testCases {
			suite.Run(tc.name, func() {
				suite.SetupTest() // reset
				isNilApp = false

				path = NewICAPath(suite.chainA, suite.chainB, ordering)
				path.SetupConnections()

				err := RegisterInterchainAccount(path.EndpointA, TestOwnerAddress)
				suite.Require().NoError(err)

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

				tc.malleate() // malleate mutates test data

				cbs, ok := suite.chainA.App.GetIBCKeeper().PortKeeper.Route(path.EndpointA.ChannelConfig.PortID)
				suite.Require().True(ok)

				err = cbs.OnChanOpenAck(suite.chainA.GetContext(), path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID, path.EndpointB.ChannelID, path.EndpointB.ChannelConfig.Version)

				if isNilApp {
					cbs = controller.NewIBCMiddleware(suite.chainA.GetSimApp().ICAControllerKeeper)
				}

				if tc.expErr == nil {
					suite.Require().NoError(err)
				} else {
					suite.Require().ErrorContains(err, tc.expErr.Error())
				}
			})
		}
	}
}

// Test initiating a ChanOpenConfirm using the controller chain instead of the host chain
// ChainA is the controller chain. ChainB is the host chain
// Sending a MsgChanOpenConfirm will never reach the application callback due to
// core IBC checks not passing, so a call to the application callback is also
// done directly.
func (suite *InterchainAccountsTestSuite) TestChanOpenConfirm() {
	for _, ordering := range []channeltypes.Order{channeltypes.UNORDERED, channeltypes.ORDERED} {
		suite.SetupTest() // reset
		path := NewICAPath(suite.chainA, suite.chainB, ordering)
		path.SetupConnections()

		err := RegisterInterchainAccount(path.EndpointA, TestOwnerAddress)
		suite.Require().NoError(err)

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

		// chainB maliciously sets channel to OPEN
		channel := channeltypes.NewChannel(channeltypes.OPEN, ordering, channeltypes.NewCounterparty(path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID), []string{path.EndpointB.ConnectionID}, TestVersion)
		suite.chainB.GetSimApp().GetIBCKeeper().ChannelKeeper.SetChannel(suite.chainB.GetContext(), path.EndpointB.ChannelConfig.PortID, path.EndpointB.ChannelID, channel)

		// commit state changes so proof can be created
		suite.chainB.NextBlock()

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

		// query proof from ChainB
		channelKey := host.ChannelKey(path.EndpointB.ChannelConfig.PortID, path.EndpointB.ChannelID)
		ackProof, proofHeight := path.EndpointB.Chain.QueryProof(channelKey)

		// use chainA (controller) for ChanOpenConfirm
		msg := channeltypes.NewMsgChannelOpenConfirm(path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID, ackProof, proofHeight, icatypes.ModuleName)
		handler := suite.chainA.GetSimApp().MsgServiceRouter().Handler(msg)
		_, err = handler(suite.chainA.GetContext(), msg)

		suite.Require().Error(err)

		cbs, ok := suite.chainA.App.GetIBCKeeper().PortKeeper.Route(path.EndpointA.ChannelConfig.PortID)
		suite.Require().True(ok)

		err = cbs.OnChanOpenConfirm(
			suite.chainA.GetContext(), path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID,
		)
		suite.Require().Error(err)
	}
}

// OnChanCloseInit on controller (chainA)
func (suite *InterchainAccountsTestSuite) TestOnChanCloseInit() {
	for _, ordering := range []channeltypes.Order{channeltypes.UNORDERED, channeltypes.ORDERED} {
		suite.SetupTest() // reset

		path := NewICAPath(suite.chainA, suite.chainB, ordering)
		path.SetupConnections()

		err := SetupICAPath(path, TestOwnerAddress)
		suite.Require().NoError(err)

		cbs, ok := suite.chainA.App.GetIBCKeeper().PortKeeper.Route(path.EndpointA.ChannelConfig.PortID)
		suite.Require().True(ok)

		err = cbs.OnChanCloseInit(
			suite.chainA.GetContext(), path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID,
		)

		suite.Require().Error(err)
	}
}

func (suite *InterchainAccountsTestSuite) TestOnChanCloseConfirm() {
	var (
		path     *ibctesting.Path
		isNilApp bool
	)

	testCases := []struct {
		name     string
		malleate func()
		expErr   error
	}{
		{
			"success", func() {}, nil,
		},
		{
			"nil underlying app", func() {
				isNilApp = true
			}, nil,
		},
	}

	for _, ordering := range []channeltypes.Order{channeltypes.UNORDERED, channeltypes.ORDERED} {
		for _, tc := range testCases {
			suite.Run(tc.name, func() {
				suite.SetupTest() // reset
				isNilApp = false

				path = NewICAPath(suite.chainA, suite.chainB, ordering)
				path.SetupConnections()

				err := SetupICAPath(path, TestOwnerAddress)
				suite.Require().NoError(err)

				tc.malleate() // malleate mutates test data

				cbs, ok := suite.chainA.App.GetIBCKeeper().PortKeeper.Route(path.EndpointA.ChannelConfig.PortID)
				suite.Require().True(ok)

				if isNilApp {
					cbs = controller.NewIBCMiddleware(suite.chainA.GetSimApp().ICAControllerKeeper)
				}

				err = cbs.OnChanCloseConfirm(
					suite.chainA.GetContext(), path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID)

				if tc.expErr == nil {
					suite.Require().NoError(err)
				} else {
					suite.Require().ErrorIs(err, tc.expErr)
				}
			})
		}
	}
}

func (suite *InterchainAccountsTestSuite) TestOnRecvPacket() {
	testCases := []struct {
		name       string
		malleate   func()
		expSuccess bool
	}{
		{
			"ICA OnRecvPacket fails with ErrInvalidChannelFlow", func() {}, false,
		},
	}

	for _, ordering := range []channeltypes.Order{channeltypes.UNORDERED, channeltypes.ORDERED} {
		for _, tc := range testCases {
			suite.Run(tc.name, func() {
				suite.SetupTest() // reset

				path := NewICAPath(suite.chainA, suite.chainB, ordering)
				path.SetupConnections()

				err := SetupICAPath(path, TestOwnerAddress)
				suite.Require().NoError(err)

				tc.malleate() // malleate mutates test data

				cbs, ok := suite.chainA.App.GetIBCKeeper().PortKeeper.Route(path.EndpointA.ChannelConfig.PortID)
				suite.Require().True(ok)

				packet := channeltypes.NewPacket(
					[]byte("empty packet data"),
					suite.chainB.SenderAccount.GetSequence(),
					path.EndpointB.ChannelConfig.PortID,
					path.EndpointB.ChannelID,
					path.EndpointA.ChannelConfig.PortID,
					path.EndpointA.ChannelID,
					clienttypes.NewHeight(0, 100),
					0,
				)

				ctx := suite.chainA.GetContext()
				ack := cbs.OnRecvPacket(ctx, path.EndpointA.GetChannel().Version, packet, nil)
				suite.Require().Equal(tc.expSuccess, ack.Success())

				expectedEvents := sdk.Events{
					sdk.NewEvent(
						icatypes.EventTypePacket,
						sdk.NewAttribute(sdk.AttributeKeyModule, icatypes.ModuleName),
						sdk.NewAttribute(icatypes.AttributeKeyControllerChannelID, packet.GetDestChannel()),
						sdk.NewAttribute(icatypes.AttributeKeyAckSuccess, strconv.FormatBool(false)),
						sdk.NewAttribute(icatypes.AttributeKeyAckError, "cannot receive packet on controller chain: invalid message sent to channel end"),
					),
				}.ToABCIEvents()

				expectedEvents = sdk.MarkEventsToIndex(expectedEvents, map[string]struct{}{})
				ibctesting.AssertEvents(&suite.Suite, expectedEvents, ctx.EventManager().Events().ToABCIEvents())
			})
		}
	}
}

func (suite *InterchainAccountsTestSuite) TestOnAcknowledgementPacket() {
	var (
		path     *ibctesting.Path
		isNilApp bool
	)

	testCases := []struct {
		msg      string
		malleate func()
		expErr   error
	}{
		{
			"success",
			func() {},
			nil,
		},
		{
			"controller submodule disabled", func() {
				suite.chainA.GetSimApp().ICAControllerKeeper.SetParams(suite.chainA.GetContext(), types.NewParams(false))
			}, types.ErrControllerSubModuleDisabled,
		},
		{
			"ICA auth module callback fails", func() {
				suite.chainA.GetSimApp().ICAAuthModule.IBCApp.OnAcknowledgementPacket = func(
					ctx sdk.Context, channelVersion string, packet channeltypes.Packet, acknowledgement []byte, relayer sdk.AccAddress,
				) error {
					return errors.New("mock ica auth fails")
				}
			}, errors.New("mock ica auth fails"),
		},
		{
			"nil underlying app", func() {
				isNilApp = true
			}, nil,
		},
		{
			"middleware disabled", func() {
				suite.chainA.GetSimApp().ICAControllerKeeper.DeleteMiddlewareEnabled(suite.chainA.GetContext(), path.EndpointA.ChannelConfig.PortID, path.EndpointA.ConnectionID)

				suite.chainA.GetSimApp().ICAAuthModule.IBCApp.OnAcknowledgementPacket = func(
					ctx sdk.Context, channelVersion string, packet channeltypes.Packet, acknowledgement []byte, relayer sdk.AccAddress,
				) error {
					return errors.New("error should be unreachable")
				}
			}, nil,
		},
	}

	for _, ordering := range []channeltypes.Order{channeltypes.UNORDERED, channeltypes.ORDERED} {
		for _, tc := range testCases {
			suite.Run(tc.msg, func() {
				suite.SetupTest() // reset
				isNilApp = false

				path = NewICAPath(suite.chainA, suite.chainB, ordering)
				path.SetupConnections()

				err := SetupICAPath(path, TestOwnerAddress)
				suite.Require().NoError(err)

				packet := channeltypes.NewPacket(
					[]byte("empty packet data"),
					suite.chainA.SenderAccount.GetSequence(),
					path.EndpointA.ChannelConfig.PortID,
					path.EndpointA.ChannelID,
					path.EndpointB.ChannelConfig.PortID,
					path.EndpointB.ChannelID,
					clienttypes.NewHeight(0, 100),
					0,
				)

				tc.malleate() // malleate mutates test data

				cbs, ok := suite.chainA.App.GetIBCKeeper().PortKeeper.Route(path.EndpointA.ChannelConfig.PortID)
				suite.Require().True(ok)

				if isNilApp {
					cbs = controller.NewIBCMiddleware(suite.chainA.GetSimApp().ICAControllerKeeper)
				}

				err = cbs.OnAcknowledgementPacket(suite.chainA.GetContext(), path.EndpointA.GetChannel().Version, packet, []byte("ack"), nil)

				if tc.expErr == nil {
					suite.Require().NoError(err)
				} else {
					suite.Require().ErrorContains(err, tc.expErr.Error())
				}
			})
		}
	}
}

func (suite *InterchainAccountsTestSuite) TestOnTimeoutPacket() {
	var (
		path     *ibctesting.Path
		isNilApp bool
	)

	testCases := []struct {
		msg      string
		malleate func()
		expErr   error
	}{
		{
			"success",
			func() {},
			nil,
		},
		{
			"controller submodule disabled", func() {
				suite.chainA.GetSimApp().ICAControllerKeeper.SetParams(suite.chainA.GetContext(), types.NewParams(false))
			}, types.ErrControllerSubModuleDisabled,
		},
		{
			"ICA auth module callback fails", func() {
				suite.chainA.GetSimApp().ICAAuthModule.IBCApp.OnTimeoutPacket = func(
					ctx sdk.Context, channelVersion string, packet channeltypes.Packet, relayer sdk.AccAddress,
				) error {
					return errors.New("mock ica auth fails")
				}
			}, errors.New("mock ica auth fails"),
		},
		{
			"nil underlying app", func() {
				isNilApp = true
			}, nil,
		},
		{
			"middleware disabled", func() {
				suite.chainA.GetSimApp().ICAControllerKeeper.DeleteMiddlewareEnabled(suite.chainA.GetContext(), path.EndpointA.ChannelConfig.PortID, path.EndpointA.ConnectionID)

				suite.chainA.GetSimApp().ICAAuthModule.IBCApp.OnTimeoutPacket = func(
					ctx sdk.Context, channelVersion string, packet channeltypes.Packet, relayer sdk.AccAddress,
				) error {
					return errors.New("error should be unreachable")
				}
			}, nil,
		},
	}

	for _, ordering := range []channeltypes.Order{channeltypes.UNORDERED, channeltypes.ORDERED} {
		for _, tc := range testCases {
			suite.Run(tc.msg, func() {
				suite.SetupTest() // reset
				isNilApp = false

				path = NewICAPath(suite.chainA, suite.chainB, ordering)
				path.SetupConnections()

				err := SetupICAPath(path, TestOwnerAddress)
				suite.Require().NoError(err)

				packet := channeltypes.NewPacket(
					[]byte("empty packet data"),
					suite.chainA.SenderAccount.GetSequence(),
					path.EndpointA.ChannelConfig.PortID,
					path.EndpointA.ChannelID,
					path.EndpointB.ChannelConfig.PortID,
					path.EndpointB.ChannelID,
					clienttypes.NewHeight(0, 100),
					0,
				)

				tc.malleate() // malleate mutates test data

				cbs, ok := suite.chainA.App.GetIBCKeeper().PortKeeper.Route(path.EndpointA.ChannelConfig.PortID)
				suite.Require().True(ok)

				if isNilApp {
					cbs = controller.NewIBCMiddleware(suite.chainA.GetSimApp().ICAControllerKeeper)
				}

				err = cbs.OnTimeoutPacket(suite.chainA.GetContext(), path.EndpointA.GetChannel().Version, packet, nil)

				if tc.expErr == nil {
					suite.Require().NoError(err)
				} else {
					suite.Require().ErrorContains(err, tc.expErr.Error())
				}
			})
		}
	}
}

func (suite *InterchainAccountsTestSuite) TestSingleHostMultipleControllers() {
	var (
		pathAToB *ibctesting.Path
		pathCToB *ibctesting.Path
	)

	testCases := []struct {
		msg      string
		malleate func()
	}{
		{
			"success",
			func() {},
		},
	}

	for _, ordering := range []channeltypes.Order{channeltypes.UNORDERED, channeltypes.ORDERED} {
		for _, tc := range testCases {
			suite.Run(tc.msg, func() {
				// reset
				suite.SetupTest()
				TestVersion = icatypes.NewDefaultMetadataString(ibctesting.FirstConnectionID, ibctesting.FirstConnectionID)

				// Setup a new path from A(controller) -> B(host)
				pathAToB = NewICAPath(suite.chainA, suite.chainB, ordering)
				pathAToB.SetupConnections()

				err := SetupICAPath(pathAToB, TestOwnerAddress)
				suite.Require().NoError(err)

				// Setup a new path from C(controller) -> B(host)
				pathCToB = NewICAPath(suite.chainC, suite.chainB, ordering)
				pathCToB.SetupConnections()

				// NOTE: Here the version metadata is overridden to include to the next host connection sequence (i.e. chainB's connection to chainC)
				// SetupICAPath() will set endpoint.ChannelConfig.Version to TestVersion
				TestVersion = string(icatypes.ModuleCdc.MustMarshalJSON(&icatypes.Metadata{
					Version:                icatypes.Version,
					ControllerConnectionId: pathCToB.EndpointA.ConnectionID,
					HostConnectionId:       pathCToB.EndpointB.ConnectionID,
					Encoding:               icatypes.EncodingProtobuf,
					TxType:                 icatypes.TxTypeSDKMultiMsg,
				}))

				err = SetupICAPath(pathCToB, TestOwnerAddress)
				suite.Require().NoError(err)

				tc.malleate() // malleate mutates test data

				accAddressChainA, found := suite.chainB.GetSimApp().ICAHostKeeper.GetInterchainAccountAddress(suite.chainB.GetContext(), pathAToB.EndpointB.ConnectionID, pathAToB.EndpointA.ChannelConfig.PortID)
				suite.Require().True(found)

				accAddressChainC, found := suite.chainB.GetSimApp().ICAHostKeeper.GetInterchainAccountAddress(suite.chainB.GetContext(), pathCToB.EndpointB.ConnectionID, pathCToB.EndpointA.ChannelConfig.PortID)
				suite.Require().True(found)

				suite.Require().NotEqual(accAddressChainA, accAddressChainC)

				chainAChannelID, found := suite.chainB.GetSimApp().ICAHostKeeper.GetActiveChannelID(suite.chainB.GetContext(), pathAToB.EndpointB.ConnectionID, pathAToB.EndpointA.ChannelConfig.PortID)
				suite.Require().True(found)

				chainCChannelID, found := suite.chainB.GetSimApp().ICAHostKeeper.GetActiveChannelID(suite.chainB.GetContext(), pathCToB.EndpointB.ConnectionID, pathCToB.EndpointA.ChannelConfig.PortID)
				suite.Require().True(found)

				suite.Require().NotEqual(chainAChannelID, chainCChannelID)
			})
		}
	}
}

func (suite *InterchainAccountsTestSuite) TestGetAppVersion() {
	for _, ordering := range []channeltypes.Order{channeltypes.UNORDERED, channeltypes.ORDERED} {
		suite.SetupTest() // reset

		path := NewICAPath(suite.chainA, suite.chainB, ordering)
		path.SetupConnections()

		err := SetupICAPath(path, TestOwnerAddress)
		suite.Require().NoError(err)

		cbs, ok := suite.chainA.App.GetIBCKeeper().PortKeeper.Route(path.EndpointA.ChannelConfig.PortID)
		suite.Require().True(ok)

		controllerStack, ok := cbs.(porttypes.ICS4Wrapper)
		suite.Require().True(ok)

		appVersion, found := controllerStack.GetAppVersion(suite.chainA.GetContext(), path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID)
		suite.Require().True(found)
		suite.Require().Equal(path.EndpointA.ChannelConfig.Version, appVersion)
	}
}

func (suite *InterchainAccountsTestSuite) TestInFlightHandshakeRespectsGoAPICaller() {
	for _, ordering := range []channeltypes.Order{channeltypes.UNORDERED, channeltypes.ORDERED} {
		suite.SetupTest() // reset

		path := NewICAPath(suite.chainA, suite.chainB, ordering)
		path.SetupConnections()

		// initiate a channel handshake such that channel.State == INIT
		err := RegisterInterchainAccount(path.EndpointA, suite.chainA.SenderAccount.GetAddress().String())
		suite.Require().NoError(err)

		// attempt to start a second handshake via the controller msg server
		msgServer := controllerkeeper.NewMsgServerImpl(&suite.chainA.GetSimApp().ICAControllerKeeper)
		msgRegisterInterchainAccount := types.NewMsgRegisterInterchainAccount(path.EndpointA.ConnectionID, suite.chainA.SenderAccount.GetAddress().String(), TestVersion, ordering)

		res, err := msgServer.RegisterInterchainAccount(suite.chainA.GetContext(), msgRegisterInterchainAccount)
		suite.Require().Error(err)
		suite.Require().Nil(res)
	}
}

func (suite *InterchainAccountsTestSuite) TestInFlightHandshakeRespectsMsgServerCaller() {
	for _, ordering := range []channeltypes.Order{channeltypes.UNORDERED, channeltypes.ORDERED} {
		suite.SetupTest() // reset

		path := NewICAPath(suite.chainA, suite.chainB, ordering)
		path.SetupConnections()

		// initiate a channel handshake such that channel.State == INIT
		msgServer := controllerkeeper.NewMsgServerImpl(&suite.chainA.GetSimApp().ICAControllerKeeper)
		msgRegisterInterchainAccount := types.NewMsgRegisterInterchainAccount(path.EndpointA.ConnectionID, suite.chainA.SenderAccount.GetAddress().String(), TestVersion, ordering)

		res, err := msgServer.RegisterInterchainAccount(suite.chainA.GetContext(), msgRegisterInterchainAccount)
		suite.Require().NotNil(res)
		suite.Require().NoError(err)

		// attempt to start a second handshake via the legacy Go API
		err = RegisterInterchainAccount(path.EndpointA, suite.chainA.SenderAccount.GetAddress().String())
		suite.Require().Error(err)
	}
}

func (suite *InterchainAccountsTestSuite) TestClosedChannelReopensWithMsgServer() {
	for _, ordering := range []channeltypes.Order{channeltypes.UNORDERED, channeltypes.ORDERED} {
		suite.SetupTest() // reset

		path := NewICAPath(suite.chainA, suite.chainB, ordering)
		path.SetupConnections()

		err := SetupICAPath(path, suite.chainA.SenderAccount.GetAddress().String())
		suite.Require().NoError(err)

		// set the channel state to closed
		path.EndpointA.UpdateChannel(func(channel *channeltypes.Channel) { channel.State = channeltypes.CLOSED })
		path.EndpointB.UpdateChannel(func(channel *channeltypes.Channel) { channel.State = channeltypes.CLOSED })

		// reset endpoint channel ids
		path.EndpointA.ChannelID = ""
		path.EndpointB.ChannelID = ""

		// fetch the next channel sequence before reinitiating the channel handshake
		channelSeq := suite.chainA.GetSimApp().GetIBCKeeper().ChannelKeeper.GetNextChannelSequence(suite.chainA.GetContext())

		// route a new MsgRegisterInterchainAccount in order to reopen the
		msgServer := controllerkeeper.NewMsgServerImpl(&suite.chainA.GetSimApp().ICAControllerKeeper)
		msgRegisterInterchainAccount := types.NewMsgRegisterInterchainAccount(path.EndpointA.ConnectionID, suite.chainA.SenderAccount.GetAddress().String(), path.EndpointA.ChannelConfig.Version, ordering)

		res, err := msgServer.RegisterInterchainAccount(suite.chainA.GetContext(), msgRegisterInterchainAccount)
		suite.Require().NoError(err)
		suite.Require().Equal(channeltypes.FormatChannelIdentifier(channelSeq), res.ChannelId)

		// assign the channel sequence to endpointA before generating proofs and initiating the TRY step
		path.EndpointA.ChannelID = channeltypes.FormatChannelIdentifier(channelSeq)

		path.EndpointA.Chain.NextBlock()

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

		err = path.EndpointA.ChanOpenAck()
		suite.Require().NoError(err)

		err = path.EndpointB.ChanOpenConfirm()
		suite.Require().NoError(err)
	}
}

func (suite *InterchainAccountsTestSuite) TestPacketDataUnmarshalerInterface() {
	for _, ordering := range []channeltypes.Order{channeltypes.UNORDERED, channeltypes.ORDERED} {
		suite.SetupTest() // reset

		path := NewICAPath(suite.chainA, suite.chainB, ordering)
		path.SetupConnections()
		err := SetupICAPath(path, TestOwnerAddress)
		suite.Require().NoError(err)

		expPacketData := icatypes.InterchainAccountPacketData{
			Type: icatypes.EXECUTE_TX,
			Data: []byte("data"),
			Memo: "",
		}

		controllerMiddleware := controller.NewIBCMiddleware(suite.chainA.GetSimApp().ICAControllerKeeper)
		packetData, version, err := controllerMiddleware.UnmarshalPacketData(suite.chainA.GetContext(), path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID, expPacketData.GetBytes())
		suite.Require().NoError(err)
		suite.Require().Equal(version, path.EndpointA.ChannelConfig.Version)
		suite.Require().Equal(expPacketData, packetData)

		// test invalid packet data
		invalidPacketData := []byte("invalid packet data")
		// Context, port identifier and channel identifier are not used for controller.
		packetData, version, err = controllerMiddleware.UnmarshalPacketData(suite.chainA.GetContext(), path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID, invalidPacketData)
		suite.Require().Error(err)
		suite.Require().Empty(version)
		suite.Require().Nil(packetData)
	}
}
