package keeper_test

import (
	"errors"
	"fmt"

	sdkmath "cosmossdk.io/math"

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

	"git.cw.tr/mukan-network/mukan-ibc/modules/apps/transfer/types"
	ibctesting "git.cw.tr/mukan-network/mukan-ibc/testing"
)

func (suite *KeeperTestSuite) TestQueryDenom() {
	var (
		req      *types.QueryDenomRequest
		expDenom types.Denom
	)

	testCases := []struct {
		msg      string
		malleate func()
		expErr   error
	}{
		{
			"success: correct ibc denom",
			func() {
				expDenom = types.NewDenom(
					"uatom",                                //nolint:goconst
					types.NewHop("transfer", "channelToA"), //nolint:goconst
					types.NewHop("transfer", "channelToB"), //nolint:goconst
				)
				suite.chainA.GetSimApp().TransferKeeper.SetDenom(suite.chainA.GetContext(), expDenom)

				req = &types.QueryDenomRequest{
					Hash: expDenom.IBCDenom(),
				}
			},
			nil,
		},
		{
			"success: correct hex hash",
			func() {
				expDenom = types.NewDenom(
					"uatom",                                //nolint:goconst
					types.NewHop("transfer", "channelToA"), //nolint:goconst
					types.NewHop("transfer", "channelToB"), //nolint:goconst
				)
				suite.chainA.GetSimApp().TransferKeeper.SetDenom(suite.chainA.GetContext(), expDenom)

				req = &types.QueryDenomRequest{
					Hash: expDenom.Hash().String(),
				}
			},
			nil,
		},
		{
			"failure: invalid hash",
			func() {
				req = &types.QueryDenomRequest{
					Hash: "!@#!@#!",
				}
			},
			errors.New("invalid denom trace hash"),
		},
		{
			"failure: not found denom trace",
			func() {
				expDenom = types.NewDenom(
					"uatom",                                //nolint:goconst
					types.NewHop("transfer", "channelToA"), //nolint:goconst
					types.NewHop("transfer", "channelToB"), //nolint:goconst
				)

				req = &types.QueryDenomRequest{
					Hash: expDenom.IBCDenom(),
				}
			},
			errors.New("denomination not found"),
		},
	}

	for _, tc := range testCases {
		suite.Run(fmt.Sprintf("Case %s", tc.msg), func() {
			tc := tc
			suite.SetupTest() // reset

			tc.malleate()
			ctx := suite.chainA.GetContext()

			res, err := suite.chainA.GetSimApp().TransferKeeper.Denom(ctx, req)

			if tc.expErr == nil {
				suite.Require().NoError(err)
				suite.Require().NotNil(res)
				suite.Require().Equal(&expDenom, res.Denom)
			} else {
				ibctesting.RequireErrorIsOrContains(suite.T(), err, tc.expErr, err.Error())
			}
		})
	}
}

func (suite *KeeperTestSuite) TestQueryDenoms() {
	var (
		req       *types.QueryDenomsRequest
		expDenoms = types.Denoms(nil)
	)

	testCases := []struct {
		msg      string
		malleate func()
		expErr   error
	}{
		{
			"empty pagination",
			func() {
				req = &types.QueryDenomsRequest{}
			},
			nil,
		},
		{
			"success",
			func() {
				expDenoms = append(expDenoms, types.NewDenom("uatom"))
				expDenoms = append(expDenoms, types.NewDenom("uatom", types.NewHop("transfer", "channelToB")))
				expDenoms = append(expDenoms, types.NewDenom("uatom", types.NewHop("transfer", "channelToA"), types.NewHop("transfer", "channelToB")))

				for _, trace := range expDenoms {
					suite.chainA.GetSimApp().TransferKeeper.SetDenom(suite.chainA.GetContext(), trace)
				}

				req = &types.QueryDenomsRequest{
					Pagination: &query.PageRequest{
						Limit:      5,
						CountTotal: false,
					},
				}
			},
			nil,
		},
	}

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

			tc.malleate()
			ctx := suite.chainA.GetContext()

			res, err := suite.chainA.GetSimApp().TransferKeeper.Denoms(ctx, req)

			if tc.expErr == nil {
				suite.Require().NoError(err)
				suite.Require().NotNil(res)
				suite.Require().Equal(expDenoms.Sort(), res.Denoms)
			} else {
				ibctesting.RequireErrorIsOrContains(suite.T(), err, tc.expErr, err.Error())
			}
		})
	}
}

func (suite *KeeperTestSuite) TestQueryParams() {
	ctx := suite.chainA.GetContext()
	expParams := types.DefaultParams()
	res, _ := suite.chainA.GetSimApp().TransferKeeper.Params(ctx, &types.QueryParamsRequest{})
	suite.Require().Equal(&expParams, res.Params)
}

func (suite *KeeperTestSuite) TestQueryDenomHash() {
	reqDenom := types.NewDenom("uatom", types.NewHop("transfer", "channelToA"), types.NewHop("transfer", "channelToB"))

	var (
		req     *types.QueryDenomHashRequest
		expHash = reqDenom.Hash().String()
	)

	testCases := []struct {
		msg      string
		malleate func()
		expErr   error
	}{
		{
			"success",
			func() {},
			nil,
		},
		{
			"invalid trace",
			func() {
				req = &types.QueryDenomHashRequest{
					Trace: "transfer%%/channel-1/transfer/channel-1/uatom",
				}
			},
			errors.New("invalid trace"),
		},
		{
			"not found denom trace",
			func() {
				req = &types.QueryDenomHashRequest{
					Trace: "transfer/channelToC/uatom",
				}
			},
			errors.New("denomination not found"),
		},
	}

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

			req = &types.QueryDenomHashRequest{
				Trace: reqDenom.Path(),
			}
			suite.chainA.GetSimApp().TransferKeeper.SetDenom(suite.chainA.GetContext(), reqDenom)

			tc.malleate()
			ctx := suite.chainA.GetContext()

			res, err := suite.chainA.GetSimApp().TransferKeeper.DenomHash(ctx, req)

			if tc.expErr == nil {
				suite.Require().NoError(err)
				suite.Require().NotNil(res)
				suite.Require().Equal(expHash, res.Hash)
			} else {
				ibctesting.RequireErrorIsOrContains(suite.T(), err, tc.expErr, err.Error())
			}
		})
	}
}

