package keeper_test

import (
	"fmt"
	"testing"

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

	sdkmath "cosmossdk.io/math"

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

	simapp "github.com/provenance-io/provenance/app"
	internalsdk "github.com/provenance-io/provenance/internal/sdk"
	attrTypes "github.com/provenance-io/provenance/x/attribute/types"
	"github.com/provenance-io/provenance/x/marker/keeper"
	markerkeeper "github.com/provenance-io/provenance/x/marker/keeper"
	"github.com/provenance-io/provenance/x/marker/types"
)

func TestSendRestrictionFn(t *testing.T) {
	c := func(amt int64, denom string) sdk.Coin {
		return sdk.NewInt64Coin(denom, amt)
	}
	cz := func(coins ...sdk.Coin) sdk.Coins {
		return sdk.NewCoins(coins...)
	}

	app := simapp.Setup(t)
	ctx := app.BaseApp.NewContext(false)
	// ctxP returns a pointer to the provided context.
	ctxP := func(ctx sdk.Context) *sdk.Context {
		return &ctx
	}
	owner := sdk.AccAddress("owner_address_______") // cosmos1damkuetjtaskgerjv4ehxh6lta047h6l54ft8x
	app.AccountKeeper.SetAccount(ctx, app.AccountKeeper.NewAccountWithAddress(ctx, owner))
	require.NoError(t, app.NameKeeper.SetNameRecord(ctx, "kyc.provenance.io", owner, false), "SetNameRecord kyc.provenance.io")
	require.NoError(t, app.NameKeeper.SetNameRecord(ctx, "not-kyc.provenance.io", owner, false), "SetNameRecord not-kyc.provenance.io")

	addrWithAttrs := sdk.AccAddress("addr_with_attributes") // cosmos1v9jxgujlwa5hg6zlv968gunfvf6hgetn0pdywn
	addrWithAttrsStr := addrWithAttrs.String()
	require.NoError(t, app.AttributeKeeper.SetAttribute(ctx,
		attrTypes.Attribute{
			Name:          "kyc.provenance.io",
			Value:         []byte("string value"),
			Address:       addrWithAttrsStr,
			AttributeType: attrTypes.AttributeType_String,
		},
		owner,
	), "SetAttribute kyc.provenance.io")
	require.NoError(t, app.AttributeKeeper.SetAttribute(ctx,
		attrTypes.Attribute{
			Name:          "not-kyc.provenance.io",
			Value:         []byte("string value"),
			Address:       addrWithAttrsStr,
			AttributeType: attrTypes.AttributeType_String,
		},
		owner,
	), "SetAttribute not-kyc.provenance.io")

	addrWithoutAttrs := sdk.AccAddress("addr_without_attribs")        // cosmos1v9jxgujlwa5hg6r0w4697ct5w3exjcnnex0ue3
	addrWithTransfer := sdk.AccAddress("addr_with_transfer__")        // cosmos1v9jxgujlwa5hg6zlw3exzmnnvejhyh6l0estr5
	addrWithForceTransfer := sdk.AccAddress("addr_with_force_tran")   // cosmos1v9jxgujlwa5hg6zlvehhycm9ta68yctwk3cx3n
	addrWithDeposit := sdk.AccAddress("addrWithDeposit_____")         // cosmos1v9jxgujhd96xs3r9wphhx6t5ta047h6lkfv2e0
	addrWithWithdraw := sdk.AccAddress("addrWithWithdraw____")        // cosmos1v9jxgujhd96xs4mfw35xgunpwa047h6lf5yvu2
	addrWithTranDep := sdk.AccAddress("addrWithTranDep_____")         // cosmos1v9jxgujhd96xs4rjv9hygetsta047h6lvx7ley
	addrWithTranWithdraw := sdk.AccAddress("addrWithTranWithdraw")    // cosmos1v9jxgujhd96xs4rjv9h9w6t5dpj8ycth4fy3dc
	addrWithTranDepWithdraw := sdk.AccAddress("addrWithTranDepWithd") // cosmos1v9jxgujhd96xs4rjv9hygets2a5hg6ry5lx6jm
	addrWithDepWithdraw := sdk.AccAddress("addrWithDepWithdraw_")     // cosmos1v9jxgujhd96xs3r9wptkjargv3exza6lsjke8w
	addrWithDenySend := sdk.AccAddress("addrWithDenySend____")        // cosmos1v9jxgujhd96xs3r9deu4xetwv3047h6lttsvaa
	addrOther := sdk.AccAddress("addrOther___________")               // cosmos1v9jxguj0w35x2ujlta047h6lta047h6ldtkks6
	addrOther2 := sdk.AccAddress("addrOther2__________")              // cosmos1v9jxguj0w35x2u3jta047h6lta047h6lucvw6t

	addrFeeCollector := app.MarkerKeeper.GetFeeCollectorAddr() // cosmos17xpfvakm2amg962yls6f84z3kell8c5lserqta
	bypassAddrs := app.MarkerKeeper.GetReqAttrBypassAddrs()
	var addrWithBypass, addrWithBypassNoDep sdk.AccAddress
	for _, addr := range bypassAddrs {
		if addr.Equals(addrFeeCollector) {
			continue
		}
		if len(addrWithBypass) == 0 {
			addrWithBypass = addr
			continue
		}
		addrWithBypassNoDep = addr
		break
	}
	require.NotEmpty(t, addrWithBypass, "addrWithBypass (first bypass address that is not the fee collector)")
	require.NotEmpty(t, addrWithBypassNoDep, "addrWithBypassNoDep (second bypass address that is not the fee collector)")

	coin := types.MarkerType_Coin
	restricted := types.MarkerType_RestrictedCoin

	newMarkerAcc := func(denom string, markerType types.MarkerType, reqAttrs []string) *types.MarkerAccount {
		addr, err := types.MarkerAddress(denom)
		require.NoError(t, err, "MarkerAddress(%q)", denom)

		rv := &types.MarkerAccount{
			BaseAccount:            authtypes.NewBaseAccountWithAddress(addr),
			Manager:                owner.String(),
			Status:                 types.StatusProposed,
			Denom:                  denom,
			Supply:                 sdkmath.NewInt(1000),
			MarkerType:             markerType,
			SupplyFixed:            true,
			AllowGovernanceControl: true,
			AllowForcedTransfer:    false,
			RequiredAttributes:     reqAttrs,
		}

		if markerType == restricted {
			rv.AccessControl = []types.AccessGrant{
				{Address: addrWithTransfer.String(), Permissions: types.AccessList{types.Access_Transfer}},
				{Address: addrWithForceTransfer.String(), Permissions: types.AccessList{types.Access_ForceTransfer}},
				{Address: addrWithDeposit.String(), Permissions: types.AccessList{types.Access_Deposit}},
				{Address: addrWithWithdraw.String(), Permissions: types.AccessList{types.Access_Withdraw}},
				{Address: addrWithTranDep.String(), Permissions: types.AccessList{types.Access_Deposit, types.Access_Transfer}},
				{Address: addrWithTranWithdraw.String(), Permissions: types.AccessList{types.Access_Withdraw, types.Access_Transfer}},
				{Address: addrWithDepWithdraw.String(), Permissions: types.AccessList{types.Access_Deposit, types.Access_Withdraw}},
				{Address: addrWithTranDepWithdraw.String(), Permissions: types.AccessList{types.Access_Deposit, types.Access_Withdraw, types.Access_Transfer}},
				// It's silly to give any permissions to a bypass address, but I do so in here to hit some test cases.
				{Address: addrWithBypass.String(), Permissions: types.AccessList{types.Access_Deposit}},
			}
		}

		return rv
	}
	createActiveMarker := func(marker *types.MarkerAccount) *types.MarkerAccount {
		nav := []types.NetAssetValue{types.NewNetAssetValue(sdk.NewInt64Coin(types.UsdDenom, int64(1)), 1)}
		err := app.MarkerKeeper.AddSetNetAssetValues(ctx, marker, nav, t.Name())
		require.NoError(t, err, "AddSetNetAssetValues(%v, %v, %v)", marker.Denom, nav, t.Name())
		err = app.MarkerKeeper.AddFinalizeAndActivateMarker(ctx, marker)
		require.NoError(t, err, "AddFinalizeAndActivateMarker(%s)", marker.Denom)
		return marker
	}
	newMarker := func(denom string, markerType types.MarkerType, reqAttrs []string) *types.MarkerAccount {
		return createActiveMarker(newMarkerAcc(denom, markerType, reqAttrs))
	}
	newProposedMarker := func(denom string, markerType types.MarkerType, reqAttrs []string) *types.MarkerAccount {
		rv := newMarkerAcc(denom, markerType, reqAttrs)
		err := app.MarkerKeeper.AddMarkerAccount(ctx, rv)
		require.NoError(t, err, "AddMarkerAccount(%s)", denom)
		return rv

	}

	nrDenom := "nonrestrictedmarker" // cosmos1kfpnkyu2vln5ywrhhvuwdy5pe9an5krxm09mzm
	nrMarker := newMarker(nrDenom, coin, nil)

	// dpmDenom is an unrestricted (coin) marker that opts in to deposit-access enforcement via require_deposit_access.
	dpmDenom := "depositprotectedmarker"
	dpmMarker := newMarkerAcc(dpmDenom, coin, nil)
	dpmMarker.RequireDepositAccess = true
	dpmMarker.AccessControl = []types.AccessGrant{
		{Address: addrWithDeposit.String(), Permissions: types.AccessList{types.Access_Deposit}},
	}
	dpmMarker = createActiveMarker(dpmMarker)

	// Create a marker similar to the ones we use for hash grants.
	gDenom := "grantmarker" // cosmos1j6pqqrczarugl3zxunvnwgsrq04rzm9wujnlj4
	gMarker := newMarkerAcc(gDenom, coin, nil)
	gMarker.Supply = sdkmath.ZeroInt()
	gMarker.AccessControl = []types.AccessGrant{
		{Address: owner.String(), Permissions: types.AccessList{
			types.Access_Mint, types.Access_Admin,
			types.Access_Deposit, types.Access_Withdraw}}}
	gMarker = createActiveMarker(gMarker)

	denomOther := "othercoin" // cosmos17hpvvkergxjys0mlsdy9s8r2r3hksl3fsffn94 (if it were a marker account).
	// And throw some other funds in there.
	require.NoError(t,
		testutil.FundAccount(types.WithBypass(ctx), app.BankKeeper, gMarker.GetAddress(), cz(c(5000, denomOther))),
		"Adding funds to %s marker account", gDenom)

	rDenomNoAttr := "restrictedmarkernoreqattributes" // cosmos1y4lw4mu35znxeuu00t2waemytpprkv9kazmkp2
	rMarkerNoAttr := newMarker(rDenomNoAttr, restricted, nil)
	app.MarkerKeeper.AddSendDeny(ctx, rMarkerNoAttr.GetAddress(), addrWithDenySend)

	rDenom1AttrNoOneHas := "restrictedmarkerreqattributes2" // cosmos1up7jx7k307r926gcaqnylnqm9nj9vukc90z6u4
	newMarker(rDenom1AttrNoOneHas, restricted, []string{"some.attribute.that.i.require"})

	rDenom1Attr := "restrictedmarkerreqattributes3" // cosmos13h8gqljs4yz8v6duauce47aw2cqnz5njf46sl9
	rMarker1Attr := newMarker(rDenom1Attr, restricted, []string{"kyc.provenance.io"})
	require.NoError(t, app.AttributeKeeper.SetAttribute(ctx,
		attrTypes.Attribute{
			Name:          "kyc.provenance.io",
			Value:         []byte("string value"),
			Address:       rMarker1Attr.GetAddress().String(),
			AttributeType: attrTypes.AttributeType_String,
		},
		owner,
	), "SetAttribute kyc.provenance.io")

	rDenom2Attrs := "restrictedmarkerreqattributes4" // cosmos1qnp4x74tlzz5w9any3etx0erhl92ufk9pfwtv2
	rMarker2Attrs := newMarker(rDenom2Attrs, restricted, []string{"kyc.provenance.io", "not-kyc.provenance.io"})

	rDenom3Attrs := "restrictedmarkerreqattributes5" // cosmos15jcvmcvecjxysdemd2t9qvy5wp8ak28z8sgcnn
	newMarker(rDenom3Attrs, restricted, []string{"kyc.provenance.io", "not-kyc.provenance.io", "foo.provenance.io"})

	rDenomProposed := "stillproposed" // cosmos1cjq467qkvef5gu4nczt42fl3q8pgmrnesx4647
	rMarkerProposed := newProposedMarker(rDenomProposed, restricted, nil)

	noAccessErr := func(addr sdk.AccAddress, role types.Access, denom string) string {
		mAddr, err := types.MarkerAddress(denom)
		require.NoError(t, err, "MarkerAddress(%q)", denom)
		return fmt.Sprintf("%s does not have %s on %s marker (%s)", addr, role, denom, mAddr)
	}
	multiNoAccessErr := func(role types.Access, denom string, addrs ...sdk.AccAddress) string {
		mAddr, err := types.MarkerAddress(denom)
		require.NoError(t, err, "MarkerAddress(%q)", denom)
		strs := make([]string, len(addrs))
		for i, addr := range addrs {
			strs[i] = addr.String()
		}
		return fmt.Sprintf("none of %q have %s on %s marker (%s)", strs, role, denom, mAddr)
	}

	testCases := []struct {
		name       string
		attrKeeper *WrappedAttrKeeper
		ctx        *sdk.Context
		from       sdk.AccAddress
		to         sdk.AccAddress
		amt        sdk.Coins
		expErr     string
	}{
		{
			name:   "unknown denom",
			from:   owner,
			to:     addrWithAttrs,
			amt:    cz(c(1, "unknowncoin")),
			expErr: "",
		},
		{
			name:   "non restricted marker",
			from:   owner,
			to:     addrWithAttrs,
			amt:    cz(c(1, nrDenom)),
			expErr: "",
		},
		{
			name: "restricted to fee collector from normal account",
			// include a transfer agent just to make sure that doesn't bypass anything.
			ctx:    ctxP(types.WithTransferAgents(ctx, addrWithTransfer)),
			from:   addrOther,
			to:     addrFeeCollector,
			amt:    cz(c(1, rDenomNoAttr)),
			expErr: "restricted denom " + rDenomNoAttr + " cannot be sent to the fee collector",
		},
		{
			name: "restricted to fee collector from marker module account",
			// include a transfer agent just to make sure that doesn't bypass anything.
			ctx:    ctxP(types.WithTransferAgents(ctx, addrWithTranWithdraw)),
			from:   app.MarkerKeeper.GetMarkerModuleAddr(),
			to:     addrFeeCollector,
			amt:    cz(c(1, rDenomNoAttr)),
			expErr: "cannot send restricted denom " + rDenomNoAttr + " to the fee collector",
		},
		{
			name: "restricted to fee collector from ibc transfer module account",
			// include a transfer agent just to make sure that doesn't bypass anything.
			ctx:    ctxP(types.WithTransferAgents(ctx, addrWithTransfer)),
			from:   app.MarkerKeeper.GetIbcTransferModuleAddr(),
			to:     addrFeeCollector,
			amt:    cz(c(1, rDenomNoAttr)),
			expErr: "cannot send restricted denom " + rDenomNoAttr + " to the fee collector",
		},
		{
			name:   "addr has transfer, denom without attrs",
			from:   addrWithTransfer,
			to:     addrWithoutAttrs,
			amt:    cz(c(1, rDenomNoAttr)),
			expErr: "",
		},
		{
			name:   "addr has force transfer, denom without attrs",
			from:   addrWithForceTransfer,
			to:     addrWithoutAttrs,
			amt:    cz(c(1, rDenomNoAttr)),
			expErr: fmt.Sprintf("%s does not have transfer permissions for %s", addrWithForceTransfer.String(), rDenomNoAttr),
		},
		{
			name:   "addr has transfer, denom with 3 attrs, to has none",
			from:   addrWithTransfer,
			to:     addrWithoutAttrs,
			amt:    cz(c(1, rDenom3Attrs)),
			expErr: "",
		},
		{
			name:       "error getting attrs",
			attrKeeper: NewWrappedAttrKeeper().WithGetAllAttributesAddrErrs("crazy injected attr error"),
			from:       addrOther,
			to:         addrWithAttrs,
			amt:        cz(c(1, rDenom3Attrs)),
			expErr:     "could not get attributes for " + addrWithAttrsStr + ": crazy injected attr error",
		},
		{
			name:   "restricted marker with empty required attributes and no transfer rights",
			from:   owner,
			to:     addrWithAttrs,
			amt:    cz(c(1, rDenomNoAttr)),
			expErr: fmt.Sprintf("%s does not have transfer permissions for %s", owner.String(), rDenomNoAttr),
		},
		{
			name: "restricted marker with required attributes but none match",
			from: owner,
			to:   addrWithAttrs,
			amt:  cz(c(1, rDenom1AttrNoOneHas)),
			expErr: fmt.Sprintf("address %s does not contain the %q required attribute: \"some.attribute.that.i.require\"",
				addrWithAttrsStr, rDenom1AttrNoOneHas),
			// This should be the exact same test as the below one, but without a bypass context, so expect an error.
		},
		{
			// This should be the exact same test as the above one, but with a bypass context, so no error is expected.
			name:   "with bypass, restricted marker with required attributes but none match",
			ctx:    ctxP(types.WithBypass(ctx)),
			from:   owner,
			to:     addrWithAttrs,
			amt:    cz(c(1, rDenom1AttrNoOneHas)),
			expErr: "",
		},
		{
			name:   "from marker module account",
			from:   app.MarkerKeeper.GetMarkerModuleAddr(),
			to:     addrWithAttrs,
			amt:    cz(c(1, rDenom1AttrNoOneHas)),
			expErr: "",
		},
		{
			name:   "from ibc transfer module account",
			from:   app.MarkerKeeper.GetIbcTransferModuleAddr(),
			to:     addrWithAttrs,
			amt:    cz(c(1, rDenom1AttrNoOneHas)),
			expErr: "",
		},
		{
			name:   "send from an account on denied list",
			from:   addrWithDenySend,
			to:     addrWithAttrs,
			amt:    cz(c(1, rDenomNoAttr)),
			expErr: addrWithDenySend.String() + " is on deny list for sending restricted marker",
		},
		{
			name:   "account contains the needed attribute",
			from:   owner,
			to:     addrWithAttrs,
			amt:    cz(c(1, rDenom1Attr)),
			expErr: "",
		},
		{
			name:   "account contains both needed attributes",
			from:   owner,
			to:     addrWithAttrs,
			amt:    cz(c(1, rDenom2Attrs)),
			expErr: "",
		},
		{
			name: "account contains 2 of 3 needed attributes",
			from: owner,
			to:   addrWithAttrs,
			amt:  cz(c(1, rDenom3Attrs)),
			expErr: fmt.Sprintf("address %s does not contain the %q required attribute: \"foo.provenance.io\"",
				addrWithAttrsStr, rDenom3Attrs),
		},
		{
			name: "account has no attributes, needs 3",
			from: owner,
			to:   addrWithoutAttrs,
			amt:  cz(c(1, rDenom3Attrs)),
			expErr: fmt.Sprintf("address %s does not contain the %q required attributes: "+
				"\"kyc.provenance.io\", \"not-kyc.provenance.io\", \"foo.provenance.io\"",
				addrWithoutAttrs, rDenom3Attrs),
		},
		{
			name:   "account has no attributes, denom not restricted",
			from:   addrWithTransfer,
			to:     addrWithoutAttrs,
			amt:    cz(c(1, nrDenom)),
			expErr: "",
		},
		{
			name:   "two denoms, unrestricted and has needed attribute",
			from:   owner,
			to:     addrWithAttrs,
			amt:    cz(c(1, nrDenom), c(1, rDenom1Attr)),
			expErr: "",
		},
		{
			name:   "two denoms, has needed attribute and unrestricted",
			from:   owner,
			to:     addrWithAttrs,
			amt:    cz(c(1, rDenom1Attr), c(1, nrDenom)),
			expErr: "",
		},
		{
			name: "two denoms, unrestricted and missing attribute",
			from: owner,
			to:   addrWithAttrs,
			amt:  cz(c(1, nrDenom), c(1, rDenom1AttrNoOneHas)),
			expErr: fmt.Sprintf("address %s does not contain the %q required attribute: \"some.attribute.that.i.require\"",
				addrWithAttrsStr, rDenom1AttrNoOneHas),
		},
		{
			name: "two denoms, missing attribute and unrestricted",
			from: owner,
			to:   addrWithAttrs,
			amt:  cz(c(1, rDenom1AttrNoOneHas), c(1, nrDenom)),
			expErr: fmt.Sprintf("address %s does not contain the %q required attribute: \"some.attribute.that.i.require\"",
				addrWithAttrsStr, rDenom1AttrNoOneHas),
		},
		{
			name:   "send to marker from account without deposit",
			from:   addrWithAttrs,
			to:     rMarkerNoAttr.GetAddress(),
			amt:    cz(c(1, rDenomNoAttr)),
			expErr: noAccessErr(addrWithAttrs, types.Access_Deposit, rDenomNoAttr),
		},
		{
			name:   "send to marker from account with deposit but no transfer",
			from:   addrWithDeposit,
			to:     rMarkerNoAttr.GetAddress(),
			amt:    cz(c(1, rDenomNoAttr)),
			expErr: noAccessErr(addrWithDeposit, types.Access_Transfer, rDenomNoAttr),
		},
		{
			name:   "send to another marker with transfer on denom but no deposit on to",
			from:   addrWithTransfer,
			to:     rMarker1Attr.GetAddress(),
			amt:    cz(c(1, rDenomNoAttr)),
			expErr: noAccessErr(addrWithTransfer, types.Access_Deposit, rDenom1Attr),
		},
		{
			name:   "send to another marker without transfer on denom but with deposit on to",
			from:   addrWithDeposit,
			to:     rMarker1Attr.GetAddress(),
			amt:    cz(c(1, rDenomNoAttr)),
			expErr: noAccessErr(addrWithDeposit, types.Access_Transfer, rDenomNoAttr),
		},
		{
			name:   "send to another marker with transfer on denom and deposit on to",
			from:   addrWithTranDep,
			to:     rMarker1Attr.GetAddress(),
			amt:    cz(c(1, rDenomNoAttr)),
			expErr: "",
		},
		{
			name:   "send non-restricted coin to the marker",
			from:   addrWithoutAttrs,
			to:     nrMarker.GetAddress(),
			amt:    cz(c(1, nrDenom)),
			expErr: "",
		},
		{
			name:   "deposit protected unrestricted marker from account without deposit",
			from:   addrWithoutAttrs,
			to:     dpmMarker.GetAddress(),
			amt:    cz(c(1, nrDenom)),
			expErr: noAccessErr(addrWithoutAttrs, types.Access_Deposit, dpmDenom),
		},
		{
			name:   "deposit protected unrestricted marker from account with deposit",
			from:   addrWithDeposit,
			to:     dpmMarker.GetAddress(),
			amt:    cz(c(1, nrDenom)),
			expErr: "",
		},
		{
			name:   "to a marker from addr with bypass but no deposit",
			from:   addrWithBypassNoDep,
			to:     rMarkerNoAttr.GetAddress(),
			amt:    cz(c(1, rDenomNoAttr)),
			expErr: noAccessErr(addrWithBypassNoDep, types.Access_Deposit, rDenomNoAttr),
		},
		{
			name:   "to a marker with req attrs from an addr with bypass",
			from:   addrWithBypass,
			to:     rMarker1Attr.GetAddress(),
			amt:    cz(c(1, rDenom1Attr)),
			expErr: noAccessErr(addrWithBypass, types.Access_Transfer, rDenom1Attr),
		},
		{
			name:   "to marker without req attrs from addr with bypass",
			from:   addrWithBypass,
			to:     rMarkerNoAttr.GetAddress(),
			amt:    cz(c(1, rDenomNoAttr)),
			expErr: noAccessErr(addrWithBypass, types.Access_Transfer, rDenomNoAttr),
		},
		{
			name:   "no req attrs from addr with bypass",
			from:   addrWithBypass,
			to:     addrOther,
			amt:    cz(c(3, rDenomNoAttr)),
			expErr: "",
		},
		{
			name: "with req attrs from addr with bypass",
			from: addrWithBypass,
			to:   addrOther,
			amt:  cz(c(1, rDenom1AttrNoOneHas)),
			expErr: fmt.Sprintf("address %s does not contain the %q required attribute: \"some.attribute.that.i.require\"",
				addrOther, rDenom1AttrNoOneHas),
		},
		{
			name:   "no req attrs to addr with bypass from without transfer",
			from:   addrOther,
			to:     addrWithBypass,
			amt:    cz(c(1, rDenomNoAttr)),
			expErr: addrOther.String() + " does not have transfer permissions for " + rDenomNoAttr,
		},
		{
			name:   "no req attrs to addr with bypass from with transfer",
			from:   addrWithTransfer,
			to:     addrWithBypass,
			amt:    cz(c(1, rDenomNoAttr)),
			expErr: "",
		},
		{
			name:   "with req attrs to addr with bypass from without transfer",
			from:   addrOther,
			to:     addrWithBypass,
			amt:    cz(c(1, rDenom1AttrNoOneHas)),
			expErr: "",
		},
		{
			name:   "with req attrs to addr with bypass from with transfer",
			from:   addrWithTransfer,
			to:     addrWithBypass,
			amt:    cz(c(1, rDenom1AttrNoOneHas)),
			expErr: "",
		},
		{
			name:   "with req attrs between bypass addrs",
			from:   addrWithBypass,
			to:     addrWithBypassNoDep,
			amt:    cz(c(1, rDenom1AttrNoOneHas)),
			expErr: "",
		},
		{
			name:   "without req attrs between bypass addrs",
			from:   addrWithBypass,
			to:     addrWithBypassNoDep,
			amt:    cz(c(1, rDenomNoAttr)),
			expErr: "",
		},
		{
			name:   "from marker: no admin",
			from:   rMarkerNoAttr.GetAddress(),
			to:     addrWithAttrs,
			amt:    cz(c(2, rDenomNoAttr)),
			expErr: "cannot withdraw from marker account " + rMarkerNoAttr.GetAddress().String() + " (" + rDenomNoAttr + ")",
		},
		{
			name:   "from grant marker: no admin, no feegrant in use",
			ctx:    ctxP(ctx),
			from:   gMarker.GetAddress(),
			to:     addrWithAttrs,
			amt:    cz(c(2, denomOther)),
			expErr: "cannot withdraw from marker account " + gMarker.GetAddress().String() + " (" + gDenom + ")",
		},
		{
			name: "from grant marker: no admin, with feegrant in use",
			ctx:  ctxP(internalsdk.WithFeeGrantInUse(ctx)),
			from: gMarker.GetAddress(),
			to:   addrWithAttrs,
			amt:  cz(c(3, denomOther)),
		},
		{
			name:   "from grant marker: no admin, without feegrant in use",
			ctx:    ctxP(internalsdk.WithoutFeeGrantInUse(ctx)),
			from:   gMarker.GetAddress(),
			to:     addrWithAttrs,
			amt:    cz(c(6, denomOther)),
			expErr: "cannot withdraw from marker account " + gMarker.GetAddress().String() + " (" + gDenom + ")",
		},
		{
			name:   "from marker: admin without withdraw permission",
			ctx:    ctxP(types.WithTransferAgents(ctx, addrWithTransfer)),
			from:   rMarkerNoAttr.GetAddress(),
			to:     addrWithAttrs,
			amt:    cz(c(2, rDenomNoAttr)),
			expErr: noAccessErr(addrWithTransfer, types.Access_Withdraw, rDenomNoAttr),
		},
		{
			name: "from marker: withdraw marker funds from inactive marker",
			ctx:  ctxP(types.WithTransferAgents(ctx, addrWithTranWithdraw)),
			from: rMarkerProposed.GetAddress(),
			to:   addrWithAttrs,
			amt:  cz(c(2, rDenomNoAttr), c(1, rDenomProposed), c(5, rDenom3Attrs)),
			expErr: fmt.Sprintf("cannot withdraw %s from %s marker (%s): marker status (proposed) is not active",
				c(1, rDenomProposed), rDenomProposed, rMarkerProposed.GetAddress(),
			),
		},
		{
			name: "from marker: withdraw non-marker funds from inactive marker",
			ctx:  ctxP(types.WithTransferAgents(ctx, addrWithTranWithdraw)),
			from: rMarkerProposed.GetAddress(),
			to:   addrWithAttrs,
			amt:  cz(c(2, rDenomNoAttr), c(5, rDenom3Attrs)),
		},
		{
			name: "from marker: withdraw from active marker",
			ctx:  ctxP(types.WithTransferAgents(ctx, addrWithTranWithdraw)),
			from: rMarkerNoAttr.GetAddress(),
			to:   addrWithAttrs,
			amt:  cz(c(3, rDenomNoAttr)),
		},
		{
			name: "with admin: does not have transfer, but from does",
			ctx:  ctxP(types.WithTransferAgents(ctx, addrOther)),
			from: owner,
			to:   addrWithAttrs,
			amt:  cz(c(1, rDenom1Attr), c(1, nrDenom)),
		},
		{
			name: "with admin: has transfer: would otherwise fail",
			ctx:  ctxP(types.WithTransferAgents(ctx, addrWithTransfer)),
			from: addrWithDenySend,
			to:   addrWithAttrs,
			amt:  cz(c(1, rDenomNoAttr)),
		},
		{
			name:   "with two admins: neither has transfer",
			ctx:    ctxP(types.WithTransferAgents(ctx, addrWithDeposit, addrWithWithdraw)),
			from:   addrOther,
			to:     addrWithAttrs,
			amt:    cz(c(1, rDenomNoAttr)),
			expErr: addrOther.String() + " does not have transfer permissions for " + rDenomNoAttr,
		},
		{
			name: "with two admins: first has transfer",
			ctx:  ctxP(types.WithTransferAgents(ctx, addrWithTransfer, addrWithWithdraw)),
			from: addrWithDenySend,
			to:   addrWithAttrs,
			amt:  cz(c(1, rDenomNoAttr)),
		},
		{
			name: "with two admins: second has transfer",
			ctx:  ctxP(types.WithTransferAgents(ctx, addrWithDeposit, addrWithTransfer)),
			from: addrWithDenySend,
			to:   addrWithAttrs,
			amt:  cz(c(1, rDenomNoAttr)),
		},
		{
			name:   "from marker to marker: admin only has transfer",
			ctx:    ctxP(types.WithTransferAgents(ctx, addrWithTransfer)),
			from:   rMarkerNoAttr.GetAddress(),
			to:     rMarker1Attr.GetAddress(),
			amt:    cz(c(1, rDenom1AttrNoOneHas)),
			expErr: noAccessErr(addrWithTransfer, types.Access_Withdraw, rDenomNoAttr),
		},
		{
			name:   "from marker to marker: admin only has deposit",
			ctx:    ctxP(types.WithTransferAgents(ctx, addrWithDeposit)),
			from:   rMarkerNoAttr.GetAddress(),
			to:     rMarker1Attr.GetAddress(),
			amt:    cz(c(1, rDenom1AttrNoOneHas)),
			expErr: noAccessErr(addrWithDeposit, types.Access_Withdraw, rDenomNoAttr),
		},
		{
			name:   "from marker to marker: admin only has withdraw",
			ctx:    ctxP(types.WithTransferAgents(ctx, addrWithWithdraw)),
			from:   rMarkerNoAttr.GetAddress(),
			to:     rMarker1Attr.GetAddress(),
			amt:    cz(c(1, rDenom1AttrNoOneHas)),
			expErr: noAccessErr(addrWithWithdraw, types.Access_Deposit, rDenom1Attr),
		},
		{
			name:   "from marker to marker: admin only has transfer and deposit",
			ctx:    ctxP(types.WithTransferAgents(ctx, addrWithTranDep)),
			from:   rMarkerNoAttr.GetAddress(),
			to:     rMarker1Attr.GetAddress(),
			amt:    cz(c(1, rDenom1AttrNoOneHas)),
			expErr: noAccessErr(addrWithTranDep, types.Access_Withdraw, rDenomNoAttr),
		},
		{
			name:   "from marker to marker: admin only has transfer and withdraw",
			ctx:    ctxP(types.WithTransferAgents(ctx, addrWithTranWithdraw)),
			from:   rMarkerNoAttr.GetAddress(),
			to:     rMarker1Attr.GetAddress(),
			amt:    cz(c(1, rDenom1AttrNoOneHas)),
			expErr: noAccessErr(addrWithTranWithdraw, types.Access_Deposit, rDenom1Attr),
		},
		{
			name:   "from marker to marker: admin only has deposit and withdraw",
			ctx:    ctxP(types.WithTransferAgents(ctx, addrWithDepWithdraw)),
			from:   rMarker1Attr.GetAddress(),
			to:     rMarker2Attrs.GetAddress(),
			amt:    cz(c(1, rDenom1Attr)),
			expErr: multiNoAccessErr(types.Access_Transfer, rDenom1Attr, rMarker1Attr.GetAddress(), addrWithDepWithdraw),
		},
		{
			name: "from marker to marker: admin has transfer and deposit and withdraw",
			ctx:  ctxP(types.WithTransferAgents(ctx, addrWithTranDepWithdraw)),
			from: rMarker1Attr.GetAddress(),
			to:   rMarker2Attrs.GetAddress(),
			amt:  cz(c(1, rDenomNoAttr)),
		},
		{
			name: "from marker to marker: 3 admins: transfer, deposit, withdraw",
			ctx:  ctxP(types.WithTransferAgents(ctx, addrWithTransfer, addrWithDeposit, addrWithWithdraw)),
			from: rMarker1Attr.GetAddress(),
			to:   rMarker2Attrs.GetAddress(),
			amt:  cz(c(1, rDenomNoAttr)),
		},
		{
			name: "from marker to marker: 3 admins: transfer, withdraw, deposit",
			ctx:  ctxP(types.WithTransferAgents(ctx, addrWithTransfer, addrWithWithdraw, addrWithDeposit)),
			from: rMarker1Attr.GetAddress(),
			to:   rMarker2Attrs.GetAddress(),
			amt:  cz(c(1, rDenomNoAttr)),
		},
		{
			name: "from marker to marker: 3 admins: withdraw, transfer, deposit",
			ctx:  ctxP(types.WithTransferAgents(ctx, addrWithWithdraw, addrWithTransfer, addrWithDeposit)),
			from: rMarker1Attr.GetAddress(),
			to:   rMarker2Attrs.GetAddress(),
			amt:  cz(c(1, rDenomNoAttr)),
		},
		{
			name: "from marker to marker: 3 admins: withdraw, deposit, transfer",
			ctx:  ctxP(types.WithTransferAgents(ctx, addrWithWithdraw, addrWithDeposit, addrWithTransfer)),
			from: rMarker1Attr.GetAddress(),
			to:   rMarker2Attrs.GetAddress(),
			amt:  cz(c(1, rDenomNoAttr)),
		},
		{
			name: "from marker to marker: 3 admins: deposit, withdraw, transfer",
			ctx:  ctxP(types.WithTransferAgents(ctx, addrWithDeposit, addrWithWithdraw, addrWithTransfer)),
			from: rMarker1Attr.GetAddress(),
			to:   rMarker2Attrs.GetAddress(),
			amt:  cz(c(1, rDenomNoAttr)),
		},
		{
			name: "from marker to marker: 3 admins: deposit, transfer, withdraw",
			ctx:  ctxP(types.WithTransferAgents(ctx, addrWithDeposit, addrWithTransfer, addrWithWithdraw)),
			from: rMarker1Attr.GetAddress(),
			to:   rMarker2Attrs.GetAddress(),
			amt:  cz(c(1, rDenomNoAttr)),
		},
		{
			name: "from marker to marker: 3 admins: trans+dep+withdraw, none, none",
			ctx:  ctxP(types.WithTransferAgents(ctx, addrWithTranDepWithdraw, addrOther, addrOther2)),
			from: rMarker1Attr.GetAddress(),
			to:   rMarker2Attrs.GetAddress(),
			amt:  cz(c(1, rDenomNoAttr)),
		},
		{
			name: "from marker to marker: 3 admins: none, trans+dep+withdraw, none",
			ctx:  ctxP(types.WithTransferAgents(ctx, addrOther, addrWithTranDepWithdraw, addrOther2)),
			from: rMarker1Attr.GetAddress(),
			to:   rMarker2Attrs.GetAddress(),
			amt:  cz(c(1, rDenomNoAttr)),
		},
		{
			name: "from marker to marker: 3 admins: none, none, trans+dep+withdraw",
			ctx:  ctxP(types.WithTransferAgents(ctx, addrOther, addrOther2, addrWithTranDepWithdraw)),
			from: rMarker1Attr.GetAddress(),
			to:   rMarker2Attrs.GetAddress(),
			amt:  cz(c(1, rDenomNoAttr)),
		},
		{
			name: "from marker to marker: 3 admins: trans+dep, withdraw, none",
			ctx:  ctxP(types.WithTransferAgents(ctx, addrWithTranDep, addrWithWithdraw, addrOther)),
			from: rMarker1Attr.GetAddress(),
			to:   rMarker2Attrs.GetAddress(),
			amt:  cz(c(1, rDenomNoAttr)),
		},
		{
			name: "from marker to marker: 3 admins: none, trans+withdraw, dep",
			ctx:  ctxP(types.WithTransferAgents(ctx, addrOther, addrWithTranWithdraw, addrWithDeposit)),
			from: rMarker1Attr.GetAddress(),
			to:   rMarker2Attrs.GetAddress(),
			amt:  cz(c(1, rDenomNoAttr)),
		},
		{
			name: "trans, none, dep+withdraw",
			ctx:  ctxP(types.WithTransferAgents(ctx, addrWithTransfer, addrOther, addrWithDepWithdraw)),
			from: rMarker1Attr.GetAddress(),
			to:   rMarker2Attrs.GetAddress(),
			amt:  cz(c(1, rDenomNoAttr)),
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			tCtx := ctx
			if tc.ctx != nil {
				tCtx = *tc.ctx
			}
			kpr := *app.MarkerKeeper
			if tc.attrKeeper != nil {
				kpr = kpr.WithAttrKeeper(tc.attrKeeper.WithParent(app.AttributeKeeper))
			}
			newTo, err := kpr.SendRestrictionFn(tCtx, tc.from, tc.to, tc.amt)
			if len(tc.expErr) > 0 {
				assert.EqualError(t, err, tc.expErr, "SendRestrictionFn error")
			} else {
				assert.NoError(t, err, "SendRestrictionFn error")
				assert.Equal(t, tc.to, newTo, "SendRestrictionFn returned address")
			}
		})
	}
}

