package keeper_test

import (
	icatypes "git.cw.tr/mukan-network/mukan-ibc/modules/apps/27-interchain-accounts/types"
	connectiontypes "git.cw.tr/mukan-network/mukan-ibc/modules/core/03-connection/types"
	channeltypes "git.cw.tr/mukan-network/mukan-ibc/modules/core/04-channel/types"
	ibcerrors "git.cw.tr/mukan-network/mukan-ibc/modules/core/errors"
	ibctesting "git.cw.tr/mukan-network/mukan-ibc/testing"
)

func (suite *KeeperTestSuite) TestOnChanOpenInit() {
	for _, ordering := range []channeltypes.Order{channeltypes.UNORDERED, channeltypes.ORDERED} {
		var (
			channel         *channeltypes.Channel
			path            *ibctesting.Path
			metadata        icatypes.Metadata
			expectedVersion string
		)

		testCases := []struct {
			name     string
			malleate func()
			expError error
		}{
			{
				"success",
				func() {},
				nil,
			},
			{
				"success: previous active channel closed",
				func() {
					suite.chainA.GetSimApp().ICAControllerKeeper.SetActiveChannelID(suite.chainA.GetContext(), ibctesting.FirstConnectionID, path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID)

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

					path.EndpointA.SetChannel(channel)
				},
				nil,
			},
			{
				"success: empty channel version returns default metadata JSON string",
				func() {
					channel.Version = ""
					expectedVersion = icatypes.NewDefaultMetadataString(path.EndpointA.ConnectionID, path.EndpointB.ConnectionID)
				},
				nil,
			},
			{
				"success: channel reopening",
				func() {
					err := SetupICAPath(path, TestOwnerAddress)
					suite.Require().NoError(err)

					path.EndpointA.UpdateChannel(func(channel *channeltypes.Channel) { channel.State = channeltypes.CLOSED })
					path.EndpointB.UpdateChannel(func(channel *channeltypes.Channel) { channel.State = channeltypes.CLOSED })

					path.EndpointA.ChannelID = ""
					path.EndpointB.ChannelID = ""
				},
				nil,
			},
			{
				"failure: different ordering from previous channel",
				func() {
					differentOrdering := channeltypes.UNORDERED
					if ordering == channeltypes.UNORDERED {
						differentOrdering = channeltypes.ORDERED
					}

					suite.chainA.GetSimApp().ICAControllerKeeper.SetActiveChannelID(suite.chainA.GetContext(), ibctesting.FirstConnectionID, path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID)

					counterparty := channeltypes.NewCounterparty(path.EndpointB.ChannelConfig.PortID, path.EndpointB.ChannelID)
					channel := channeltypes.Channel{
						State:          channeltypes.CLOSED,
						Ordering:       differentOrdering,
						Counterparty:   counterparty,
						ConnectionHops: []string{path.EndpointA.ConnectionID},
						Version:        TestVersion,
					}

					path.EndpointA.SetChannel(channel)
				},
				channeltypes.ErrInvalidChannelOrdering,
			},
			{
				"invalid metadata -  previous metadata is different",
				func() {
					// set active channel to closed
					suite.chainA.GetSimApp().ICAControllerKeeper.SetActiveChannelID(suite.chainA.GetContext(), ibctesting.FirstConnectionID, path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID)

					// attempt to downgrade version by reinitializing channel with version 1, but setting channel to version 2
					metadata.Version = "ics27-2"

					versionBytes, err := icatypes.ModuleCdc.MarshalJSON(&metadata)
					suite.Require().NoError(err)

					counterparty := channeltypes.NewCounterparty(path.EndpointB.ChannelConfig.PortID, path.EndpointB.ChannelID)
					closedChannel := channeltypes.Channel{
						State:          channeltypes.CLOSED,
						Ordering:       ordering,
						Counterparty:   counterparty,
						ConnectionHops: []string{path.EndpointA.ConnectionID},
						Version:        string(versionBytes),
					}
					path.EndpointA.SetChannel(closedChannel)
				},
				icatypes.ErrInvalidVersion,
			},
			{
				"invalid port ID",
				func() {
					path.EndpointA.ChannelConfig.PortID = "invalid-port-id" //nolint:goconst
				},
				icatypes.ErrInvalidControllerPort,
			},
			{
				"invalid counterparty port ID",
				func() {
					path.EndpointA.SetChannel(*channel)
					channel.Counterparty.PortId = "invalid-port-id" //nolint:goconst
				},
				icatypes.ErrInvalidHostPort,
			},
			{
				"invalid metadata bytestring",
				func() {
					path.EndpointA.SetChannel(*channel)
					channel.Version = "invalid-metadata-bytestring"
				},
				ibcerrors.ErrInvalidType,
			},
			{
				"unsupported encoding format",
				func() {
					metadata.Encoding = "invalid-encoding-format"

					versionBytes, err := icatypes.ModuleCdc.MarshalJSON(&metadata)
					suite.Require().NoError(err)

					channel.Version = string(versionBytes)
					path.EndpointA.SetChannel(*channel)
				},
				icatypes.ErrInvalidCodec,
			},
			{
				"unsupported transaction type",
				func() {
					metadata.TxType = "invalid-tx-types"

					versionBytes, err := icatypes.ModuleCdc.MarshalJSON(&metadata)
					suite.Require().NoError(err)

					channel.Version = string(versionBytes)
					path.EndpointA.SetChannel(*channel)
				},
				icatypes.ErrUnknownDataType,
			},
			{
				"connection not found",
				func() {
					channel.ConnectionHops = []string{ibctesting.InvalidID}
					path.EndpointA.SetChannel(*channel)
				},
				connectiontypes.ErrConnectionNotFound,
			},
			{
				"connection not found with default empty channel version",
				func() {
					channel.ConnectionHops = []string{"connection-10"}
					channel.Version = ""
				},
				connectiontypes.ErrConnectionNotFound,
			},
			{
				"invalid controller connection ID",
				func() {
					metadata.ControllerConnectionId = ibctesting.InvalidID

					versionBytes, err := icatypes.ModuleCdc.MarshalJSON(&metadata)
					suite.Require().NoError(err)

					channel.Version = string(versionBytes)
					path.EndpointA.SetChannel(*channel)
				},
				connectiontypes.ErrInvalidConnection,
			},
			{
				"invalid host connection ID",
				func() {
					metadata.HostConnectionId = ibctesting.InvalidID

					versionBytes, err := icatypes.ModuleCdc.MarshalJSON(&metadata)
					suite.Require().NoError(err)

					channel.Version = string(versionBytes)
					path.EndpointA.SetChannel(*channel)
				},
				connectiontypes.ErrInvalidConnection,
			},
			{
				"invalid version",
				func() {
					metadata.Version = "invalid-version"

					versionBytes, err := icatypes.ModuleCdc.MarshalJSON(&metadata)
					suite.Require().NoError(err)

					channel.Version = string(versionBytes)
					path.EndpointA.SetChannel(*channel)
				},
				icatypes.ErrInvalidVersion,
			},
			{
				"channel is already active (OPEN state)",
				func() {
					suite.chainA.GetSimApp().ICAControllerKeeper.SetActiveChannelID(suite.chainA.GetContext(), ibctesting.FirstConnectionID, path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID)

					counterparty := channeltypes.NewCounterparty(path.EndpointB.ChannelConfig.PortID, path.EndpointB.ChannelID)
					channel := channeltypes.Channel{
						State:          channeltypes.OPEN,
						Ordering:       ordering,
						Counterparty:   counterparty,
						ConnectionHops: []string{path.EndpointA.ConnectionID},
						Version:        TestVersion,
					}
					suite.chainA.GetSimApp().IBCKeeper.ChannelKeeper.SetChannel(suite.chainA.GetContext(), path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID, channel)
				},
				icatypes.ErrActiveChannelAlreadySet,
			},
		}

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

				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

				// default values
				metadata = icatypes.NewMetadata(icatypes.Version, path.EndpointA.ConnectionID, path.EndpointB.ConnectionID, "", icatypes.EncodingProtobuf, icatypes.TxTypeSDKMultiMsg)
				versionBytes, err := icatypes.ModuleCdc.MarshalJSON(&metadata)
				suite.Require().NoError(err)

				expectedVersion = string(versionBytes)

				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:        string(versionBytes),
				}

				channelID := channeltypes.FormatChannelIdentifier(suite.chainA.GetSimApp().IBCKeeper.ChannelKeeper.GetNextChannelSequence(suite.chainA.GetContext()))
				path.EndpointA.ChannelID = channelID

				tc.malleate() // malleate mutates test data

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

				if tc.expError == nil {
					suite.Require().NoError(err)
					suite.Require().Equal(expectedVersion, version)
				} else {
					suite.Require().Error(err)
					suite.Require().ErrorIs(err, tc.expError)
				}
			})
		}
	}
}