func (suite *KeeperTestSuite) TestEscrowAddress() {
	var req *types.QueryEscrowAddressRequest
	var path *ibctesting.Path

	testCases := []struct {
		msg      string
		malleate func()
		expErr   error
	}{
		{
			"success",
			func() {
				req = &types.QueryEscrowAddressRequest{
					PortId:    ibctesting.TransferPort,
					ChannelId: path.EndpointA.ChannelID,
				}
			},
			nil,
		},
		{
			"failure - channel not found",
			func() {
				req = &types.QueryEscrowAddressRequest{
					PortId:    ibctesting.InvalidID,
					ChannelId: ibctesting.FirstChannelID,
				}
			},
			errors.New("channel not found"),
		},
		{
			"failure - empty channelID",
			func() {
				req = &types.QueryEscrowAddressRequest{
					PortId:    ibctesting.TransferPort,
					ChannelId: "",
				}
			},
			errors.New("identifier cannot be blank"),
		},
		{
			"failure - empty portID",
			func() {
				req = &types.QueryEscrowAddressRequest{
					PortId:    "",
					ChannelId: ibctesting.FirstChannelID,
				}
			},
			errors.New("identifier cannot be blank"),
		},
	}

	for _, tc := range testCases {
		suite.Run(fmt.Sprintf("Case %s", tc.msg), func() {
			suite.SetupTest() // reset
			path = ibctesting.NewTransferPath(suite.chainA, suite.chainB)
			path.Setup()

			tc.malleate()
			ctx := suite.chainA.GetContext()

			res, err := suite.chainA.GetSimApp().TransferKeeper.EscrowAddress(ctx, req)

			if tc.expErr == nil {
				suite.Require().NoError(err)
				expected := types.GetEscrowAddress(ibctesting.TransferPort, path.EndpointA.ChannelID).String()
				suite.Require().Equal(expected, res.EscrowAddress)
			} else {
				ibctesting.RequireErrorIsOrContains(suite.T(), err, tc.expErr, err.Error())
			}
		})
	}
}

func (suite *KeeperTestSuite) TestTotalEscrowForDenom() {
	var (
		req             *types.QueryTotalEscrowForDenomRequest
		expEscrowAmount sdkmath.Int
	)

	testCases := []struct {
		msg      string
		malleate func()
		expErr   error
	}{
		{
			"valid native denom with escrow amount < 2^63",
			func() {
				req = &types.QueryTotalEscrowForDenomRequest{
					Denom: sdk.DefaultBondDenom,
				}

				expEscrowAmount = sdkmath.NewInt(100)
				suite.chainA.GetSimApp().TransferKeeper.SetTotalEscrowForDenom(suite.chainA.GetContext(), sdk.NewCoin(sdk.DefaultBondDenom, expEscrowAmount))
			},
			nil,
		},
		{
			"valid ibc denom with escrow amount > 2^63",
			func() {
				denom := types.NewDenom(sdk.DefaultBondDenom, types.NewHop("transfer", "channel-0"))

				suite.chainA.GetSimApp().TransferKeeper.SetDenom(suite.chainA.GetContext(), denom)
				expEscrowAmount, ok := sdkmath.NewIntFromString("100000000000000000000")
				suite.Require().True(ok)
				suite.chainA.GetSimApp().TransferKeeper.SetTotalEscrowForDenom(suite.chainA.GetContext(), sdk.NewCoin(sdk.DefaultBondDenom, expEscrowAmount))

				req = &types.QueryTotalEscrowForDenomRequest{
					Denom: denom.IBCDenom(),
				}
			},
			nil,
		},
		{
			"valid ibc denom treated as native denom",
			func() {
				denom := types.NewDenom(sdk.DefaultBondDenom, types.NewHop("transfer", "channel-0"))

				req = &types.QueryTotalEscrowForDenomRequest{
					Denom: denom.IBCDenom(),
				}
			},
			nil, // denom trace is not found, thus the denom is considered a native token
		},
		{
			"invalid ibc denom treated as valid native denom",
			func() {
				req = &types.QueryTotalEscrowForDenomRequest{
					Denom: "ibc/123",
				}
			},
			nil, // the ibc denom does not contain a valid hash, thus the denom is considered a native token
		},
		{
			"invalid denom",
			func() {
				req = &types.QueryTotalEscrowForDenomRequest{
					Denom: "??𓃠🐾??",
				}
			},
			errors.New("invalid denom"),
		},
	}

	for _, tc := range testCases {
		suite.Run(fmt.Sprintf("Case %s", tc.msg), func() {
			suite.SetupTest() // reset

			expEscrowAmount = sdkmath.ZeroInt()
			tc.malleate()
			ctx := suite.chainA.GetContext()

			res, err := suite.chainA.GetSimApp().TransferKeeper.TotalEscrowForDenom(ctx, req)

			if tc.expErr == nil {
				suite.Require().NoError(err)
				suite.Require().Equal(expEscrowAmount, res.Amount.Amount)
			} else {
				ibctesting.RequireErrorIsOrContains(suite.T(), err, tc.expErr, err.Error())
				suite.Require().Error(err)
			}
		})
	}
}