func TestBankSendCoinsUsesSendRestrictionFn(t *testing.T) {
	// This test only checks that the marker SendRestrictionFn is applied during a SendCoins.
	// Testing of the actual SendRestrictionFn is assumed to be done elsewhere more extensively.

	cz := func(amt int64, denom string) sdk.Coins {
		return sdk.NewCoins(sdk.NewInt64Coin(denom, amt))
	}

	markerDenom := "markercoin"

	addrNameOwner := sdk.AccAddress("name_owner__________")
	addrHasWithdraw := sdk.AccAddress("has_withdraw________")
	addrHasAttr := sdk.AccAddress("has_attribute_______")
	addrOther := sdk.AccAddress("other_address_______")

	app := simapp.Setup(t)
	ctx := app.BaseApp.NewContext(false)
	msgServer := markerkeeper.NewMsgServerImpl(*app.MarkerKeeper)
	app.MarkerKeeper.AddMarkerAccount(ctx, types.NewEmptyMarkerAccount("navcoin", addrNameOwner.String(), []types.AccessGrant{}))
	app.AccountKeeper.SetAccount(ctx, app.AccountKeeper.NewAccountWithAddress(ctx, addrNameOwner))
	err := app.NameKeeper.SetNameRecord(ctx, "kyc.provenance.io", addrNameOwner, false)
	require.NoError(t, err, "SetNameRecord kyc.provenance.io")
	require.NoError(t, app.AttributeKeeper.SetAttribute(ctx,
		attrTypes.Attribute{
			Name:          "kyc.provenance.io",
			Value:         []byte("string value"),
			Address:       addrHasAttr.String(),
			AttributeType: attrTypes.AttributeType_String,
		},
		addrNameOwner,
	), "SetAttribute kyc.provenance.io")

	makeMarkerMsg := &types.MsgAddFinalizeActivateMarkerRequest{
		Amount:      sdk.NewInt64Coin(markerDenom, 1000),
		Manager:     addrHasWithdraw.String(),
		FromAddress: addrHasWithdraw.String(),
		MarkerType:  types.MarkerType_RestrictedCoin,
		AccessList: []types.AccessGrant{
			{Address: addrHasWithdraw.String(), Permissions: types.AccessList{types.Access_Withdraw}},
		},
		SupplyFixed:            true,
		AllowGovernanceControl: true,
		AllowForcedTransfer:    false,
		RequiredAttributes:     []string{"kyc.provenance.io"},
	}
	_, err = msgServer.AddFinalizeActivateMarker(ctx, makeMarkerMsg)
	require.NoError(t, err, "makeMarkerMsg")
	err = app.MarkerKeeper.WithdrawCoins(ctx, addrHasWithdraw, addrHasAttr, markerDenom, cz(100, markerDenom))
	require.NoError(t, err, "WithdrawCoins to addrHasTransfer")
	err = app.MarkerKeeper.WithdrawCoins(ctx, addrHasWithdraw, addrOther, markerDenom, cz(100, markerDenom))
	require.NoError(t, err, "WithdrawCoins to addrOther")

	// Done with setup.
	// addrOther and addrHasAttr now each have 100 of the marker denom.
	// addrHasAttr has the attribute needed to receive the denom, and addrOther does not.

	// sendWithCache uses a cache context to call SendCoins, and only writes it if there wasn't an error.
	// This is needed because SendCoins subtracts the amount from the fromAddr before checking the send restriction.
	// That's usually okay because it's all being done in a single transaction, which gets rolled back on error.
	sendWithCache := func(fromAddr, toAddr sdk.AccAddress, amt sdk.Coins) error {
		cacheCtx, writeCache := ctx.CacheContext()
		err = app.BankKeeper.SendCoins(cacheCtx, fromAddr, toAddr, amt)
		if err == nil {
			writeCache()
		}
		return err
	}

	t.Run("send to address without attributes", func(t *testing.T) {
		expErr := fmt.Sprintf("address %s does not contain the %q required attribute: \"kyc.provenance.io\"",
			addrOther, markerDenom)
		err = sendWithCache(addrHasAttr, addrOther, cz(5, markerDenom))
		assert.EqualError(t, err, expErr, "SendCoins")
		expBal := cz(100, markerDenom)
		hasAttrBal := app.BankKeeper.GetBalance(ctx, addrHasAttr, markerDenom)
		assert.Equal(t, expBal.String(), hasAttrBal.String(), "GetBalance addrHasAttr")
		otherBal := app.BankKeeper.GetBalance(ctx, addrOther, markerDenom)
		assert.Equal(t, expBal.String(), otherBal.String(), "GetBalance addrOther")
	})

	t.Run("send to address with attributes", func(t *testing.T) {
		err = sendWithCache(addrOther, addrHasAttr, cz(6, markerDenom))
		assert.NoError(t, err, "SendCoins")
		hasAttrExpBal := cz(106, markerDenom)
		hasAttrBal := app.BankKeeper.GetBalance(ctx, addrHasAttr, markerDenom)
		assert.Equal(t, hasAttrExpBal.String(), hasAttrBal.String(), "GetBalance addrHasAttr")
		otherExpBal := cz(94, markerDenom)
		otherBal := app.BankKeeper.GetBalance(ctx, addrOther, markerDenom)
		assert.Equal(t, otherExpBal.String(), otherBal.String(), "GetBalance addrOther")
	})
}