func (suite *KeeperTestSuite) TestOnChanOpenAck() {
	var (
		path     *ibctesting.Path
		metadata icatypes.Metadata
	)

	testCases := []struct {
		name     string
		malleate func()
		expErr   error
	}{
		{
			"success", func() {}, nil,
		},
		{
			"invalid port ID - host chain",
			func() {
				path.EndpointA.ChannelConfig.PortID = icatypes.HostPortID
			},
			icatypes.ErrInvalidControllerPort,
		},
		{
			"invalid port ID - unexpected prefix",
			func() {
				path.EndpointA.ChannelConfig.PortID = "invalid-port-id" //nolint:goconst
			},
			icatypes.ErrInvalidControllerPort,
		},
		{
			"invalid metadata bytestring",
			func() {
				path.EndpointA.Counterparty.ChannelConfig.Version = "invalid-metadata-bytestring"
			},
			ibcerrors.ErrInvalidType,
		},
		{
			"unsupported encoding format",
			func() {
				metadata.Encoding = "invalid-encoding-format"

				versionBytes, err := icatypes.ModuleCdc.MarshalJSON(&metadata)
				suite.Require().NoError(err)

				path.EndpointA.Counterparty.ChannelConfig.Version = string(versionBytes)
			},
			icatypes.ErrInvalidCodec,
		},
		{
			"unsupported transaction type",
			func() {
				metadata.TxType = "invalid-tx-types"

				versionBytes, err := icatypes.ModuleCdc.MarshalJSON(&metadata)
				suite.Require().NoError(err)

				path.EndpointA.Counterparty.ChannelConfig.Version = string(versionBytes)
			},
			icatypes.ErrUnknownDataType,
		},
		{
			"invalid account address",
			func() {
				metadata.Address = "invalid-account-address"

				versionBytes, err := icatypes.ModuleCdc.MarshalJSON(&metadata)
				suite.Require().NoError(err)

				path.EndpointA.Counterparty.ChannelConfig.Version = string(versionBytes)
			},
			icatypes.ErrInvalidAccountAddress,
		},
		{
			"empty account address",
			func() {
				metadata.Address = ""

				versionBytes, err := icatypes.ModuleCdc.MarshalJSON(&metadata)
				suite.Require().NoError(err)

				path.EndpointA.Counterparty.ChannelConfig.Version = string(versionBytes)
			},
			icatypes.ErrInvalidAccountAddress,
		},
		{
			"invalid counterparty version",
			func() {
				metadata.Version = "invalid-version"

				versionBytes, err := icatypes.ModuleCdc.MarshalJSON(&metadata)
				suite.Require().NoError(err)

				path.EndpointA.Counterparty.ChannelConfig.Version = string(versionBytes)
			},
			icatypes.ErrInvalidVersion,
		},
		{
			"active channel already set",
			func() {
				// create a new channel and set it in state
				ch := channeltypes.NewChannel(channeltypes.OPEN, channeltypes.ORDERED, channeltypes.NewCounterparty(path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID), []string{path.EndpointB.ConnectionID}, ibctesting.DefaultChannelVersion)
				suite.chainA.GetSimApp().GetIBCKeeper().ChannelKeeper.SetChannel(suite.chainA.GetContext(), path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID, ch)

				// set the active channelID in state
				suite.chainA.GetSimApp().ICAControllerKeeper.SetActiveChannelID(suite.chainA.GetContext(), ibctesting.FirstConnectionID, path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID)
			}, icatypes.ErrActiveChannelAlreadySet,
		},
	}

	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 := RegisterInterchainAccount(path.EndpointA, TestOwnerAddress)
				suite.Require().NoError(err)

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

				interchainAccAddr, exists := suite.chainB.GetSimApp().ICAHostKeeper.GetInterchainAccountAddress(suite.chainB.GetContext(), path.EndpointB.ConnectionID, path.EndpointA.ChannelConfig.PortID)
				suite.Require().True(exists)

				metadata = icatypes.NewMetadata(icatypes.Version, ibctesting.FirstConnectionID, ibctesting.FirstConnectionID, interchainAccAddr, icatypes.EncodingProtobuf, icatypes.TxTypeSDKMultiMsg)
				versionBytes, err := icatypes.ModuleCdc.MarshalJSON(&metadata)
				suite.Require().NoError(err)

				path.EndpointB.ChannelConfig.Version = string(versionBytes)

				tc.malleate() // malleate mutates test data

				err = suite.chainA.GetSimApp().ICAControllerKeeper.OnChanOpenAck(suite.chainA.GetContext(),
					path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID, path.EndpointA.Counterparty.ChannelConfig.Version,
				)

				if tc.expErr == nil {
					suite.Require().NoError(err)

					activeChannelID, found := suite.chainA.GetSimApp().ICAControllerKeeper.GetActiveChannelID(suite.chainA.GetContext(), ibctesting.FirstConnectionID, path.EndpointA.ChannelConfig.PortID)
					suite.Require().True(found)

					suite.Require().Equal(path.EndpointA.ChannelID, activeChannelID)

					interchainAccAddress, found := suite.chainA.GetSimApp().ICAControllerKeeper.GetInterchainAccountAddress(suite.chainA.GetContext(), ibctesting.FirstConnectionID, path.EndpointA.ChannelConfig.PortID)
					suite.Require().True(found)

					suite.Require().Equal(metadata.Address, interchainAccAddress)
				} else {
					suite.Require().ErrorIs(err, tc.expErr)
				}
			})
		}
	}
}

func (suite *KeeperTestSuite) TestOnChanCloseConfirm() {
	var path *ibctesting.Path

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

	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

				err = suite.chainB.GetSimApp().ICAControllerKeeper.OnChanCloseConfirm(suite.chainB.GetContext(),
					path.EndpointB.ChannelConfig.PortID, path.EndpointB.ChannelID)

				activeChannelID, found := suite.chainB.GetSimApp().ICAControllerKeeper.GetActiveChannelID(suite.chainB.GetContext(), ibctesting.FirstConnectionID, path.EndpointB.ChannelConfig.PortID)

				if tc.expErr == nil {
					suite.Require().NoError(err)
					suite.Require().False(found)
					suite.Require().Empty(activeChannelID)
				} else {
					suite.Require().Error(err)
				}
			})
		}
	}
}
