package ibccallbacks_test

import (
	"fmt"
	"time"

	"github.com/cosmos/gogoproto/proto"

	sdkmath "cosmossdk.io/math"

	sdk "git.cw.tr/mukan-network/mukan-sdk/types"
	stakingtypes "git.cw.tr/mukan-network/mukan-sdk/x/staking/types"

	icacontrollertypes "git.cw.tr/mukan-network/mukan-ibc/modules/apps/27-interchain-accounts/controller/types"
	icahosttypes "git.cw.tr/mukan-network/mukan-ibc/modules/apps/27-interchain-accounts/host/types"
	icatypes "git.cw.tr/mukan-network/mukan-ibc/modules/apps/27-interchain-accounts/types"
	"git.cw.tr/mukan-network/mukan-ibc/modules/apps/callbacks/testing/simapp"
	"git.cw.tr/mukan-network/mukan-ibc/modules/apps/callbacks/types"
	ibctesting "git.cw.tr/mukan-network/mukan-ibc/testing"
)

func (s *CallbacksTestSuite) TestICACallbacks() {
	// Destination callbacks are not supported for ICA packets
	testCases := []struct {
		name        string
		icaMemo     string
		expCallback types.CallbackType
		expSuccess  bool
	}{
		{
			"success: send ica tx with no memo",
			"",
			"none",
			true,
		},
		{
			"success: dest callback",
			fmt.Sprintf(`{"dest_callback": {"address": "%s"}}`, simapp.SuccessContract),
			"none",
			true,
		},
		{
			"success: source callback",
			fmt.Sprintf(`{"src_callback": {"address": "%s"}}`, simapp.SuccessContract),
			types.CallbackTypeAcknowledgementPacket,
			true,
		},
		{
			"success: source callback with other json fields",
			fmt.Sprintf(`{"src_callback": {"address": "%s"}, "something_else": {}}`, simapp.SuccessContract),
			types.CallbackTypeAcknowledgementPacket,
			true,
		},
		{
			"success: source callback with malformed json",
			fmt.Sprintf(`{"src_callback": {"address": "%s"}, malformed}`, simapp.SuccessContract),
			"none",
			true,
		},
		{
			"success: source callback with missing address",
			`{"src_callback": {"address": ""}}`,
			"none",
			true,
		},
		{
			"failure: source callback with low gas (panic)",
			fmt.Sprintf(`{"src_callback": {"address": "%s"}}`, simapp.OogPanicContract),
			types.CallbackTypeSendPacket,
			false,
		},
		{
			"failure: source callback with low gas (error)",
			fmt.Sprintf(`{"src_callback": {"address": "%s"}}`, simapp.OogErrorContract),
			types.CallbackTypeSendPacket,
			false,
		},
	}

	for _, tc := range testCases {
		s.Run(tc.name, func() {
			icaAddr := s.SetupICATest()

			s.ExecuteICATx(icaAddr, tc.icaMemo)
			s.AssertHasExecutedExpectedCallback(tc.expCallback, tc.expSuccess)
		})
	}
}

func (s *CallbacksTestSuite) TestICATimeoutCallbacks() {
	// ICA channels are closed after a timeout packet is executed
	testCases := []struct {
		name        string
		icaMemo     string
		expCallback types.CallbackType
		expSuccess  bool
	}{
		{
			"success: send ica tx timeout with no memo",
			"",
			"none",
			true,
		},
		{
			"success: dest callback",
			fmt.Sprintf(`{"dest_callback": {"address": "%s"}}`, simapp.SuccessContract),
			"none",
			true,
		},
		{
			"success: source callback",
			fmt.Sprintf(`{"src_callback": {"address": "%s"}}`, simapp.SuccessContract),
			types.CallbackTypeTimeoutPacket,
			true,
		},
		{
			"failure: source callback with low gas (panic)",
			fmt.Sprintf(`{"src_callback": {"address": "%s"}}`, simapp.OogPanicContract),
			types.CallbackTypeSendPacket,
			false,
		},
		{
			"failure: source callback with low gas (error)",
			fmt.Sprintf(`{"src_callback": {"address": "%s"}}`, simapp.OogErrorContract),
			types.CallbackTypeSendPacket,
			false,
		},
	}

	for _, tc := range testCases {
		s.Run(tc.name, func() {
			icaAddr := s.SetupICATest()

			s.ExecuteICATimeout(icaAddr, tc.icaMemo)
			s.AssertHasExecutedExpectedCallback(tc.expCallback, tc.expSuccess)
		})
	}
}