func TestBankInputOutputCoinsUsesSendRestrictionFn(t *testing.T) {
	// This test only checks that the marker SendRestrictionFn is applied during a InputOutputCoins.
	// Testing of the actual SendRestrictionFn is assumed to be done elsewhere more extensively.

	markerDenom := "cowcoin"
	cz := func(amt int64) sdk.Coins {
		return sdk.NewCoins(sdk.NewInt64Coin(markerDenom, amt))
	}

	addrManager := sdk.AccAddress("addrManager_________")
	addrInput := sdk.AccAddress("addrInput___________")
	addrOutput1 := sdk.AccAddress("addrOutput1_________")
	addrOutput2 := sdk.AccAddress("addrOutput2_________")
	addrWithoutTransfer := sdk.AccAddress("addrWithoutTransfer_")
	addrWithAttr1 := sdk.AccAddress("addrWithAttr1_______")
	addrWithAttr2 := sdk.AccAddress("addrWithAttr2_______")

	app := simapp.Setup(t)
	ctx := app.BaseApp.NewContext(false)
	msgServer := markerkeeper.NewMsgServerImpl(*app.MarkerKeeper)
	app.MarkerKeeper.AddMarkerAccount(ctx, types.NewEmptyMarkerAccount("navcoin", addrManager.String(), []types.AccessGrant{}))
	app.AccountKeeper.SetAccount(ctx, app.AccountKeeper.NewAccountWithAddress(ctx, addrManager))
	err := app.NameKeeper.SetNameRecord(ctx, "rando.io", addrManager, false)
	require.NoError(t, err, "SetNameRecord rando.io")
	require.NoError(t, app.AttributeKeeper.SetAttribute(ctx,
		attrTypes.Attribute{
			Name:          "rando.io",
			Value:         []byte("random value 1"),
			Address:       addrWithAttr1.String(),
			AttributeType: attrTypes.AttributeType_String,
		},
		addrManager,
	), "SetAttribute rando.io on addrWithAttr1")
	require.NoError(t, app.AttributeKeeper.SetAttribute(ctx,
		attrTypes.Attribute{
			Name:          "rando.io",
			Value:         []byte("random value 2"),
			Address:       addrWithAttr2.String(),
			AttributeType: attrTypes.AttributeType_String,
		},
		addrManager,
	), "SetAttribute rando.io on addrWithAttr2")

	makeMarkerMsg := &types.MsgAddFinalizeActivateMarkerRequest{
		Amount:      sdk.NewInt64Coin(markerDenom, 1000),
		Manager:     addrManager.String(),
		FromAddress: addrManager.String(),
		MarkerType:  types.MarkerType_RestrictedCoin,
		AccessList: []types.AccessGrant{
			{Address: addrManager.String(), Permissions: types.AccessList{
				types.Access_Mint, types.Access_Burn,
				types.Access_Deposit, types.Access_Withdraw,
				types.Access_Delete, types.Access_Admin, types.Access_Transfer,
			}},
		},
		SupplyFixed:            true,
		AllowGovernanceControl: true,
		AllowForcedTransfer:    false,
		RequiredAttributes:     []string{"rando.io"},
	}
	_, err = msgServer.AddFinalizeActivateMarker(ctx, makeMarkerMsg)
	require.NoError(t, err, "MsgAddFinalizeActivateMarkerRequest")
	err = app.MarkerKeeper.WithdrawCoins(ctx, addrManager, addrManager, markerDenom, cz(100))
	require.NoError(t, err, "WithdrawCoins to addrInput")
	err = app.MarkerKeeper.WithdrawCoins(ctx, addrManager, addrInput, markerDenom, cz(100))
	require.NoError(t, err, "WithdrawCoins to addrInput")
	err = app.MarkerKeeper.WithdrawCoins(ctx, addrManager, addrWithoutTransfer, markerDenom, cz(100))
	require.NoError(t, err, "WithdrawCoins to addrWithoutTransfer")

	type expBal struct {
		name string
		addr sdk.AccAddress
		bal  sdk.Coins
	}
	newExpBal := func(name string, addr sdk.AccAddress, bal sdk.Coins) expBal {
		return expBal{
			name: name,
			addr: addr,
			bal:  bal,
		}
	}
	assertBalance := func(t *testing.T, exp expBal) bool {
		t.Helper()
		bal := app.BankKeeper.GetBalance(ctx, exp.addr, markerDenom)
		return assert.Equal(t, exp.bal.String(), bal.String(), "GetBalance %s", exp.name)
	}

	noAttrErr := func(addr sdk.AccAddress) string {
		return fmt.Sprintf("address %s does not contain the %q required attribute: %q",
			addr.String(), markerDenom, "rando.io")
	}

	tests := []struct {
		name    string
		input   banktypes.Input
		outputs []banktypes.Output
		expErr  string
		expBals []expBal
	}{
		{
			name:  "from address with transfer permission",
			input: banktypes.Input{Address: addrManager.String(), Coins: cz(99)},
			outputs: []banktypes.Output{
				{Address: addrOutput1.String(), Coins: cz(33)},
				{Address: addrOutput2.String(), Coins: cz(66)},
			},
			expErr: "",
			expBals: []expBal{
				newExpBal("addrManager", addrManager, cz(1)),
				newExpBal("addrOutput1", addrOutput1, cz(33)),
				newExpBal("addrOutput2", addrOutput2, cz(66)),
			},
		},
		{
			name:  "from address without transfer permission",
			input: banktypes.Input{Address: addrInput.String(), Coins: cz(100)},
			outputs: []banktypes.Output{
				{Address: addrOutput1.String(), Coins: cz(60)},
				{Address: addrOutput2.String(), Coins: cz(40)},
			},
			expErr: noAttrErr(addrOutput1),
			// Note: The input coins are subtracted before running the restriction function.
			//       Usually this is done in a transaction so the error would roll it back.
			//       Here, we just skip checking that balance.
			expBals: []expBal{
				newExpBal("addrOutput1", addrOutput1, cz(33)), // from previous test
				newExpBal("addrOutput2", addrOutput2, cz(66)), // from previous test
			},
		},
		{
			name:  "to addresses with attributes",
			input: banktypes.Input{Address: addrWithoutTransfer.String(), Coins: cz(77)},
			outputs: []banktypes.Output{
				{Address: addrWithAttr1.String(), Coins: cz(33)},
				{Address: addrWithAttr2.String(), Coins: cz(44)},
			},
			expErr: "",
			expBals: []expBal{
				newExpBal("addrWithoutTransfer", addrWithoutTransfer, cz(23)),
				newExpBal("addrWithAttr1", addrWithAttr1, cz(33)),
				newExpBal("addrWithAttr2", addrWithAttr2, cz(44)),
			},
		},
		{
			name:  "to one address with and one without",
			input: banktypes.Input{Address: addrWithoutTransfer.String(), Coins: cz(20)},
			outputs: []banktypes.Output{
				{Address: addrWithAttr1.String(), Coins: cz(3)},
				{Address: addrOutput2.String(), Coins: cz(17)},
			},
			expErr: noAttrErr(addrOutput2),
			// Note: Here too, the input should come out and the first output go through before getting the error.
			//       Normally, that'd get rolled back because of the error, but we're not in a Tx here.
			//       So all I'm going to do is check that the last output didn't go through.
			expBals: []expBal{newExpBal("addrOutput2", addrOutput2, cz(66))}, // from first test.
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			err = app.BankKeeper.InputOutputCoins(ctx, tc.input, tc.outputs)
			if len(tc.expErr) != 0 {
				assert.EqualError(t, err, tc.expErr, "InputOutputCoins")
			} else {
				assert.NoError(t, err, "InputOutputCoins")
			}

			for _, exp := range tc.expBals {
				assertBalance(t, exp)
			}
		})
	}
}

