// Copyright 2025 The Erigon Authors
// This file is part of Erigon.
//
// Erigon is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Erigon is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with Erigon. If not, see <http://www.gnu.org/licenses/>.

package engineapi_test

import (
	"context"
	"math/big"
	"strings"
	"testing"

	"github.com/stretchr/testify/require"

	"github.com/erigontech/erigon/common/hexutil"

	"github.com/erigontech/erigon/common"
	"github.com/erigontech/erigon/common/log/v3"
	"github.com/erigontech/erigon/common/testlog"
	"github.com/erigontech/erigon/execution/abi/bind"
	enginetypes "github.com/erigontech/erigon/execution/engineapi/engine_types"
	"github.com/erigontech/erigon/execution/engineapi/engineapitester"
	"github.com/erigontech/erigon/execution/protocol/params"
	"github.com/erigontech/erigon/execution/state/contracts"
	"github.com/erigontech/erigon/node/ethconfig"
	"github.com/erigontech/erigon/rpc"
)

func TestEngineApiInvalidPayloadThenValidCanonicalFcuWithPayloadShouldSucceed(t *testing.T) {
	ctx := t.Context()
	logger := testlog.Logger(t, log.LvlDebug)
	eat, err := engineapitester.DefaultEngineApiTester(ctx, logger, t.TempDir())
	require.NoError(t, err)
	t.Cleanup(func() {
		err := eat.Close()
		require.NoError(t, err)
	})
	eat.Run(t, func(ctx context.Context, t *testing.T, eat engineapitester.EngineApiTester) {
		// deploy changer at b2
		transactOpts, err := bind.NewKeyedTransactorWithChainID(eat.CoinbaseKey, eat.ChainId())
		require.NoError(t, err)
		transactOpts.GasLimit = params.MaxTxnGasLimit
		_, txn, changer, err := contracts.DeployChanger(transactOpts, eat.ContractBackend)
		require.NoError(t, err)
		b2Canon, err := eat.MockCl.BuildCanonicalBlock(ctx)
		require.NoError(t, err)
		err = eat.TxnInclusionVerifier.VerifyTxnsInclusion(ctx, b2Canon.ExecutionPayload, txn.Hash())
		require.NoError(t, err)
		// change changer at b3
		txn, err = changer.Change(transactOpts)
		require.NoError(t, err)
		b3Canon, err := eat.MockCl.BuildCanonicalBlock(ctx)
		require.NoError(t, err)
		err = eat.TxnInclusionVerifier.VerifyTxnsInclusion(ctx, b3Canon.ExecutionPayload, txn.Hash())
		require.NoError(t, err)
		// create an invalid fork at b3
		b3Faulty := engineapitester.TamperMockClPayloadStateRoot(b3Canon, common.HexToHash("0xb3f"))
		status, err := eat.MockCl.InsertNewPayload(ctx, b3Faulty)
		require.NoError(t, err)
		require.Equal(t, enginetypes.InvalidStatus, status.Status)
		t.Log(status.ValidationError.Error().Error())
		require.True(t, strings.Contains(status.ValidationError.Error().Error(), "invalid block hash"))
		// build b4 on the canonical chain
		txn, err = changer.Change(transactOpts)
		require.NoError(t, err)
		b4Canon, err := eat.MockCl.BuildCanonicalBlock(ctx)
		require.NoError(t, err)
		err = eat.TxnInclusionVerifier.VerifyTxnsInclusion(ctx, b4Canon.ExecutionPayload, txn.Hash())
		require.NoError(t, err)
	})
}

