package e2e_test

import (
	"context"
	"math/big"

	sdk "github.com/cosmos/cosmos-sdk/types"
	authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
	"github.com/ethereum/go-ethereum/common"
	"github.com/ethereum/go-ethereum/crypto"
	ethermint "github.com/evmos/ethermint/types"
	evmtypes "github.com/evmos/ethermint/x/evm/types"

	"github.com/kava-labs/kava/precompile/registry"
)

// TestPrecompileGenesis tests that the the following is true for enabled precompiles:
//
//   - An enabled precompile has an EthAccount with matching code hash,
//     sequence of 1, and no public key
//   - An enabled precompile has code equal to 0x01
//   - An enabled precompile has a nonce of 1
//
// This is important to ensure the genesis setup for precompiles is correct.
func (suite *IntegrationTestSuite) TestPrecompileGenesis() {
	type fixture struct {
		address            string
		expectIsEnabled    bool
		expectIsEthAccount bool
		expectCode         []byte
		expectNonce        uint64
	}

	// enabled represnets the expected state for a registered precompile
	// that is enabled at genesis
	enabled := func(address string) func() fixture {
		return func() fixture {
			return fixture{
				address:            address,
				expectIsEnabled:    true,
				expectIsEthAccount: true,
				expectCode:         []byte{0x01},
				expectNonce:        uint64(1),
			}
		}
	}

	// disabled represents the expected state for a registered precompile
	// that is not enabled at genesis
	disabled := func(address string) func() fixture {
		return func() fixture {
			return fixture{
				address:            address,
				expectIsEnabled:    false,
				expectIsEthAccount: false,
				expectCode:         []byte{},
				expectNonce:        uint64(0),
			}
		}
	}

	testCases := []struct {
		name       string
		genFixture func() fixture
	}{
		{
			name:       "noop contract address is enabled and initialized",
			genFixture: enabled(registry.NoopContractAddress),
		},
		{
			name:       "noop contract address second address is disabled and not initialized",
			genFixture: disabled(registry.NoopContractAddress2),
		},
	}

	for _, tc := range testCases {
		suite.Run(tc.name, func() {
			tf := tc.genFixture()

			//
			// Addresses
			//
			evmAddress := common.HexToAddress(tf.address)
			sdkAddress := sdk.AccAddress(evmAddress.Bytes())

			//
			// Heights
			//
			// We ensure all queries happen at block 1 after genesis
			// and help ensure determisitc behavior
			genesisHeight := big.NewInt(1)
			grpcGenesisContext := suite.Kava.Grpc.CtxAtHeight(genesisHeight.Int64())

			//
			// Queries
			//
			evmParamsResp, err := suite.Kava.Grpc.Query.Evm.Params(grpcGenesisContext, &evmtypes.QueryParamsRequest{})
			suite.Require().NoError(err)

			// accountErr is checked during in the assertions below if tf.expectIsEthAccount is true
			// This is due to the service returning a not found error for address that are not enabled,
			// which can be ignored when we do not expect an eth account to exist.
			accountResponse, accountErr := suite.Kava.Grpc.Query.Auth.Account(
				grpcGenesisContext, &authtypes.QueryAccountRequest{Address: sdkAddress.String()})
			var account authtypes.AccountI
			if accountErr == nil {
				err = suite.Kava.EncodingConfig.Marshaler.UnpackAny(accountResponse.Account, &account)
				suite.Require().NoError(err)
			}

			// We ensure both the evm json rpc and x/evm grpc code endpoints return the same value
			// in the assertions below
			grpcCodeResponse, err := suite.Kava.Grpc.Query.Evm.Code(grpcGenesisContext,
				&evmtypes.QueryCodeRequest{Address: evmAddress.String()})
			suite.Require().NoError(err)
			rpcCode, err := suite.Kava.EvmClient.CodeAt(context.Background(), evmAddress, genesisHeight)
			suite.Require().NoError(err)

			nonce, err := suite.Kava.EvmClient.NonceAt(context.Background(), evmAddress, genesisHeight)
			suite.Require().NoError(err)

			//
			// Assertions
			//
			if tf.expectIsEnabled {
				suite.Containsf(evmParamsResp.Params.EnabledPrecompiles, tf.address,
					"expected %s to be enabled in evm params", tf.address)
			} else {
				suite.NotContainsf(evmParamsResp.Params.EnabledPrecompiles, tf.address,
					"expected %s to not be enabled in evm params", tf.address)
			}

			// Fail if fixuture configuration is invalid
			if tf.expectIsEthAccount && len(tf.expectCode) == 0 {
				suite.Failf("an eth account must have expected code for address %s", tf.address)
			}

			// Run addition EthAccount assertions
			if tf.expectIsEthAccount {
				suite.Require().NoErrorf(accountErr, "expected account query to not error for address %s", tf.address)

				// All contracts including precompiles must be EthAccount's
				ethAccount, isEthAccount := account.(*ethermint.EthAccount)
				suite.Require().Truef(isEthAccount, "expected account at address %s to be an eth account", tf.address)

				// Code hash must always match the EthAccount
				codeHash := ethAccount.GetCodeHash()
				suite.Equalf(crypto.Keccak256Hash(tf.expectCode), codeHash,
					"expected codehash for account %s to match expected code", tf.address)

				// A precompile (and contract) should never have a public key set
				suite.Nilf(ethAccount.PubKey, "expected account %s to have no public key", tf.address)

				// Assert the account sequence matches the expected nonce
				// This a duplicate of the nonce assertion below, but also helps ensure these
				// two sources agree
				suite.Equal(tf.expectNonce, ethAccount.GetSequence())
			}

			// We assert both methods of code retrieval report the same value
			suite.Equalf(tf.expectCode, rpcCode, "expected code for address %s to match expected", tf.address)
			// The GRPC endpoint returns []byte(nil) when the code is not in state, which is different from
			// the rpc endpoint that returns []byte{}.
			grpcExpectedCode := tf.expectCode
			if len(grpcExpectedCode) == 0 {
				grpcExpectedCode = []byte(nil)
			}
			suite.Equalf(grpcExpectedCode, grpcCodeResponse.Code, "expected code for address %s to match expected", tf.address)
			// We assert this outside of the account context since the evm rpc always returns a nonce
			suite.Equalf(tf.expectNonce, nonce, "expected nonce for address %s to match expected", tf.address)
		})
	}
}