package app

import (
	"fmt"
	"math/big"
	"testing"

	ethcmn "github.com/ethereum/go-ethereum/common"
	"github.com/stretchr/testify/require"
	tmcrypto "github.com/tendermint/tendermint/crypto"

	sdk "github.com/cosmos/cosmos-sdk/types"
	"github.com/cosmos/cosmos-sdk/x/auth"
	"github.com/cosmos/ethermint/types"
	evmtypes "github.com/cosmos/ethermint/x/evm/types"
)

func requireValidTx(
	t *testing.T, anteHandler sdk.AnteHandler, ctx sdk.Context, tx sdk.Tx, sim bool,
) {

	_, result, abort := anteHandler(ctx, tx, sim)
	require.Equal(t, sdk.CodeOK, result.Code, result.Log)
	require.False(t, abort)
	require.True(t, result.IsOK())
}

func requireInvalidTx(
	t *testing.T, anteHandler sdk.AnteHandler, ctx sdk.Context,
	tx sdk.Tx, sim bool, code sdk.CodeType,
) {

	newCtx, result, abort := anteHandler(ctx, tx, sim)
	require.True(t, abort)
	require.Equal(t, code, result.Code, fmt.Sprintf("invalid result: %v", result))

	if code == sdk.CodeOutOfGas {
		stdTx, ok := tx.(auth.StdTx)
		require.True(t, ok, "tx must be in form auth.StdTx")

		// require GasWanted is set correctly
		require.Equal(t, stdTx.Fee.Gas, result.GasWanted, "'GasWanted' wanted not set correctly")
		require.True(t, result.GasUsed > result.GasWanted, "'GasUsed' not greater than GasWanted")

		// require that context is set correctly
		require.Equal(t, result.GasUsed, newCtx.GasMeter().GasConsumed(), "Context not updated correctly")
	}
}

func TestValidEthTx(t *testing.T) {
	input := newTestSetup()
	input.ctx = input.ctx.WithBlockHeight(1)

	addr1, priv1 := newTestAddrKey()
	addr2, _ := newTestAddrKey()

	acc1 := input.accKeeper.NewAccountWithAddress(input.ctx, addr1)
	// nolint:errcheck
	acc1.SetCoins(newTestCoins())
	input.accKeeper.SetAccount(input.ctx, acc1)

	acc2 := input.accKeeper.NewAccountWithAddress(input.ctx, addr2)
	// nolint:errcheck
	acc2.SetCoins(newTestCoins())
	input.accKeeper.SetAccount(input.ctx, acc2)

	// require a valid Ethereum tx to pass
	to := ethcmn.BytesToAddress(addr2.Bytes())
	amt := big.NewInt(32)
	gas := big.NewInt(20)
	ethMsg := evmtypes.NewEthereumTxMsg(0, to, amt, 22000, gas, []byte("test"))

	tx := newTestEthTx(input.ctx, ethMsg, priv1)
	requireValidTx(t, input.anteHandler, input.ctx, tx, false)
}

func TestValidTx(t *testing.T) {
	input := newTestSetup()
	input.ctx = input.ctx.WithBlockHeight(1)

	addr1, priv1 := newTestAddrKey()
	addr2, priv2 := newTestAddrKey()

	acc1 := input.accKeeper.NewAccountWithAddress(input.ctx, addr1)
	// nolint:errcheck
	acc1.SetCoins(newTestCoins())
	input.accKeeper.SetAccount(input.ctx, acc1)

	acc2 := input.accKeeper.NewAccountWithAddress(input.ctx, addr2)
	// nolint:errcheck
	acc2.SetCoins(newTestCoins())
	input.accKeeper.SetAccount(input.ctx, acc2)

	// require a valid SDK tx to pass
	fee := newTestStdFee()
	msg1 := newTestMsg(addr1, addr2)
	msgs := []sdk.Msg{msg1}

	privKeys := []tmcrypto.PrivKey{priv1, priv2}
	accNums := []uint64{acc1.GetAccountNumber(), acc2.GetAccountNumber()}
	accSeqs := []uint64{acc1.GetSequence(), acc2.GetSequence()}

	tx := newTestSDKTx(input.ctx, msgs, privKeys, accNums, accSeqs, fee)
	requireValidTx(t, input.anteHandler, input.ctx, tx, false)

	// require accounts to update
	acc1 = input.accKeeper.GetAccount(input.ctx, addr1)
	acc2 = input.accKeeper.GetAccount(input.ctx, addr2)
	require.Equal(t, accSeqs[0]+1, acc1.GetSequence())
	require.Equal(t, accSeqs[1]+1, acc2.GetSequence())
}