func TestEngineApiExecBlockBatchWithLenLtMaxReorgDepthAtTipThenUnwindShouldSucceed(t *testing.T) {
	ctx := t.Context()
	logger := testlog.Logger(t, log.LvlDebug)
	// Scenario:
	//   - we were following the tip efficiently and exec-ing 1 block at a time
	//   - the CL went offline for a some time
	//   - when it came back online we executed
	//   - fcu1 with 357 blocks forward, no unwinding (it connected to the last fcu before CL went offline)
	//   - fcu2 with 64 blocks forward, no unwinding (it connected to fcu1)
	//   - newPayload for a side chain at height fcu2+1 that requires an unwind to height fcu2-1
	//   - we got stuck with error domains.GetDiffset(fcu2.BlockNum, fcu2.BlockHash) not found
	//
	// In this test we simplify this to just exec-ing a batch of N blocks <= maxReorgDepth and then
	// doing a new payload with a side chain which requires 1 block unwind.
	//
	// Generate a canonical chain of N blocks
	n := uint64(64)
	receiver1 := common.HexToAddress("0x111")
	receiver2 := common.HexToAddress("0x222")
	sharedGenesis, coinbaseKey, err := engineapitester.DefaultEngineApiTesterGenesis()
	require.NoError(t, err)
	canonicalChain := make([]*engineapitester.MockClPayload, n)
	eatCanonical, err := engineapitester.InitialiseEngineApiTester(ctx, engineapitester.EngineApiTesterInitArgs{
		Logger:      logger,
		DataDir:     t.TempDir(),
		Genesis:     sharedGenesis,
		CoinbaseKey: coinbaseKey,
		EthConfigTweaker: func(config *ethconfig.Config) {
			config.MaxReorgDepth = n
		},
	})
	require.NoError(t, err)
	t.Cleanup(func() {
		err := eatCanonical.Close()
		require.NoError(t, err)
	})
	eatCanonical.Run(t, func(ctx context.Context, t *testing.T, eatCanonical engineapitester.EngineApiTester) {
		for i := range canonicalChain {
			txn, err := eatCanonical.Transactor.SubmitSimpleTransfer(eatCanonical.CoinbaseKey, receiver1, big.NewInt(1))
			require.NoError(t, err)
			clPayload, err := eatCanonical.MockCl.BuildCanonicalBlock(ctx)
			require.NoError(t, err)
			err = eatCanonical.TxnInclusionVerifier.VerifyTxnsInclusion(ctx, clPayload.ExecutionPayload, txn.Hash())
			require.NoError(t, err)
			canonicalChain[i] = clPayload
		}
	})
	// Generate a side chain which goes up to N and executes the same txns until N-1 but at N executes different txns
	sideChain := make([]*engineapitester.MockClPayload, n)
	eatSide, err := engineapitester.InitialiseEngineApiTester(ctx, engineapitester.EngineApiTesterInitArgs{
		Logger:      logger,
		DataDir:     t.TempDir(),
		Genesis:     sharedGenesis,
		CoinbaseKey: coinbaseKey,
		EthConfigTweaker: func(config *ethconfig.Config) {
			config.MaxReorgDepth = n
		},
	})
	require.NoError(t, err)
	t.Cleanup(func() {
		err := eatSide.Close()
		require.NoError(t, err)
	})
	eatSide.Run(t, func(ctx context.Context, t *testing.T, eatSide engineapitester.EngineApiTester) {
		forkPoint := n - 1
		for i := range sideChain[:forkPoint] {
			txn, err := eatSide.Transactor.SubmitSimpleTransfer(eatSide.CoinbaseKey, receiver1, big.NewInt(1))
			require.NoError(t, err)
			clPayload, err := eatSide.MockCl.BuildCanonicalBlock(ctx)
			require.NoError(t, err)
			err = eatSide.TxnInclusionVerifier.VerifyTxnsInclusion(ctx, clPayload.ExecutionPayload, txn.Hash())
			require.NoError(t, err)
			sideChain[i] = clPayload
		}
		for i := forkPoint; i < uint64(len(sideChain)); i++ {
			txn, err := eatSide.Transactor.SubmitSimpleTransfer(eatSide.CoinbaseKey, receiver2, big.NewInt(1))
			require.NoError(t, err)
			clPayload, err := eatSide.MockCl.BuildCanonicalBlock(ctx)
			require.NoError(t, err)
			err = eatSide.TxnInclusionVerifier.VerifyTxnsInclusion(ctx, clPayload.ExecutionPayload, txn.Hash())
			require.NoError(t, err)
			sideChain[i] = clPayload
		}
	})
	// Sync another EL all the way up to the canonical tip, then give it the side chain tip as a new payload
	eatSync, err := engineapitester.InitialiseEngineApiTester(ctx, engineapitester.EngineApiTesterInitArgs{
		Logger:      logger,
		DataDir:     t.TempDir(),
		Genesis:     sharedGenesis,
		CoinbaseKey: coinbaseKey,
		EthConfigTweaker: func(config *ethconfig.Config) {
			config.MaxReorgDepth = n
		},
	})
	require.NoError(t, err)
	t.Cleanup(func() {
		err := eatSync.Close()
		require.NoError(t, err)
	})
	eatSync.Run(t, func(ctx context.Context, t *testing.T, eatSync engineapitester.EngineApiTester) {
		for _, payload := range canonicalChain {
			_, err := eatSync.MockCl.InsertNewPayload(ctx, payload)
			require.NoError(t, err)
		}
		err := eatSync.MockCl.UpdateForkChoice(ctx, canonicalChain[len(canonicalChain)-1])
		require.NoError(t, err)
		_, err = eatSync.MockCl.InsertNewPayload(ctx, sideChain[len(sideChain)-1])
		require.NoError(t, err)
	})
}

