package handlers_test

import (
	"fmt"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	abci "github.com/cometbft/cometbft/abci/types"
	cmtproto "github.com/cometbft/cometbft/proto/tendermint/types"

	cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types"
	"github.com/cosmos/cosmos-sdk/testutil/testdata"
	sdk "github.com/cosmos/cosmos-sdk/types"
	authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
	banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"

	piosimapp "github.com/provenance-io/provenance/app"
	"github.com/provenance-io/provenance/internal/pioconfig"
	attrtypes "github.com/provenance-io/provenance/x/attribute/types"
	"github.com/provenance-io/provenance/x/marker/types"
	markertypes "github.com/provenance-io/provenance/x/marker/types"
)

func TestBankSend(tt *testing.T) {
	txFailureCode := uint32(1)
	pioconfig.SetProvConfig(sdk.DefaultBondDenom) // Set denom as stake.
	priv1, _, addr1 := testdata.KeyTestPubAddr()
	priv2, _, addr2 := testdata.KeyTestPubAddr()
	priv3, _, addr3 := testdata.KeyTestPubAddr()
	acct1 := authtypes.NewBaseAccount(addr1, priv1.PubKey(), 0, 0)
	acct1Balance := sdk.NewCoins(sdk.NewInt64Coin(sdk.DefaultBondDenom, 1_000_000_000))
	acct2 := authtypes.NewBaseAccount(addr2, priv2.PubKey(), 1, 0)
	acct2Balance := sdk.NewCoins(sdk.NewInt64Coin(sdk.DefaultBondDenom, 1_000_000_000))
	acct3 := authtypes.NewBaseAccount(addr3, priv3.PubKey(), 2, 0)

	app := piosimapp.SetupWithGenesisAccounts(tt, "bank-restriction-testing",
		[]authtypes.GenesisAccount{acct1, acct2, acct3},
		banktypes.Balance{Address: addr1.String(), Coins: acct1Balance},
		banktypes.Balance{Address: addr2.String(), Coins: acct2Balance},
	)
	ctx := app.BaseApp.NewContextLegacy(false, cmtproto.Header{ChainID: "bank-restriction-testing"})
	require.NoError(tt, app.AccountKeeper.Params.Set(ctx, authtypes.DefaultParams()), "AccountKeeper.Params.Set(...)")

	require.NoError(tt, app.NameKeeper.SetNameRecord(ctx, "some.kyc.provenance.io", addr1, false))
	require.NoError(tt, app.AttributeKeeper.SetAttribute(ctx, attrtypes.NewAttribute("some.kyc.provenance.io", acct3.Address, attrtypes.AttributeType_Bytes, []byte{}, nil, ""), addr1))

	nrMarkerDenom := "nonrestrictedmarker"
	nrMarkerBaseAcct := authtypes.NewBaseAccount(types.MustGetMarkerAddress(nrMarkerDenom), nil, 200, 0)
	nrMarkerAcct := markertypes.NewMarkerAccount(nrMarkerBaseAcct, sdk.NewInt64Coin(nrMarkerDenom, 10_000), addr1, []markertypes.AccessGrant{{Address: acct1.Address,
		Permissions: []markertypes.Access{markertypes.Access_Withdraw}}}, markertypes.StatusProposed, markertypes.MarkerType_Coin, true, true, false, []string{})
	require.NoError(tt, app.MarkerKeeper.SetNetAssetValue(ctx, nrMarkerAcct, markertypes.NetAssetValue{
		Price:  sdk.NewInt64Coin(pioconfig.GetProvConfig().BondDenom, 1),
		Volume: 1,
	}, "test"), "SetNetAssetValue failed to create nav for marker")
	require.NoError(tt, app.MarkerKeeper.AddFinalizeAndActivateMarker(ctx, nrMarkerAcct),
		"AddFinalizeAndActivateMarker failed to create marker")

	restrictedMarkerDenom := "restrictedmarker"
	rMarkerBaseAcct := authtypes.NewBaseAccount(types.MustGetMarkerAddress(restrictedMarkerDenom), nil, 300, 0)
	rMarkerAcct := markertypes.NewMarkerAccount(rMarkerBaseAcct, sdk.NewInt64Coin(restrictedMarkerDenom, 10_000), addr1, []markertypes.AccessGrant{{Address: acct1.Address,
		Permissions: []markertypes.Access{markertypes.Access_Withdraw, markertypes.Access_Transfer}}}, markertypes.StatusProposed, markertypes.MarkerType_RestrictedCoin, true, true, false, []string{})
	require.NoError(tt, app.MarkerKeeper.SetNetAssetValue(ctx, rMarkerAcct, markertypes.NetAssetValue{
		Price:  sdk.NewInt64Coin(pioconfig.GetProvConfig().BondDenom, 1),
		Volume: 1,
	}, "test"), "SetNetAssetValue failed to create nav for marker")
	require.NoError(tt, app.MarkerKeeper.AddFinalizeAndActivateMarker(ctx, rMarkerAcct), "AddFinalizeAndActivateMarker failed to create marker")

	restrictedAttrMarkerDenom := "restrictedmarkerattr"
	raMarkerBaseAcct := authtypes.NewBaseAccount(types.MustGetMarkerAddress(restrictedAttrMarkerDenom), nil, 400, 0)
	raMarkerAcct := markertypes.NewMarkerAccount(raMarkerBaseAcct, sdk.NewInt64Coin(restrictedAttrMarkerDenom, 10_000), addr1, []markertypes.AccessGrant{{Address: acct1.Address,
		Permissions: []markertypes.Access{markertypes.Access_Withdraw, markertypes.Access_Transfer}}}, markertypes.StatusProposed, markertypes.MarkerType_RestrictedCoin, true, true, false, []string{"some.kyc.provenance.io"})
	require.NoError(tt, app.MarkerKeeper.SetNetAssetValue(ctx, raMarkerAcct, markertypes.NetAssetValue{
		Price:  sdk.NewInt64Coin(pioconfig.GetProvConfig().BondDenom, 1),
		Volume: 1,
	}, "test"), "SetNetAssetValue failed to create nav for marker")
	require.NoError(tt, app.MarkerKeeper.AddFinalizeAndActivateMarker(ctx, raMarkerAcct), "AddFinalizeAndActivateMarker failed to create marker")

	// Check both account balances before we begin.
	addr1beforeBalance := app.BankKeeper.GetAllBalances(ctx, addr1).String()
	assert.Equal(tt, "1000000000stake", addr1beforeBalance, "addr1beforeBalance")
	addr2beforeBalance := app.BankKeeper.GetAllBalances(ctx, addr2).String()
	assert.Equal(tt, "1000000000stake", addr2beforeBalance, "addr2beforeBalance")

	// send withdraw for coins
	withdrawMsg := markertypes.NewMsgWithdrawRequest(addr1, addr1, nrMarkerDenom,
		sdk.NewCoins(sdk.NewInt64Coin(nrMarkerDenom, 1000)))
	ConstructAndSendTx(tt, *app, ctx, acct1, priv1, withdrawMsg, abci.CodeTypeOK, "")
	// Check both account balances before we begin.
	addr1afterBalance := app.BankKeeper.GetAllBalances(ctx, addr1).String()
	assert.Equal(tt, "1000nonrestrictedmarker,999850000stake", addr1afterBalance, "addr1afterBalance")

	// send withdraw for coins
	withdrawMsg = markertypes.NewMsgWithdrawRequest(addr1, addr1, restrictedMarkerDenom,
		sdk.NewCoins(sdk.NewInt64Coin(restrictedMarkerDenom, 1000)))
	ConstructAndSendTx(tt, *app, ctx, acct1, priv1, withdrawMsg, abci.CodeTypeOK, "")
	addr1afterBalance = app.BankKeeper.GetAllBalances(ctx, addr1).String()
	assert.Equal(tt, "1000nonrestrictedmarker,1000restrictedmarker,999700000stake", addr1afterBalance, "addr1afterBalance")

	// send withdraw for coins
	withdrawMsg = markertypes.NewMsgWithdrawRequest(addr1, addr1, restrictedAttrMarkerDenom,
		sdk.NewCoins(sdk.NewInt64Coin(restrictedAttrMarkerDenom, 1000)))
	ConstructAndSendTx(tt, *app, ctx, acct1, priv1, withdrawMsg, abci.CodeTypeOK, "")
	addr1afterBalance = app.BankKeeper.GetAllBalances(ctx, addr1).String()
	assert.Equal(tt, "1000nonrestrictedmarker,1000restrictedmarker,1000restrictedmarkerattr,999550000stake", addr1afterBalance, "addr1afterBalance")

	// send restricted marker from account with transfer rights and no required attributes, expect success
	sendRMarker := banktypes.NewMsgSend(addr1, addr2, sdk.NewCoins(sdk.NewInt64Coin(restrictedMarkerDenom, 100)))
	ConstructAndSendTx(tt, *app, ctx, acct1, priv1, sendRMarker, abci.CodeTypeOK, "")
	addr1afterBalance = app.BankKeeper.GetAllBalances(ctx, addr1).String()
	assert.Equal(tt, "1000nonrestrictedmarker,900restrictedmarker,1000restrictedmarkerattr,999400000stake", addr1afterBalance, "addr1afterBalance")
	addr2afterBalance := app.BankKeeper.GetAllBalances(ctx, addr2).String()
	assert.Equal(tt, "100restrictedmarker,1000000000stake", addr2afterBalance, "addr2afterBalance")

	// send non restricted marker, expect success
	sendRMarker = banktypes.NewMsgSend(addr1, addr2, sdk.NewCoins(sdk.NewInt64Coin(nrMarkerDenom, 100)))
	ConstructAndSendTx(tt, *app, ctx, acct1, priv1, sendRMarker, abci.CodeTypeOK, "")
	addr1afterBalance = app.BankKeeper.GetAllBalances(ctx, addr1).String()
	assert.Equal(tt, "900nonrestrictedmarker,900restrictedmarker,1000restrictedmarkerattr,999250000stake", addr1afterBalance, "addr1afterBalance")
	addr2afterBalance = app.BankKeeper.GetAllBalances(ctx, addr2).String()
	assert.Equal(tt, "100nonrestrictedmarker,100restrictedmarker,1000000000stake", addr2afterBalance, "addr2afterBalance")

	sendNRMarker := banktypes.NewMsgSend(addr2, addr1, sdk.NewCoins(sdk.NewInt64Coin(nrMarkerDenom, 50)))
	ConstructAndSendTx(tt, *app, ctx, acct2, priv2, sendNRMarker, abci.CodeTypeOK, "")
	addr1afterBalance = app.BankKeeper.GetAllBalances(ctx, addr1).String()
	assert.Equal(tt, "950nonrestrictedmarker,900restrictedmarker,1000restrictedmarkerattr,999250000stake", addr1afterBalance, "addr1afterBalance")
	addr2afterBalance = app.BankKeeper.GetAllBalances(ctx, addr2).String()
	assert.Equal(tt, "50nonrestrictedmarker,100restrictedmarker,999850000stake", addr2afterBalance, "addr2afterBalance")

	// On a restricted coin without required attributes where the sender doesn't have TRANSFER permission.
	sendRMarker = banktypes.NewMsgSend(addr2, addr1, sdk.NewCoins(sdk.NewInt64Coin(restrictedMarkerDenom, 100)))
	ConstructAndSendTx(tt, *app, ctx, acct2, priv2, sendRMarker, txFailureCode, addr2.String()+" does not have transfer permissions")
	addr1afterBalance = app.BankKeeper.GetAllBalances(ctx, addr1).String()
	assert.Equal(tt, "950nonrestrictedmarker,900restrictedmarker,1000restrictedmarkerattr,999250000stake", addr1afterBalance, "addr1afterBalance")
	addr2afterBalance = app.BankKeeper.GetAllBalances(ctx, addr2).String()
	assert.Equal(tt, "50nonrestrictedmarker,100restrictedmarker,999849999stake", addr2afterBalance, "addr2afterBalance")

	// On a restricted coin with required attributes from a sender that has TRANSFER permission, but the receiver doesn't have the required attributes.
	sendRMarker = banktypes.NewMsgSend(addr1, addr2, sdk.NewCoins(sdk.NewInt64Coin(restrictedAttrMarkerDenom, 100)))
	ConstructAndSendTx(tt, *app, ctx, acct1, priv1, sendRMarker, abci.CodeTypeOK, "")
	addr1afterBalance = app.BankKeeper.GetAllBalances(ctx, addr1).String()
	assert.Equal(tt, "950nonrestrictedmarker,900restrictedmarker,900restrictedmarkerattr,999100000stake", addr1afterBalance, "addr1afterBalance")
	addr2afterBalance = app.BankKeeper.GetAllBalances(ctx, addr2).String()
	assert.Equal(tt, "50nonrestrictedmarker,100restrictedmarker,100restrictedmarkerattr,999849999stake", addr2afterBalance, "addr2afterBalance")

	// On a restricted coin with required attributes from a sender that does not have TRANSFER permission, but the receiver DOES have the required attributes.
	sendRMarker = banktypes.NewMsgSend(addr2, addr3, sdk.NewCoins(sdk.NewInt64Coin(restrictedAttrMarkerDenom, 25)))
	ConstructAndSendTx(tt, *app, ctx, acct2, priv2, sendRMarker, abci.CodeTypeOK, "")
	addr2afterBalance = app.BankKeeper.GetAllBalances(ctx, addr2).String()
	assert.Equal(tt, "50nonrestrictedmarker,100restrictedmarker,75restrictedmarkerattr,999699999stake", addr2afterBalance, "addr2afterBalance")
	addr3afterBalance := app.BankKeeper.GetAllBalances(ctx, addr3).String()
	assert.Equal(tt, "25restrictedmarkerattr", addr3afterBalance, "addr3afterBalance")

	// MsgTransfer Tests
	// On a restricted coin with required attributes using an admin that has TRANSFER permission, but the receiver doesn't have the required attributes.
	tranferRMarker := markertypes.NewMsgTransferRequest(addr1, addr1, addr2, sdk.NewInt64Coin(restrictedMarkerDenom, 25))
	ConstructAndSendTx(tt, *app, ctx, acct1, priv1, tranferRMarker, abci.CodeTypeOK, "")
	addr1afterBalance = app.BankKeeper.GetAllBalances(ctx, addr1).String()
	assert.Equal(tt, "950nonrestrictedmarker,875restrictedmarker,900restrictedmarkerattr,998950000stake", addr1afterBalance, "addr1afterBalance")
	addr2afterBalance = app.BankKeeper.GetAllBalances(ctx, addr2).String()
	assert.Equal(tt, "50nonrestrictedmarker,125restrictedmarker,75restrictedmarkerattr,999699999stake", addr2afterBalance, "addr2afterBalance")

	// On a restricted coin with required attributes using an admin that does not have TRANSFER permission, but the receiver DOES have the required attributes.
	tranferRAMarker := markertypes.NewMsgTransferRequest(addr2, addr2, addr3, sdk.NewInt64Coin(restrictedAttrMarkerDenom, 25))
	expErr := fmt.Sprintf("%s does not have ACCESS_TRANSFER on restrictedmarkerattr marker (%s)",
		addr2.String(), raMarkerAcct.GetAddress().String())
	ConstructAndSendTx(tt, *app, ctx, acct2, priv2, tranferRAMarker, txFailureCode, expErr)
	addr2afterBalance = app.BankKeeper.GetAllBalances(ctx, addr2).String()
	assert.Equal(tt, "50nonrestrictedmarker,125restrictedmarker,75restrictedmarkerattr,999699998stake", addr2afterBalance, "addr2afterBalance")
	addr3afterBalance = app.BankKeeper.GetAllBalances(ctx, addr3).String()
	assert.Equal(tt, "25restrictedmarkerattr", addr3afterBalance, "addr3afterBalance")
}