// ExecuteICATx executes a stakingtypes.MsgDelegate on chainB by sending a packet containing the msg to chainB
func (s *CallbacksTestSuite) ExecuteICATx(icaAddress, memo string) {
	timeoutTimestamp := uint64(s.chainA.GetContext().BlockTime().Add(time.Minute).UnixNano())
	icaOwner := s.chainA.SenderAccount.GetAddress().String()
	connectionID := s.path.EndpointA.ConnectionID
	// build the interchain accounts packet data
	packetData := s.buildICAMsgDelegatePacketData(icaAddress, memo)
	msg := icacontrollertypes.NewMsgSendTx(icaOwner, connectionID, timeoutTimestamp, packetData)

	res, err := s.chainA.SendMsgs(msg)
	if err != nil {
		return // we return if send packet is rejected
	}

	packet, err := ibctesting.ParseV1PacketFromEvents(res.GetEvents())
	s.Require().NoError(err)

	err = s.path.RelayPacket(packet)
	s.Require().NoError(err)
}

// ExecuteICATimeout sends and times out an ICA tx
func (s *CallbacksTestSuite) ExecuteICATimeout(icaAddress, memo string) {
	relativeTimeout := uint64(1)
	icaOwner := s.chainA.SenderAccount.GetAddress().String()
	connectionID := s.path.EndpointA.ConnectionID
	// build the interchain accounts packet data
	packetData := s.buildICAMsgDelegatePacketData(icaAddress, memo)
	msg := icacontrollertypes.NewMsgSendTx(icaOwner, connectionID, relativeTimeout, packetData)

	res, err := s.chainA.SendMsgs(msg)
	if err != nil {
		return // we return if send packet is rejected
	}

	packet, err := ibctesting.ParseV1PacketFromEvents(res.GetEvents())
	s.Require().NoError(err)

	// proof query requires up to date client
	err = s.path.EndpointA.UpdateClient()
	s.Require().NoError(err)

	err = s.path.EndpointA.TimeoutPacket(packet)
	s.Require().NoError(err)
}

// buildICAMsgDelegatePacketData builds a packetData containing a stakingtypes.MsgDelegate to be executed on chainB
func (s *CallbacksTestSuite) buildICAMsgDelegatePacketData(icaAddress string, memo string) icatypes.InterchainAccountPacketData {
	// prepare a simple stakingtypes.MsgDelegate to be used as the interchain account msg executed on chainB
	validatorAddr := (sdk.ValAddress)(s.chainB.Vals.Validators[0].Address)
	msgDelegate := &stakingtypes.MsgDelegate{
		DelegatorAddress: icaAddress,
		ValidatorAddress: validatorAddr.String(),
		Amount:           sdk.NewCoin(sdk.DefaultBondDenom, sdkmath.NewInt(5000)),
	}

	// ensure chainB is allowed to execute stakingtypes.MsgDelegate
	params := icahosttypes.NewParams(true, []string{sdk.MsgTypeURL(msgDelegate)})
	GetSimApp(s.chainB).ICAHostKeeper.SetParams(s.chainB.GetContext(), params)

	data, err := icatypes.SerializeCosmosTx(GetSimApp(s.chainA).AppCodec(), []proto.Message{msgDelegate}, icatypes.EncodingProtobuf)
	s.Require().NoError(err)

	icaPacketData := icatypes.InterchainAccountPacketData{
		Type: icatypes.EXECUTE_TX,
		Data: data,
		Memo: memo,
	}

	return icaPacketData
}