func TestEthGetLogsDoNotGetAffectedAfterNewPayloadOnSideChain(t *testing.T) {
	ctx := t.Context()
	logger := testlog.Logger(t, log.LvlDebug)
	sharedGenesis, coinbaseKey, err := engineapitester.DefaultEngineApiTesterGenesis()
	require.NoError(t, err)
	var b2Side *engineapitester.MockClPayload
	eatSide, err := engineapitester.InitialiseEngineApiTester(ctx, engineapitester.EngineApiTesterInitArgs{
		Logger:      logger,
		DataDir:     t.TempDir(),
		Genesis:     sharedGenesis,
		CoinbaseKey: coinbaseKey,
	})
	require.NoError(t, err)
	t.Cleanup(func() {
		err := eatSide.Close()
		require.NoError(t, err)
	})
	eatSide.Run(t, func(ctx context.Context, t *testing.T, eat engineapitester.EngineApiTester) {
		// do a simple eth transfer at bn2
		txn, err := eat.Transactor.SubmitSimpleTransfer(eat.CoinbaseKey, common.HexToAddress("0x333"), big.NewInt(1))
		require.NoError(t, err)
		b2Side, err = eat.MockCl.BuildCanonicalBlock(ctx)
		require.NoError(t, err)
		err = eat.TxnInclusionVerifier.VerifyTxnsInclusion(ctx, b2Side.ExecutionPayload, txn.Hash())
		require.NoError(t, err)
	})
	eatCanonical, err := engineapitester.InitialiseEngineApiTester(ctx, engineapitester.EngineApiTesterInitArgs{
		Logger:      logger,
		DataDir:     t.TempDir(),
		Genesis:     sharedGenesis,
		CoinbaseKey: coinbaseKey,
	})
	require.NoError(t, err)
	t.Cleanup(func() {
		err := eatCanonical.Close()
		require.NoError(t, err)
	})
	eatCanonical.Run(t, func(ctx context.Context, t *testing.T, eat engineapitester.EngineApiTester) {
		// deploy a smart contract at bn2
		transactOpts, err := bind.NewKeyedTransactorWithChainID(eat.CoinbaseKey, eat.ChainId())
		require.NoError(t, err)
		transactOpts.GasLimit = params.MaxTxnGasLimit
		_, txn, poly, err := contracts.DeployPoly(transactOpts, eat.ContractBackend)
		require.NoError(t, err)
		b2Canon, err := eat.MockCl.BuildCanonicalBlock(ctx)
		require.NoError(t, err)
		err = eat.TxnInclusionVerifier.VerifyTxnsInclusion(ctx, b2Canon.ExecutionPayload, txn.Hash())
		require.NoError(t, err)
		// interact with the smart contract at bn3 to produce a log
		txn, err = poly.Deploy(transactOpts, big.NewInt(1))
		require.NoError(t, err)
		b3Canon, err := eat.MockCl.BuildCanonicalBlock(ctx)
		require.NoError(t, err)
		err = eat.TxnInclusionVerifier.VerifyTxnsInclusion(ctx, b3Canon.ExecutionPayload, txn.Hash())
		require.NoError(t, err)
		// check the log is present by querying rpc
		latestBlock, err := eat.RpcApiClient.GetBlockByNumber(ctx, rpc.LatestBlockNumber, false)
		require.NoError(t, err)
		require.Equal(t, uint64(3), latestBlock.Number.Uint64())
		logs, err := eat.RpcApiClient.FilterLogs(ctx, bind.FilterQuery{BlockHash: &latestBlock.Hash})
		require.NoError(t, err)
		require.Len(t, logs, 1)
		require.Equal(t, hexutil.Uint64(3), logs[0].BlockNumber)
		// now insert a new payload on the side chain and check the log is still present
		_, err = eat.MockCl.InsertNewPayload(ctx, b2Side)
		require.NoError(t, err)
		logs, err = eat.RpcApiClient.FilterLogs(ctx, bind.FilterQuery{BlockHash: &latestBlock.Hash})
		require.NoError(t, err)
		require.Len(t, logs, 1)
		require.Equal(t, hexutil.Uint64(3), logs[0].BlockNumber)
	})
}