func TestSDKInvalidSigs(t *testing.T) {
	input := newTestSetup()
	input.ctx = input.ctx.WithBlockHeight(1)

	addr1, priv1 := newTestAddrKey()
	addr2, priv2 := newTestAddrKey()
	addr3, priv3 := newTestAddrKey()

	acc1 := input.accKeeper.NewAccountWithAddress(input.ctx, addr1)
	// nolint:errcheck
	acc1.SetCoins(newTestCoins())
	input.accKeeper.SetAccount(input.ctx, acc1)

	acc2 := input.accKeeper.NewAccountWithAddress(input.ctx, addr2)
	// nolint:errcheck
	acc2.SetCoins(newTestCoins())
	input.accKeeper.SetAccount(input.ctx, acc2)

	fee := newTestStdFee()
	msg1 := newTestMsg(addr1, addr2)

	// require validation failure with no signers
	msgs := []sdk.Msg{msg1}

	privKeys := []tmcrypto.PrivKey{}
	accNums := []uint64{acc1.GetAccountNumber(), acc2.GetAccountNumber()}
	accSeqs := []uint64{acc1.GetSequence(), acc2.GetSequence()}

	tx := newTestSDKTx(input.ctx, msgs, privKeys, accNums, accSeqs, fee)
	requireInvalidTx(t, input.anteHandler, input.ctx, tx, false, sdk.CodeNoSignatures)

	// require validation failure with invalid number of signers
	msgs = []sdk.Msg{msg1}

	privKeys = []tmcrypto.PrivKey{priv1}
	accNums = []uint64{acc1.GetAccountNumber(), acc2.GetAccountNumber()}
	accSeqs = []uint64{acc1.GetSequence(), acc2.GetSequence()}

	tx = newTestSDKTx(input.ctx, msgs, privKeys, accNums, accSeqs, fee)
	requireInvalidTx(t, input.anteHandler, input.ctx, tx, false, sdk.CodeUnauthorized)

	// require validation failure with an invalid signer
	msg2 := newTestMsg(addr1, addr3)
	msgs = []sdk.Msg{msg1, msg2}

	privKeys = []tmcrypto.PrivKey{priv1, priv2, priv3}
	accNums = []uint64{acc1.GetAccountNumber(), acc2.GetAccountNumber(), 0}
	accSeqs = []uint64{acc1.GetSequence(), acc2.GetSequence(), 0}

	tx = newTestSDKTx(input.ctx, msgs, privKeys, accNums, accSeqs, fee)
	requireInvalidTx(t, input.anteHandler, input.ctx, tx, false, sdk.CodeUnknownAddress)
}

func TestSDKInvalidAcc(t *testing.T) {
	input := newTestSetup()
	input.ctx = input.ctx.WithBlockHeight(1)

	addr1, priv1 := newTestAddrKey()

	acc1 := input.accKeeper.NewAccountWithAddress(input.ctx, addr1)
	// nolint:errcheck
	acc1.SetCoins(newTestCoins())
	input.accKeeper.SetAccount(input.ctx, acc1)

	fee := newTestStdFee()
	msg1 := newTestMsg(addr1)
	msgs := []sdk.Msg{msg1}
	privKeys := []tmcrypto.PrivKey{priv1}

	// require validation failure with invalid account number
	accNums := []uint64{1}
	accSeqs := []uint64{acc1.GetSequence()}

	tx := newTestSDKTx(input.ctx, msgs, privKeys, accNums, accSeqs, fee)
	requireInvalidTx(t, input.anteHandler, input.ctx, tx, false, sdk.CodeUnauthorized)

	// TODO: Reenable broken test when fixed inside cosmos SDK
	// // require validation failure with invalid sequence (nonce)
	// accNums = []uint64{acc1.GetAccountNumber()}
	// accSeqs = []uint64{1}

	// tx = newTestSDKTx(input.ctx, msgs, privKeys, accNums, accSeqs, fee)
	// requireInvalidTx(t, input.anteHandler, input.ctx, tx, false, sdk.CodeUnauthorized)
}

func TestEthInvalidSig(t *testing.T) {
	input := newTestSetup()
	input.ctx = input.ctx.WithBlockHeight(1)

	_, priv1 := newTestAddrKey()
	addr2, _ := newTestAddrKey()
	to := ethcmn.BytesToAddress(addr2.Bytes())
	amt := big.NewInt(32)
	gas := big.NewInt(20)
	ethMsg := evmtypes.NewEthereumTxMsg(0, to, amt, 22000, gas, []byte("test"))

	tx := newTestEthTx(input.ctx, ethMsg, priv1)
	ctx := input.ctx.WithChainID("4")
	requireInvalidTx(t, input.anteHandler, ctx, tx, false, sdk.CodeUnauthorized)
}

