package ante_test

import (
	"testing"
	"time"

	"github.com/cosmos/cosmos-sdk/simapp/helpers"
	sdk "github.com/cosmos/cosmos-sdk/types"
	sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
	"github.com/cosmos/cosmos-sdk/x/authz"
	banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
	stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
	"github.com/stretchr/testify/require"
	evmtypes "github.com/tharsis/ethermint/x/evm/types"

	"github.com/kava-labs/kava/app"
	"github.com/kava-labs/kava/app/ante"
)

func newMsgGrant(granter sdk.AccAddress, grantee sdk.AccAddress, a authz.Authorization, expiration time.Time) *authz.MsgGrant {
	msg, err := authz.NewMsgGrant(granter, grantee, a, expiration)
	if err != nil {
		panic(err)
	}
	return msg
}

func newMsgExec(grantee sdk.AccAddress, msgs []sdk.Msg) *authz.MsgExec {
	msg := authz.NewMsgExec(grantee, msgs)
	return &msg
}

func TestAuthzLimiterDecorator(t *testing.T) {
	testPrivKeys, testAddresses := app.GeneratePrivKeyAddressPairs(5)
	distantFuture := time.Date(9000, 1, 1, 0, 0, 0, 0, time.UTC)

	validator := sdk.ValAddress(testAddresses[4])
	stakingAuthDelegate, err := stakingtypes.NewStakeAuthorization([]sdk.ValAddress{validator}, nil, stakingtypes.AuthorizationType_AUTHORIZATION_TYPE_DELEGATE, nil)
	require.NoError(t, err)
	stakingAuthUndelegate, err := stakingtypes.NewStakeAuthorization([]sdk.ValAddress{validator}, nil, stakingtypes.AuthorizationType_AUTHORIZATION_TYPE_UNDELEGATE, nil)
	require.NoError(t, err)

	decorator := ante.NewAuthzLimiterDecorator(
		sdk.MsgTypeURL(&evmtypes.MsgEthereumTx{}),
		sdk.MsgTypeURL(&stakingtypes.MsgUndelegate{}),
	)

	testCases := []struct {
		name        string
		msgs        []sdk.Msg
		checkTx     bool
		expectedErr error
	}{
		{
			name: "a non blocked msg is not blocked",
			msgs: []sdk.Msg{
				banktypes.NewMsgSend(
					testAddresses[0],
					testAddresses[1],
					sdk.NewCoins(sdk.NewInt64Coin("ukava", 100e6)),
				),
			},
			checkTx: false,
		},
		{
			name: "a blocked msg is not blocked when not wrapped in MsgExec",
			msgs: []sdk.Msg{
				&evmtypes.MsgEthereumTx{},
			},
			checkTx: false,
		},
		{
			name: "when a MsgGrant contains a non blocked msg, it passes",
			msgs: []sdk.Msg{
				newMsgGrant(
					testAddresses[0],
					testAddresses[1],
					authz.NewGenericAuthorization(sdk.MsgTypeURL(&banktypes.MsgSend{})),
					distantFuture,
				),
			},
			checkTx: false,
		},
		{
			name: "when a MsgGrant contains a non blocked msg, it passes",
			msgs: []sdk.Msg{
				newMsgGrant(
					testAddresses[0],
					testAddresses[1],
					stakingAuthDelegate,
					distantFuture,
				),
			},
			checkTx: false,
		},
		{
			name: "when a MsgGrant contains a blocked msg, it is blocked",
			msgs: []sdk.Msg{
				newMsgGrant(
					testAddresses[0],
					testAddresses[1],
					authz.NewGenericAuthorization(sdk.MsgTypeURL(&evmtypes.MsgEthereumTx{})),
					distantFuture,
				),
			},
			checkTx:     false,
			expectedErr: sdkerrors.ErrUnauthorized,
		},
		{
			name: "when a MsgGrant contains a blocked msg, it is blocked",
			msgs: []sdk.Msg{
				newMsgGrant(
					testAddresses[0],
					testAddresses[1],
					stakingAuthUndelegate,
					distantFuture,
				),
			},
			checkTx:     false,
			expectedErr: sdkerrors.ErrUnauthorized,
		},
		{
			name: "when a MsgExec contains a non blocked msg, it passes",
			msgs: []sdk.Msg{
				newMsgExec(
					testAddresses[1],
					[]sdk.Msg{banktypes.NewMsgSend(
						testAddresses[0],
						testAddresses[3],
						sdk.NewCoins(sdk.NewInt64Coin("ukava", 100e6)),
					)}),
			},
			checkTx: false,
		},
		{
			name: "when a MsgExec contains a blocked msg, it is blocked",
			msgs: []sdk.Msg{
				newMsgExec(
					testAddresses[1],
					[]sdk.Msg{
						&evmtypes.MsgEthereumTx{},
					},
				),
			},
			checkTx:     false,
			expectedErr: sdkerrors.ErrUnauthorized,
		},
		{
			name: "blocked msg surrounded by valid msgs is still blocked",
			msgs: []sdk.Msg{
				newMsgGrant(
					testAddresses[0],
					testAddresses[1],
					stakingAuthDelegate,
					distantFuture,
				),
				newMsgExec(
					testAddresses[1],
					[]sdk.Msg{
						banktypes.NewMsgSend(
							testAddresses[0],
							testAddresses[3],
							sdk.NewCoins(sdk.NewInt64Coin("ukava", 100e6)),
						),
						&evmtypes.MsgEthereumTx{},
					},
				),
			},
			checkTx:     false,
			expectedErr: sdkerrors.ErrUnauthorized,
		},
		{
			name: "a nested MsgExec containing a blocked msg is still blocked",
			msgs: []sdk.Msg{
				newMsgExec(
					testAddresses[1],
					[]sdk.Msg{
						newMsgExec(
							testAddresses[2],
							[]sdk.Msg{
								&evmtypes.MsgEthereumTx{},
							},
						),
					},
				),
			},
			checkTx:     false,
			expectedErr: sdkerrors.ErrUnauthorized,
		},
		{
			name: "a nested MsgGrant containing a blocked msg is still blocked",
			msgs: []sdk.Msg{
				newMsgExec(
					testAddresses[1],
					[]sdk.Msg{
						newMsgGrant(
							testAddresses[0],
							testAddresses[1],
							authz.NewGenericAuthorization(sdk.MsgTypeURL(&evmtypes.MsgEthereumTx{})),
							distantFuture,
						),
					},
				),
			},
			checkTx:     false,
			expectedErr: sdkerrors.ErrUnauthorized,
		},
	}

	txConfig := app.MakeEncodingConfig().TxConfig

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			tx, err := helpers.GenTx(
				txConfig,
				tc.msgs,
				sdk.NewCoins(),
				helpers.DefaultGenTxGas,
				"testing-chain-id",
				[]uint64{0},
				[]uint64{0},
				testPrivKeys[0],
			)
			require.NoError(t, err)
			mmd := MockAnteHandler{}
			ctx := sdk.Context{}.WithIsCheckTx(tc.checkTx)
			_, err = decorator.AnteHandle(ctx, tx, false, mmd.AnteHandle)
			if tc.expectedErr != nil {
				require.ErrorIs(t, err, tc.expectedErr)
			} else {
				require.NoError(t, err)
			}
		})
	}
}