func TestNewPayloadShouldReturnValidWhenSideChainGoingBackIsLtMaxReorgDepth(t *testing.T) {
	ctx := t.Context()
	logger := testlog.Logger(t, log.LvlDebug)
	// we had an issue where some benchmark tests were doing more than a 32-block reorg backwards
	// while our MAX_REORG_DEPTH was 96 blocks, however, NewPayload returned ACCEPTED instead of VALID
	// and caused issues with benchmarkoor
	// this test captures that by calling NewPayload for a side fork back to block 1 from canonical fork at block 34
	canonicalChainLen := 34 // a few blocks > 32
	maxReorgDepth := uint64(96)
	receiver1 := common.HexToAddress("0x111")
	receiver2 := common.HexToAddress("0x222")
	sharedGenesis, coinbaseKey, err := engineapitester.DefaultEngineApiTesterGenesis()
	require.NoError(t, err)
	// Generate a side chain which goes up to 1
	var sideChain *engineapitester.MockClPayload
	eatSide, err := engineapitester.InitialiseEngineApiTester(ctx, engineapitester.EngineApiTesterInitArgs{
		Logger:      logger,
		DataDir:     t.TempDir(),
		Genesis:     sharedGenesis,
		CoinbaseKey: coinbaseKey,
		EthConfigTweaker: func(config *ethconfig.Config) {
			config.MaxReorgDepth = maxReorgDepth
		},
	})
	require.NoError(t, err)
	t.Cleanup(func() {
		err := eatSide.Close()
		require.NoError(t, err)
	})
	eatSide.Run(t, func(ctx context.Context, t *testing.T, eatSide engineapitester.EngineApiTester) {
		txn, err := eatSide.Transactor.SubmitSimpleTransfer(eatSide.CoinbaseKey, receiver2, big.NewInt(1))
		require.NoError(t, err)
		clPayload, err := eatSide.MockCl.BuildCanonicalBlock(ctx)
		require.NoError(t, err)
		err = eatSide.TxnInclusionVerifier.VerifyTxnsInclusion(ctx, clPayload.ExecutionPayload, txn.Hash())
		require.NoError(t, err)
		sideChain = clPayload
	})
	eatCanonical, err := engineapitester.InitialiseEngineApiTester(ctx, engineapitester.EngineApiTesterInitArgs{
		Logger:      logger,
		DataDir:     t.TempDir(),
		Genesis:     sharedGenesis,
		CoinbaseKey: coinbaseKey,
		EthConfigTweaker: func(config *ethconfig.Config) {
			config.MaxReorgDepth = maxReorgDepth
		},
	})
	require.NoError(t, err)
	t.Cleanup(func() {
		err := eatCanonical.Close()
		require.NoError(t, err)
	})
	eatCanonical.Run(t, func(ctx context.Context, t *testing.T, eatCanonical engineapitester.EngineApiTester) {
		// build the canonical chain up to canonicalChainLen
		for i := 0; i < canonicalChainLen; i++ {
			txn, err := eatCanonical.Transactor.SubmitSimpleTransfer(eatCanonical.CoinbaseKey, receiver1, big.NewInt(1))
			require.NoError(t, err)
			clPayload, err := eatCanonical.MockCl.BuildCanonicalBlock(ctx)
			require.NoError(t, err)
			err = eatCanonical.TxnInclusionVerifier.VerifyTxnsInclusion(ctx, clPayload.ExecutionPayload, txn.Hash())
			require.NoError(t, err)
		}
		// now do a new payload on the side chain at block 1
		ps, err := eatCanonical.MockCl.InsertNewPayload(ctx, sideChain)
		require.NoError(t, err)
		require.Equal(t, enginetypes.ValidStatus, ps.Status)
	})
}