func TestEthInvalidNonce(t *testing.T) {
	input := newTestSetup()
	input.ctx = input.ctx.WithBlockHeight(1)

	addr1, priv1 := newTestAddrKey()
	addr2, _ := newTestAddrKey()

	acc := input.accKeeper.NewAccountWithAddress(input.ctx, addr1)
	// nolint:errcheck
	acc.SetCoins(newTestCoins())
	// nolint:errcheck
	acc.SetSequence(10)
	input.accKeeper.SetAccount(input.ctx, acc)

	// require a valid Ethereum tx to pass
	to := ethcmn.BytesToAddress(addr2.Bytes())
	amt := big.NewInt(32)
	gas := big.NewInt(20)
	ethMsg := evmtypes.NewEthereumTxMsg(0, to, amt, 22000, gas, []byte("test"))

	tx := newTestEthTx(input.ctx, ethMsg, priv1)
	requireInvalidTx(t, input.anteHandler, input.ctx, tx, false, sdk.CodeInvalidSequence)
}

func TestEthInsufficientBalance(t *testing.T) {
	input := newTestSetup()
	input.ctx = input.ctx.WithBlockHeight(1)

	addr1, priv1 := newTestAddrKey()
	addr2, _ := newTestAddrKey()

	acc := input.accKeeper.NewAccountWithAddress(input.ctx, addr1)
	input.accKeeper.SetAccount(input.ctx, acc)

	// require a valid Ethereum tx to pass
	to := ethcmn.BytesToAddress(addr2.Bytes())
	amt := big.NewInt(32)
	gas := big.NewInt(20)
	ethMsg := evmtypes.NewEthereumTxMsg(0, to, amt, 22000, gas, []byte("test"))

	tx := newTestEthTx(input.ctx, ethMsg, priv1)
	requireInvalidTx(t, input.anteHandler, input.ctx, tx, false, sdk.CodeInsufficientFunds)
}

func TestEthInvalidIntrinsicGas(t *testing.T) {
	input := newTestSetup()
	input.ctx = input.ctx.WithBlockHeight(1)

	addr1, priv1 := newTestAddrKey()
	addr2, _ := newTestAddrKey()

	acc := input.accKeeper.NewAccountWithAddress(input.ctx, addr1)
	// nolint:errcheck
	acc.SetCoins(newTestCoins())
	input.accKeeper.SetAccount(input.ctx, acc)

	// require a valid Ethereum tx to pass
	to := ethcmn.BytesToAddress(addr2.Bytes())
	amt := big.NewInt(32)
	gas := big.NewInt(20)
	gasLimit := uint64(1000)
	ethMsg := evmtypes.NewEthereumTxMsg(0, to, amt, gasLimit, gas, []byte("test"))

	tx := newTestEthTx(input.ctx, ethMsg, priv1)
	requireInvalidTx(t, input.anteHandler, input.ctx, tx, false, sdk.CodeInternal)
}

func TestEthInvalidMempoolFees(t *testing.T) {
	input := newTestSetup()
	input.ctx = input.ctx.WithBlockHeight(1)
	input.ctx = input.ctx.WithMinGasPrices(sdk.DecCoins{sdk.NewDecCoin(types.DenomDefault, sdk.NewInt(500000))})

	addr1, priv1 := newTestAddrKey()
	addr2, _ := newTestAddrKey()

	acc := input.accKeeper.NewAccountWithAddress(input.ctx, addr1)
	// nolint:errcheck
	acc.SetCoins(newTestCoins())
	input.accKeeper.SetAccount(input.ctx, acc)

	// require a valid Ethereum tx to pass
	to := ethcmn.BytesToAddress(addr2.Bytes())
	amt := big.NewInt(32)
	gas := big.NewInt(20)
	ethMsg := evmtypes.NewEthereumTxMsg(0, to, amt, 22000, gas, []byte("test"))

	tx := newTestEthTx(input.ctx, ethMsg, priv1)
	requireInvalidTx(t, input.anteHandler, input.ctx, tx, false, sdk.CodeInsufficientFee)
}

func TestEthInvalidChainID(t *testing.T) {
	input := newTestSetup()
	input.ctx = input.ctx.WithBlockHeight(1)

	addr1, priv1 := newTestAddrKey()
	addr2, _ := newTestAddrKey()

	acc := input.accKeeper.NewAccountWithAddress(input.ctx, addr1)
	// nolint:errcheck
	acc.SetCoins(newTestCoins())
	input.accKeeper.SetAccount(input.ctx, acc)

	// require a valid Ethereum tx to pass
	to := ethcmn.BytesToAddress(addr2.Bytes())
	amt := big.NewInt(32)
	gas := big.NewInt(20)
	ethMsg := evmtypes.NewEthereumTxMsg(0, to, amt, 22000, gas, []byte("test"))

	tx := newTestEthTx(input.ctx, ethMsg, priv1)
	ctx := input.ctx.WithChainID("bad-chain-id")
	requireInvalidTx(t, input.anteHandler, ctx, tx, false, types.CodeInvalidChainID)
}