func TestNormalizeRequiredAttributes(t *testing.T) {
	app := simapp.Setup(t)
	ctx := app.BaseApp.NewContext(false)

	testCases := []struct {
		name               string
		requiredAttributes []string
		expectedNormalized []string
		expectedError      string
	}{
		{
			name:               "should succeed - empty required attrs",
			requiredAttributes: []string{},
			expectedNormalized: []string{},
			expectedError:      "",
		},
		{
			name:               "should fail - segment name too short",
			requiredAttributes: []string{"."},
			expectedNormalized: []string{},
			expectedError:      "segment of name is too short",
		},
		{
			name:               "should fail - segment name too short2",
			requiredAttributes: []string{"provenance.io"},
			expectedNormalized: []string{"provenance.io"},
			expectedError:      "",
		},
		{
			name:               "should fail - invalid wild card value",
			requiredAttributes: []string{"*b.provenance.io"},
			expectedNormalized: []string{},
			expectedError:      "value provided for name is invalid",
		},
		{
			name:               "should succeed - valid wild card value",
			requiredAttributes: []string{"*.provenance.io"},
			expectedNormalized: []string{"*.provenance.io"},
			expectedError:      "",
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			result, err := app.MarkerKeeper.NormalizeRequiredAttributes(ctx, tc.requiredAttributes)
			if len(tc.expectedError) > 0 {
				require.EqualError(t, err, tc.expectedError, "NormalizeRequiredAttributes error")
			} else {
				require.NoError(t, err, "NormalizeRequiredAttributes error")
				require.Equal(t, tc.expectedNormalized, result, "NormalizeRequiredAttributes result")
			}
		})
	}
}