func TestFcuAllowsReorgBackOnCanonicalChainWhenAfterFinalisedHash(t *testing.T) {
	ctx := t.Context()
	logger := testlog.Logger(t, log.LvlDebug)
	// as per spec update: https://github.com/ethereum/execution-apis/pull/786
	eat, err := engineapitester.DefaultEngineApiTester(ctx, logger, t.TempDir())
	require.NoError(t, err)
	t.Cleanup(func() {
		err := eat.Close()
		require.NoError(t, err)
	})
	eat.Run(t, func(ctx context.Context, t *testing.T, eat engineapitester.EngineApiTester) {
		// deploy changer at b2
		transactOpts, err := bind.NewKeyedTransactorWithChainID(eat.CoinbaseKey, eat.ChainId())
		require.NoError(t, err)
		transactOpts.GasLimit = params.MaxTxnGasLimit
		_, txn, changer, err := contracts.DeployChanger(transactOpts, eat.ContractBackend)
		require.NoError(t, err)
		b2Canon, err := eat.MockCl.BuildCanonicalBlock(ctx)
		require.NoError(t, err)
		err = eat.TxnInclusionVerifier.VerifyTxnsInclusion(ctx, b2Canon.ExecutionPayload, txn.Hash())
		require.NoError(t, err)
		// change changer at b3
		txn, err = changer.Change(transactOpts)
		require.NoError(t, err)
		b3Canon, err := eat.MockCl.BuildCanonicalBlock(ctx)
		require.NoError(t, err)
		err = eat.TxnInclusionVerifier.VerifyTxnsInclusion(ctx, b3Canon.ExecutionPayload, txn.Hash())
		require.NoError(t, err)
		blockNum, err := eat.RpcApiClient.BlockNumber()
		require.NoError(t, err)
		require.Equal(t, uint64(3), blockNum)
		// unwind back to b2
		err = eat.MockCl.UpdateForkChoice(ctx, b2Canon)
		require.NoError(t, err)
		// verify canonical head went back
		blockNum, err = eat.RpcApiClient.BlockNumber()
		require.NoError(t, err)
		require.Equal(t, uint64(2), blockNum)
	})
}

func TestFcuReturnsReorgTooDeepCode38006(t *testing.T) {
	ctx := t.Context()
	logger := testlog.Logger(t, log.LvlDebug)
	// as per spec update: https://github.com/ethereum/execution-apis/pull/786
	genesis, coinbaseKey, err := engineapitester.DefaultEngineApiTesterGenesis()
	require.NoError(t, err)
	eat, err := engineapitester.InitialiseEngineApiTester(ctx, engineapitester.EngineApiTesterInitArgs{
		Logger:      logger,
		DataDir:     t.TempDir(),
		Genesis:     genesis,
		CoinbaseKey: coinbaseKey,
		EthConfigTweaker: func(config *ethconfig.Config) {
			config.MaxReorgDepth = 2
		},
	})
	require.NoError(t, err)
	t.Cleanup(func() {
		err := eat.Close()
		require.NoError(t, err)
	})
	eat.Run(t, func(ctx context.Context, t *testing.T, eat engineapitester.EngineApiTester) {
		// deploy changer at b2
		transactOpts, err := bind.NewKeyedTransactorWithChainID(eat.CoinbaseKey, eat.ChainId())
		require.NoError(t, err)
		transactOpts.GasLimit = params.MaxTxnGasLimit
		_, txn, changer, err := contracts.DeployChanger(transactOpts, eat.ContractBackend)
		require.NoError(t, err)
		b2Canon, err := eat.MockCl.BuildCanonicalBlock(ctx)
		require.NoError(t, err)
		err = eat.TxnInclusionVerifier.VerifyTxnsInclusion(ctx, b2Canon.ExecutionPayload, txn.Hash())
		require.NoError(t, err)
		// change changer at b3
		txn, err = changer.Change(transactOpts)
		require.NoError(t, err)
		b3Canon, err := eat.MockCl.BuildCanonicalBlock(ctx)
		require.NoError(t, err)
		err = eat.TxnInclusionVerifier.VerifyTxnsInclusion(ctx, b3Canon.ExecutionPayload, txn.Hash())
		require.NoError(t, err)
		// change changer at b4
		txn, err = changer.Change(transactOpts)
		require.NoError(t, err)
		b4Canon, err := eat.MockCl.BuildCanonicalBlock(ctx)
		require.NoError(t, err)
		err = eat.TxnInclusionVerifier.VerifyTxnsInclusion(ctx, b4Canon.ExecutionPayload, txn.Hash())
		require.NoError(t, err)
		// change changer at b5
		txn, err = changer.Change(transactOpts)
		require.NoError(t, err)
		b5Canon, err := eat.MockCl.BuildCanonicalBlock(ctx)
		require.NoError(t, err)
		err = eat.TxnInclusionVerifier.VerifyTxnsInclusion(ctx, b5Canon.ExecutionPayload, txn.Hash())
		require.NoError(t, err)
		// unwind back to b2 should return "Too deep reorg" error with code 38006
		err = eat.MockCl.UpdateForkChoice(ctx, b2Canon)
		require.Error(t, err)
		var rpcErr rpc.Error
		require.ErrorAs(t, err, &rpcErr)
		require.Equal(t, -38006, rpcErr.ErrorCode())
		require.Equal(t, "Too deep reorg", rpcErr.Error())
	})
}
