package tx

import (
	"testing"

	"github.com/stretchr/testify/require"

	"github.com/cosmos/cosmos-sdk/codec"
	"github.com/cosmos/cosmos-sdk/codec/legacy"
	codectypes "github.com/cosmos/cosmos-sdk/codec/types"
	"github.com/cosmos/cosmos-sdk/testutil/testdata"
	sdk "github.com/cosmos/cosmos-sdk/types"
	sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
	txtypes "github.com/cosmos/cosmos-sdk/types/tx"
	"github.com/cosmos/cosmos-sdk/types/tx/signing"
)

func TestTxBuilder(t *testing.T) {
	_, pubkey, addr := testdata.KeyTestPubAddr()

	marshaler := codec.NewProtoCodec(codectypes.NewInterfaceRegistry())
	txBuilder := newBuilder()

	memo := "sometestmemo"
	msgs := []sdk.Msg{testdata.NewTestMsg(addr)}
	accSeq := uint64(2) // Arbitrary account sequence
	any, err := codectypes.NewAnyWithValue(pubkey)
	require.NoError(t, err)

	var signerInfo []*txtypes.SignerInfo
	signerInfo = append(signerInfo, &txtypes.SignerInfo{
		PublicKey: any,
		ModeInfo: &txtypes.ModeInfo{
			Sum: &txtypes.ModeInfo_Single_{
				Single: &txtypes.ModeInfo_Single{
					Mode: signing.SignMode_SIGN_MODE_DIRECT,
				},
			},
		},
		Sequence: accSeq,
	})

	var sig signing.SignatureV2 = signing.SignatureV2{
		PubKey: pubkey,
		Data: &signing.SingleSignatureData{
			SignMode:  signing.SignMode_SIGN_MODE_DIRECT,
			Signature: legacy.Cdc.MustMarshalBinaryBare(pubkey),
		},
		Sequence: accSeq,
	}

	fee := txtypes.Fee{Amount: sdk.NewCoins(sdk.NewInt64Coin("atom", 150)), GasLimit: 20000}

	t.Log("verify that authInfo bytes encoded with DefaultTxEncoder and decoded with DefaultTxDecoder can be retrieved from getAuthInfoBytes")
	authInfo := &txtypes.AuthInfo{
		Fee:         &fee,
		SignerInfos: signerInfo,
	}

	authInfoBytes := marshaler.MustMarshalBinaryBare(authInfo)

	require.NotEmpty(t, authInfoBytes)

	t.Log("verify that body bytes encoded with DefaultTxEncoder and decoded with DefaultTxDecoder can be retrieved from getBodyBytes")
	anys := make([]*codectypes.Any, len(msgs))

	for i, msg := range msgs {
		var err error
		anys[i], err = codectypes.NewAnyWithValue(msg)
		if err != nil {
			panic(err)
		}
	}

	txBody := &txtypes.TxBody{
		Memo:     memo,
		Messages: anys,
	}
	bodyBytes := marshaler.MustMarshalBinaryBare(txBody)
	require.NotEmpty(t, bodyBytes)
	require.Empty(t, txBuilder.getBodyBytes())

	t.Log("verify that calling the SetMsgs, SetMemo results in the correct getBodyBytes")
	require.NotEqual(t, bodyBytes, txBuilder.getBodyBytes())
	err = txBuilder.SetMsgs(msgs...)
	require.NoError(t, err)
	require.NotEqual(t, bodyBytes, txBuilder.getBodyBytes())
	txBuilder.SetMemo(memo)
	require.Equal(t, bodyBytes, txBuilder.getBodyBytes())
	require.Equal(t, len(msgs), len(txBuilder.GetMsgs()))
	pks, err := txBuilder.GetPubKeys()
	require.NoError(t, err)
	require.Empty(t, pks)

	t.Log("verify that updated AuthInfo  results in the correct getAuthInfoBytes and GetPubKeys")
	require.NotEqual(t, authInfoBytes, txBuilder.getAuthInfoBytes())
	txBuilder.SetFeeAmount(fee.Amount)
	require.NotEqual(t, authInfoBytes, txBuilder.getAuthInfoBytes())
	txBuilder.SetGasLimit(fee.GasLimit)
	require.NotEqual(t, authInfoBytes, txBuilder.getAuthInfoBytes())
	err = txBuilder.SetSignatures(sig)
	require.NoError(t, err)

	// once fee, gas and signerInfos are all set, AuthInfo bytes should match
	require.Equal(t, authInfoBytes, txBuilder.getAuthInfoBytes())

	require.Equal(t, len(msgs), len(txBuilder.GetMsgs()))
	pks, err = txBuilder.GetPubKeys()
	require.NoError(t, err)
	require.Equal(t, 1, len(pks))
	require.True(t, pubkey.Equals(pks[0]))

	any, err = codectypes.NewAnyWithValue(testdata.NewTestMsg())
	require.NoError(t, err)
	txBuilder.SetExtensionOptions(any)
	require.Equal(t, []*codectypes.Any{any}, txBuilder.GetExtensionOptions())
	txBuilder.SetNonCriticalExtensionOptions(any)
	require.Equal(t, []*codectypes.Any{any}, txBuilder.GetNonCriticalExtensionOptions())

	txBuilder = &wrapper{}
	require.NotPanics(t, func() {
		_ = txBuilder.GetMsgs()
	})
}