func TestMatchAttribute(t *testing.T) {
	testCases := []struct {
		name           string
		reqAttr        string
		attr           string
		expectedResult bool
	}{
		{
			name:           "should succeed - wildcard on single name",
			reqAttr:        "*.provenance.io",
			attr:           "test.provenance.io",
			expectedResult: true,
		},
		{
			name:           "should succeed - wildcard on multiple names",
			reqAttr:        "*.provenance.io",
			attr:           "test.test.test.provenance.io",
			expectedResult: true,
		},
		{
			name:           "should succeed - literal match",
			reqAttr:        "test.provenance.io",
			attr:           "test.provenance.io",
			expectedResult: true,
		},
		{
			name:           "should fail - wildcard match",
			reqAttr:        "*.provenance.io",
			attr:           "test.provenance.com",
			expectedResult: false,
		},
		{
			name:           "should fail - literal match",
			reqAttr:        "test.provenance.io",
			attr:           "test.provenance.com",
			expectedResult: false,
		},
		{
			name:           "should fail - empty required attr",
			reqAttr:        "",
			attr:           "test.provenance.com",
			expectedResult: false,
		},
		{
			name:           "should fail - empty required attr and attr",
			reqAttr:        "",
			attr:           "",
			expectedResult: false,
		},
		{
			name:           "should fail - extra ending",
			reqAttr:        "test.provenance.io",
			attr:           "test.provenance.iox",
			expectedResult: false,
		},
		{
			name:           "should fail - wildcard extra ending",
			reqAttr:        "*.provenance.io",
			attr:           "test.provenance.iox",
			expectedResult: false,
		},
		{
			name:           "should fail - wildcard extra beginning",
			reqAttr:        "*.provenance.io",
			attr:           "test.xprovenance.io",
			expectedResult: false,
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			result := keeper.MatchAttribute(tc.reqAttr, tc.attr)
			require.Equal(t, tc.expectedResult, result, "MatchAttribute")
		})
	}
}