func ConstructAndSendTx(tt *testing.T, app piosimapp.App, ctx sdk.Context, acct *authtypes.BaseAccount, priv cryptotypes.PrivKey, msg sdk.Msg, expectedCode uint32, expectedError string) {
	encCfg := app.GetEncodingConfig()
	fees := sdk.NewCoins(sdk.NewInt64Coin(sdk.DefaultBondDenom, int64(TestGasLimit)))
	acct = app.AccountKeeper.GetAccount(ctx, acct.GetAddress()).(*authtypes.BaseAccount)
	txBytes, err := SignTxAndGetBytes(ctx, TestGasLimit, fees, encCfg, priv.PubKey(), priv, *acct, ctx.ChainID(), msg)
	require.NoError(tt, err, "SignTxAndGetBytes")
	res, err := app.FinalizeBlock(&abci.RequestFinalizeBlock{
		Height: ctx.BlockHeight() + 1,
		Time:   time.Now().UTC(),
		Txs:    [][]byte{txBytes},
	},
	)
	require.NoError(tt, err, "FinalizeBlock expected no error")
	require.Len(tt, res.TxResults, 1, "TxResults expected len not met")
	require.Equal(tt, int(expectedCode), int(res.TxResults[0].Code), "res=%+v", res)
	if len(expectedError) > 0 {
		require.Contains(tt, res.TxResults[0].Log, expectedError, "DeliverTx result.Log")
	}
}