func TestBuilderValidateBasic(t *testing.T) {
	// keys and addresses
	_, pubKey1, addr1 := testdata.KeyTestPubAddr()
	_, pubKey2, addr2 := testdata.KeyTestPubAddr()

	// msg and signatures
	msg1 := testdata.NewTestMsg(addr1, addr2)
	feeAmount := testdata.NewTestFeeAmount()
	msgs := []sdk.Msg{msg1}

	// require to fail validation upon invalid fee
	badFeeAmount := testdata.NewTestFeeAmount()
	badFeeAmount[0].Amount = sdk.NewInt(-5)
	txBuilder := newBuilder()

	var sig1, sig2 signing.SignatureV2
	sig1 = signing.SignatureV2{
		PubKey: pubKey1,
		Data: &signing.SingleSignatureData{
			SignMode:  signing.SignMode_SIGN_MODE_DIRECT,
			Signature: legacy.Cdc.MustMarshalBinaryBare(pubKey1),
		},
		Sequence: 0, // Arbitrary account sequence
	}

	sig2 = signing.SignatureV2{
		PubKey: pubKey2,
		Data: &signing.SingleSignatureData{
			SignMode:  signing.SignMode_SIGN_MODE_DIRECT,
			Signature: legacy.Cdc.MustMarshalBinaryBare(pubKey2),
		},
		Sequence: 0, // Arbitrary account sequence
	}

	err := txBuilder.SetMsgs(msgs...)
	require.NoError(t, err)
	txBuilder.SetGasLimit(200000)
	err = txBuilder.SetSignatures(sig1, sig2)
	require.NoError(t, err)
	txBuilder.SetFeeAmount(badFeeAmount)
	err = txBuilder.ValidateBasic()
	require.Error(t, err)
	_, code, _ := sdkerrors.ABCIInfo(err, false)
	require.Equal(t, sdkerrors.ErrInsufficientFee.ABCICode(), code)

	// require to fail validation when no signatures exist
	err = txBuilder.SetSignatures()
	require.NoError(t, err)
	txBuilder.SetFeeAmount(feeAmount)
	err = txBuilder.ValidateBasic()
	require.Error(t, err)
	_, code, _ = sdkerrors.ABCIInfo(err, false)
	require.Equal(t, sdkerrors.ErrNoSignatures.ABCICode(), code)

	// require to fail with nil values for tx, authinfo
	err = txBuilder.SetMsgs(msgs...)
	require.NoError(t, err)
	err = txBuilder.ValidateBasic()
	require.Error(t, err)

	// require to fail validation when signatures do not match expected signers
	err = txBuilder.SetSignatures(sig1)
	require.NoError(t, err)

	err = txBuilder.ValidateBasic()
	require.Error(t, err)
	_, code, _ = sdkerrors.ABCIInfo(err, false)
	require.Equal(t, sdkerrors.ErrUnauthorized.ABCICode(), code)

	require.Error(t, err)
	txBuilder.SetFeeAmount(feeAmount)
	err = txBuilder.SetSignatures(sig1, sig2)
	require.NoError(t, err)
	err = txBuilder.ValidateBasic()
	require.NoError(t, err)

	// gas limit too high
	txBuilder.SetGasLimit(txtypes.MaxGasWanted + 1)
	err = txBuilder.ValidateBasic()
	require.Error(t, err)
	txBuilder.SetGasLimit(txtypes.MaxGasWanted - 1)
	err = txBuilder.ValidateBasic()
	require.NoError(t, err)

	// bad builder structs

	// missing body
	body := txBuilder.tx.Body
	txBuilder.tx.Body = nil
	err = txBuilder.ValidateBasic()
	require.Error(t, err)
	txBuilder.tx.Body = body
	err = txBuilder.ValidateBasic()
	require.NoError(t, err)

	// missing fee
	f := txBuilder.tx.AuthInfo.Fee
	txBuilder.tx.AuthInfo.Fee = nil
	err = txBuilder.ValidateBasic()
	require.Error(t, err)
	txBuilder.tx.AuthInfo.Fee = f
	err = txBuilder.ValidateBasic()
	require.NoError(t, err)

	// missing AuthInfo
	authInfo := txBuilder.tx.AuthInfo
	txBuilder.tx.AuthInfo = nil
	err = txBuilder.ValidateBasic()
	require.Error(t, err)
	txBuilder.tx.AuthInfo = authInfo
	err = txBuilder.ValidateBasic()
	require.NoError(t, err)

	// missing tx
	txBuilder.tx = nil
	err = txBuilder.ValidateBasic()
	require.Error(t, err)
}

func TestBuilderFeePayer(t *testing.T) {
	// keys and addresses
	_, _, addr1 := testdata.KeyTestPubAddr()
	_, _, addr2 := testdata.KeyTestPubAddr()
	_, _, addr3 := testdata.KeyTestPubAddr()

	// msg and signatures
	msg1 := testdata.NewTestMsg(addr1, addr2)
	feeAmount := testdata.NewTestFeeAmount()
	msgs := []sdk.Msg{msg1}

	cases := map[string]struct {
		txFeePayer      sdk.AccAddress
		expectedSigners []sdk.AccAddress
		expectedPayer   sdk.AccAddress
	}{
		"no fee payer specified": {
			expectedSigners: []sdk.AccAddress{addr1, addr2},
			expectedPayer:   addr1,
		},
		"secondary signer set as fee payer": {
			txFeePayer:      addr2,
			expectedSigners: []sdk.AccAddress{addr1, addr2},
			expectedPayer:   addr2,
		},
		"outside signer set as fee payer": {
			txFeePayer:      addr3,
			expectedSigners: []sdk.AccAddress{addr1, addr2, addr3},
			expectedPayer:   addr3,
		},
	}

	for name, tc := range cases {
		t.Run(name, func(t *testing.T) {
			// setup basic tx
			txBuilder := newBuilder()
			err := txBuilder.SetMsgs(msgs...)
			require.NoError(t, err)
			txBuilder.SetGasLimit(200000)
			txBuilder.SetFeeAmount(feeAmount)

			// set fee payer
			txBuilder.SetFeePayer(tc.txFeePayer)
			// and check it updates fields properly
			require.Equal(t, tc.expectedSigners, txBuilder.GetSigners())
			require.Equal(t, tc.expectedPayer, txBuilder.FeePayer())
		})
	}
}

func TestBuilderFeeGranter(t *testing.T) {
	// keys and addresses
	_, _, addr1 := testdata.KeyTestPubAddr()

	// msg and signatures
	msg1 := testdata.NewTestMsg(addr1, addr2)
	feeAmount := testdata.NewTestFeeAmount()
	msgs := []sdk.Msg{msg1}

	txBuilder := newBuilder()
	err := txBuilder.SetMsgs(msgs...)
	require.NoError(t, err)
	txBuilder.SetGasLimit(200000)
	txBuilder.SetFeeAmount(feeAmount)

	require.Empty(t, txBuilder.GetTx().FeeGranter())

	// set fee granter
	txBuilder.SetFeeGranter(addr1)
	require.Equal(t, addr1, txBuilder.GetTx().FeeGranter())
}
