package sender

import (
	"context"
	"crypto/ecdsa"
	"crypto/rand"
	"errors"
	"fmt"
	"math/big"
	"os"
	"testing"
	"time"

	"github.com/agiledragon/gomonkey/v2"
	"github.com/consensys/gnark-crypto/ecc/bls12-381/fr"
	gokzg4844 "github.com/crate-crypto/go-kzg-4844"
	"github.com/holiman/uint256"
	"github.com/scroll-tech/go-ethereum/accounts/abi/bind"
	"github.com/scroll-tech/go-ethereum/common"
	gethTypes "github.com/scroll-tech/go-ethereum/core/types"
	"github.com/scroll-tech/go-ethereum/crypto"
	"github.com/scroll-tech/go-ethereum/crypto/kzg4844"
	"github.com/scroll-tech/go-ethereum/ethclient"
	"github.com/scroll-tech/go-ethereum/log"
	"github.com/scroll-tech/go-ethereum/rpc"
	"github.com/stretchr/testify/assert"
	"gorm.io/gorm"

	dockercompose "scroll-tech/common/docker-compose/l1"
	"scroll-tech/common/testcontainers"
	"scroll-tech/common/types"
	"scroll-tech/database/migrate"

	bridgeAbi "scroll-tech/rollup/abi"
	"scroll-tech/rollup/internal/config"
	"scroll-tech/rollup/internal/orm"
	"scroll-tech/rollup/mock_bridge"
)

var (
	privateKey           *ecdsa.PrivateKey
	cfg                  *config.Config
	testApps             *testcontainers.TestcontainerApps
	posL1TestEnv         *dockercompose.PoSL1TestEnv
	txTypes              = []string{"LegacyTx", "DynamicFeeTx", "DynamicFeeTx"}
	txBlob               = []*kzg4844.Blob{nil, nil, randBlob()}
	txUint8Types         = []uint8{0, 2, 3}
	db                   *gorm.DB
	testContractsAddress common.Address
)

func TestMain(m *testing.M) {
	defer func() {
		if testApps != nil {
			testApps.Free()
		}
		if posL1TestEnv != nil {
			posL1TestEnv.Stop()
		}
	}()
	m.Run()
}

func setupEnv(t *testing.T) {
	glogger := log.NewGlogHandler(log.StreamHandler(os.Stderr, log.LogfmtFormat()))
	glogger.Verbosity(log.LvlInfo)
	log.Root().SetHandler(glogger)

	var err error
	cfg, err = config.NewConfig("../../../conf/config.json")
	assert.NoError(t, err)
	priv, err := crypto.HexToECDSA("1212121212121212121212121212121212121212121212121212121212121212")
	assert.NoError(t, err)
	privateKey = priv

	posL1TestEnv, err = dockercompose.NewPoSL1TestEnv()
	assert.NoError(t, err, "failed to create PoS L1 test environment")
	assert.NoError(t, posL1TestEnv.Start(), "failed to start PoS L1 test environment")

	testApps = testcontainers.NewTestcontainerApps()
	assert.NoError(t, testApps.StartPostgresContainer())
	assert.NoError(t, testApps.StartL1GethContainer())
	assert.NoError(t, testApps.StartL2GethContainer())

	cfg.L2Config.RelayerConfig.SenderConfig.Endpoint = posL1TestEnv.Endpoint()

	db, err = testApps.GetGormDBClient()
	assert.NoError(t, err)
	sqlDB, err := db.DB()
	assert.NoError(t, err)
	assert.NoError(t, migrate.ResetDB(sqlDB))

	l1Client, err := posL1TestEnv.L1Client()
	assert.NoError(t, err)

	chainID, err := l1Client.ChainID(context.Background())
	assert.NoError(t, err)

	auth, err := bind.NewKeyedTransactorWithChainID(privateKey, chainID)
	assert.NoError(t, err)

	nonce, err := l1Client.PendingNonceAt(context.Background(), auth.From)
	assert.NoError(t, err)

	testContractsAddress = crypto.CreateAddress(auth.From, nonce)

	tx := gethTypes.NewContractCreation(nonce, big.NewInt(0), 10000000, big.NewInt(10000000000), common.FromHex(mock_bridge.MockBridgeMetaData.Bin))
	signedTx, err := auth.Signer(auth.From, tx)
	assert.NoError(t, err)
	err = l1Client.SendTransaction(context.Background(), signedTx)
	assert.NoError(t, err)

	assert.Eventually(t, func() bool {
		_, isPending, err := l1Client.TransactionByHash(context.Background(), signedTx.Hash())
		return err == nil && !isPending
	}, 30*time.Second, time.Second)

	assert.Eventually(t, func() bool {
		receipt, err := l1Client.TransactionReceipt(context.Background(), signedTx.Hash())
		return err == nil && receipt.Status == gethTypes.ReceiptStatusSuccessful
	}, 30*time.Second, time.Second)

	assert.Eventually(t, func() bool {
		code, err := l1Client.CodeAt(context.Background(), testContractsAddress, nil)
		return err == nil && len(code) > 0
	}, 30*time.Second, time.Second)
}

func TestSender(t *testing.T) {
	setupEnv(t)

	t.Run("test new sender", testNewSender)
	t.Run("test send and retrieve transaction", testSendAndRetrieveTransaction)
	t.Run("test fallback gas limit", testFallbackGasLimit)
	t.Run("test access list transaction gas limit", testAccessListTransactionGasLimit)
	t.Run("test resubmit zero gas price transaction", testResubmitZeroGasPriceTransaction)
	t.Run("test resubmit non-zero gas price transaction", testResubmitNonZeroGasPriceTransaction)
	t.Run("test resubmit under priced transaction", testResubmitUnderpricedTransaction)
	t.Run("test resubmit dynamic fee transaction with rising base fee", testResubmitDynamicFeeTransactionWithRisingBaseFee)
	t.Run("test resubmit blob transaction with rising base fee and blob base fee", testResubmitBlobTransactionWithRisingBaseFeeAndBlobBaseFee)
	t.Run("test check pending transaction tx confirmed", testCheckPendingTransactionTxConfirmed)
	t.Run("test check pending transaction resubmit tx confirmed", testCheckPendingTransactionResubmitTxConfirmed)
	t.Run("test check pending transaction replaced tx confirmed", testCheckPendingTransactionReplacedTxConfirmed)
	t.Run("test check pending transaction multiple times with only one transaction pending", testCheckPendingTransactionTxMultipleTimesWithOnlyOneTxPending)
	t.Run("test blob transaction with blobhash op contract call", testBlobTransactionWithBlobhashOpContractCall)
}

func testNewSender(t *testing.T) {
	for _, txType := range txTypes {
		sqlDB, err := db.DB()
		assert.NoError(t, err)
		assert.NoError(t, migrate.ResetDB(sqlDB))

		// exit by Stop()
		cfgCopy1 := *cfg.L2Config.RelayerConfig.SenderConfig
		cfgCopy1.TxType = txType
		newSender1, err := NewSender(context.Background(), &cfgCopy1, privateKey, "test", "test", types.SenderTypeUnknown, db, nil)
		assert.NoError(t, err)
		newSender1.Stop()

		// exit by ctx.Done()
		cfgCopy2 := *cfg.L2Config.RelayerConfig.SenderConfig
		cfgCopy2.TxType = txType
		subCtx, cancel := context.WithCancel(context.Background())
		_, err = NewSender(subCtx, &cfgCopy2, privateKey, "test", "test", types.SenderTypeUnknown, db, nil)
		assert.NoError(t, err)
		cancel()
	}
}

func testSendAndRetrieveTransaction(t *testing.T) {
	for i, txType := range txTypes {
		sqlDB, err := db.DB()
		assert.NoError(t, err)
		assert.NoError(t, migrate.ResetDB(sqlDB))

		cfgCopy := *cfg.L2Config.RelayerConfig.SenderConfig
		cfgCopy.TxType = txType
		s, err := NewSender(context.Background(), &cfgCopy, privateKey, "test", "test", types.SenderTypeUnknown, db, nil)
		assert.NoError(t, err)

		hash, err := s.SendTransaction("0", &common.Address{}, nil, txBlob[i], 0)
		assert.NoError(t, err)
		txs, err := s.pendingTransactionOrm.GetPendingOrReplacedTransactionsBySenderType(context.Background(), s.senderType, 1)
		assert.NoError(t, err)
		assert.Len(t, txs, 1)
		assert.Equal(t, "0", txs[0].ContextID)
		assert.Equal(t, hash.String(), txs[0].Hash)
		assert.Equal(t, txUint8Types[i], txs[0].Type)
		assert.Equal(t, types.TxStatusPending, txs[0].Status)
		assert.Equal(t, "0x1C5A77d9FA7eF466951B2F01F724BCa3A5820b63", txs[0].SenderAddress)
		assert.Equal(t, types.SenderTypeUnknown, txs[0].SenderType)
		assert.Equal(t, "test", txs[0].SenderService)
		assert.Equal(t, "test", txs[0].SenderName)

		assert.Eventually(t, func() bool {
			txs, err = s.pendingTransactionOrm.GetPendingOrReplacedTransactionsBySenderType(context.Background(), s.senderType, 100)
			assert.NoError(t, err)
			return len(txs) == 0
		}, 30*time.Second, time.Second)

		s.Stop()
	}
}

func testFallbackGasLimit(t *testing.T) {
	for i, txType := range txTypes {
		sqlDB, err := db.DB()
		assert.NoError(t, err)
		assert.NoError(t, migrate.ResetDB(sqlDB))

		cfgCopy := *cfg.L2Config.RelayerConfig.SenderConfig
		cfgCopy.TxType = txType
		cfgCopy.Confirmations = rpc.LatestBlockNumber
		s, err := NewSender(context.Background(), &cfgCopy, privateKey, "test", "test", types.SenderTypeUnknown, db, nil)
		assert.NoError(t, err)

		client, err := ethclient.Dial(cfgCopy.Endpoint)
		assert.NoError(t, err)

		// FallbackGasLimit = 0
		txHash0, err := s.SendTransaction("0", &common.Address{}, nil, txBlob[i], 0)
		assert.NoError(t, err)
		tx0, _, err := client.TransactionByHash(context.Background(), txHash0)
		assert.NoError(t, err)
		assert.Greater(t, tx0.Gas(), uint64(0))

		assert.Eventually(t, func() bool {
			var txs []orm.PendingTransaction
			txs, err = s.pendingTransactionOrm.GetPendingOrReplacedTransactionsBySenderType(context.Background(), s.senderType, 100)
			assert.NoError(t, err)
			return len(txs) == 0
		}, 30*time.Second, time.Second)

		// FallbackGasLimit = 100000
		patchGuard := gomonkey.ApplyPrivateMethod(s, "estimateGasLimit",
			func(contract *common.Address, data []byte, sidecar *gethTypes.BlobTxSidecar, gasPrice, gasTipCap, gasFeeCap, blobGasFeeCap *big.Int) (uint64, *gethTypes.AccessList, error) {
				return 0, nil, errors.New("estimateGasLimit error")
			},
		)

		txHash1, err := s.SendTransaction("1", &common.Address{}, nil, txBlob[i], 100000)
		assert.NoError(t, err)
		tx1, _, err := client.TransactionByHash(context.Background(), txHash1)
		assert.NoError(t, err)
		assert.Equal(t, uint64(100000), tx1.Gas())

		assert.Eventually(t, func() bool {
			txs, err := s.pendingTransactionOrm.GetPendingOrReplacedTransactionsBySenderType(context.Background(), s.senderType, 100)
			assert.NoError(t, err)
			return len(txs) == 0
		}, 30*time.Second, time.Second)

		s.Stop()
		patchGuard.Reset()
	}
}

func testResubmitZeroGasPriceTransaction(t *testing.T) {
	for i, txType := range txTypes {
		if txBlob[i] != nil {
			continue
		}

		sqlDB, err := db.DB()
		assert.NoError(t, err)
		assert.NoError(t, migrate.ResetDB(sqlDB))

		cfgCopy := *cfg.L2Config.RelayerConfig.SenderConfig
		cfgCopy.TxType = txType
		s, err := NewSender(context.Background(), &cfgCopy, privateKey, "test", "test", types.SenderTypeUnknown, db, nil)
		assert.NoError(t, err)
		feeData := &FeeData{
			gasPrice:  big.NewInt(0),
			gasTipCap: big.NewInt(0),
			gasFeeCap: big.NewInt(0),
			gasLimit:  50000,
		}
		tx, err := s.createAndSendTx(feeData, &common.Address{}, nil, nil, nil)
		assert.NoError(t, err)
		assert.NotNil(t, tx)
		// Increase at least 1 wei in gas price, gas tip cap and gas fee cap.
		// Bumping the fees enough times to let the transaction be included in a block.
		for i := 0; i < 30; i++ {
			tx, err = s.resubmitTransaction(tx, 0, 0)
			assert.NoError(t, err)
		}

		assert.Eventually(t, func() bool {
			_, isPending, err := s.client.TransactionByHash(context.Background(), tx.Hash())
			return err == nil && !isPending
		}, 30*time.Second, time.Second)

		assert.Eventually(t, func() bool {
			receipt, err := s.client.TransactionReceipt(context.Background(), tx.Hash())
			return err == nil && receipt != nil
		}, 30*time.Second, time.Second)

		s.Stop()
	}
}

func testAccessListTransactionGasLimit(t *testing.T) {
	for i, txType := range txTypes {
		sqlDB, err := db.DB()
		assert.NoError(t, err)
		assert.NoError(t, migrate.ResetDB(sqlDB))

		cfgCopy := *cfg.L2Config.RelayerConfig.SenderConfig
		cfgCopy.TxType = txType
		s, err := NewSender(context.Background(), &cfgCopy, privateKey, "test", "test", types.SenderTypeUnknown, db, nil)
		assert.NoError(t, err)

		l2GasOracleABI, err := bridgeAbi.L2GasPriceOracleMetaData.GetAbi()
		assert.NoError(t, err)

		data, err := l2GasOracleABI.Pack("setL2BaseFee", big.NewInt(int64(i+1)))
		assert.NoError(t, err)

		var sidecar *gethTypes.BlobTxSidecar
		if txBlob[i] != nil {
			sidecar, err = makeSidecar(txBlob[i])
			assert.NoError(t, err)
		}

		gasLimit, accessList, err := s.estimateGasLimit(&testContractsAddress, data, sidecar, nil, big.NewInt(1000000000), big.NewInt(1000000000), big.NewInt(1000000000))
		assert.NoError(t, err)

		if txType == LegacyTxType { // Legacy transactions can not have an access list.
			assert.Equal(t, uint64(43956), gasLimit)
			assert.Nil(t, accessList)
		} else { // Dynamic fee and blob transactions can have an access list.
			assert.Equal(t, uint64(43479), gasLimit)
			assert.NotNil(t, accessList)
		}

		s.Stop()
	}
}

func testResubmitNonZeroGasPriceTransaction(t *testing.T) {
	for i, txType := range txTypes {
		sqlDB, err := db.DB()
		assert.NoError(t, err)
		assert.NoError(t, migrate.ResetDB(sqlDB))

		cfgCopy := *cfg.L2Config.RelayerConfig.SenderConfig
		// Bump gas price, gas tip cap and gas fee cap just touch the minimum threshold of 10% (default config of geth).
		cfgCopy.EscalateMultipleNum = 110
		cfgCopy.EscalateMultipleDen = 100
		cfgCopy.TxType = txType
		s, err := NewSender(context.Background(), &cfgCopy, privateKey, "test", "test", types.SenderTypeUnknown, db, nil)
		assert.NoError(t, err)
		feeData := &FeeData{
			gasPrice:      big.NewInt(1000000000),
			gasTipCap:     big.NewInt(1000000000),
			gasFeeCap:     big.NewInt(1000000000),
			blobGasFeeCap: big.NewInt(1000000000),
			gasLimit:      50000,
		}
		var sidecar *gethTypes.BlobTxSidecar
		if txBlob[i] != nil {
			sidecar, err = makeSidecar(txBlob[i])
			assert.NoError(t, err)
		}
		tx, err := s.createAndSendTx(feeData, &common.Address{}, nil, sidecar, nil)
		assert.NoError(t, err)
		assert.NotNil(t, tx)
		resubmittedTx, err := s.resubmitTransaction(tx, 0, 0)
		assert.NoError(t, err)

		assert.Eventually(t, func() bool {
			_, isPending, err := s.client.TransactionByHash(context.Background(), resubmittedTx.Hash())
			return err == nil && !isPending
		}, 30*time.Second, time.Second)

		assert.Eventually(t, func() bool {
			receipt, err := s.client.TransactionReceipt(context.Background(), resubmittedTx.Hash())
			return err == nil && receipt != nil
		}, 30*time.Second, time.Second)

		s.Stop()
	}
}

func testResubmitUnderpricedTransaction(t *testing.T) {
	for i, txType := range txTypes {
		if txBlob[i] != nil {
			continue
		}

		sqlDB, err := db.DB()
		assert.NoError(t, err)
		assert.NoError(t, migrate.ResetDB(sqlDB))

		cfgCopy := *cfg.L2Config.RelayerConfig.SenderConfig
		// Bump gas price, gas tip cap and gas fee cap less than 10% (default config of geth).
		cfgCopy.EscalateMultipleNum = 109
		cfgCopy.EscalateMultipleDen = 100
		cfgCopy.TxType = txType
		s, err := NewSender(context.Background(), &cfgCopy, privateKey, "test", "test", types.SenderTypeUnknown, db, nil)
		assert.NoError(t, err)
		feeData := &FeeData{
			gasPrice:  big.NewInt(1000000000),
			gasTipCap: big.NewInt(1000000000),
			gasFeeCap: big.NewInt(1000000000),
			gasLimit:  50000,
		}
		tx, err := s.createAndSendTx(feeData, &common.Address{}, nil, nil, nil)
		assert.NoError(t, err)
		assert.NotNil(t, tx)
		_, err = s.resubmitTransaction(tx, 0, 0)
		assert.Error(t, err, "replacement transaction underpriced")

		assert.Eventually(t, func() bool {
			_, isPending, err := s.client.TransactionByHash(context.Background(), tx.Hash())
			return err == nil && !isPending
		}, 30*time.Second, time.Second)

		assert.Eventually(t, func() bool {
			receipt, err := s.client.TransactionReceipt(context.Background(), tx.Hash())
			return err == nil && receipt != nil
		}, 30*time.Second, time.Second)

		s.Stop()
	}
}

func testResubmitDynamicFeeTransactionWithRisingBaseFee(t *testing.T) {
	sqlDB, err := db.DB()
	assert.NoError(t, err)
	assert.NoError(t, migrate.ResetDB(sqlDB))

	txType := "DynamicFeeTx"
	cfgCopy := *cfg.L2Config.RelayerConfig.SenderConfig
	cfgCopy.TxType = txType

	s, err := NewSender(context.Background(), &cfgCopy, privateKey, "test", "test", types.SenderTypeUnknown, db, nil)
	assert.NoError(t, err)

	patchGuard := gomonkey.ApplyMethodFunc(s.client, "SendTransaction", func(_ context.Context, _ *gethTypes.Transaction) error {
		return nil
	})
	defer patchGuard.Reset()

	tx := gethTypes.NewTx(&gethTypes.DynamicFeeTx{
		Nonce:     s.auth.Nonce.Uint64(),
		To:        &common.Address{},
		Data:      nil,
		Gas:       21000,
		ChainID:   s.chainID,
		GasTipCap: big.NewInt(0),
		GasFeeCap: big.NewInt(0),
	})
	baseFeePerGas := uint64(1000)
	// bump the basefee by 10x
	baseFeePerGas *= 10
	// resubmit and check that the gas fee has been adjusted accordingly
	newTx, err := s.resubmitTransaction(tx, baseFeePerGas, 0)
	assert.NoError(t, err)

	maxGasPrice := new(big.Int).SetUint64(s.config.MaxGasPrice)
	expectedGasFeeCap := getGasFeeCap(new(big.Int).SetUint64(baseFeePerGas), tx.GasTipCap())
	if expectedGasFeeCap.Cmp(maxGasPrice) > 0 {
		expectedGasFeeCap = maxGasPrice
	}

	assert.Equal(t, expectedGasFeeCap.Uint64(), newTx.GasFeeCap().Uint64())
	s.Stop()
}

func testResubmitBlobTransactionWithRisingBaseFeeAndBlobBaseFee(t *testing.T) {
	sqlDB, err := db.DB()
	assert.NoError(t, err)
	assert.NoError(t, migrate.ResetDB(sqlDB))

	cfgCopy := *cfg.L2Config.RelayerConfig.SenderConfig
	cfgCopy.TxType = DynamicFeeTxType

	s, err := NewSender(context.Background(), &cfgCopy, privateKey, "test", "test", types.SenderTypeUnknown, db, nil)
	assert.NoError(t, err)

	patchGuard := gomonkey.ApplyMethodFunc(s.client, "SendTransaction", func(_ context.Context, _ *gethTypes.Transaction) error {
		return nil
	})
	defer patchGuard.Reset()

	sidecar, err := makeSidecar(randBlob())
	assert.NoError(t, err)
	tx := gethTypes.NewTx(&gethTypes.BlobTx{
		ChainID:    uint256.MustFromBig(s.chainID),
		Nonce:      s.auth.Nonce.Uint64(),
		GasTipCap:  uint256.MustFromBig(big.NewInt(0)),
		GasFeeCap:  uint256.MustFromBig(big.NewInt(0)),
		Gas:        21000,
		To:         common.Address{},
		Data:       nil,
		BlobFeeCap: uint256.MustFromBig(big.NewInt(1)),
		BlobHashes: sidecar.BlobHashes(),
		Sidecar:    sidecar,
	})
	baseFeePerGas := uint64(1000)
	blobBaseFeePerGas := uint64(10000000000000) // bounded by max blob base fee.
	// bump the basefee and blobbasefee by 10x
	baseFeePerGas *= 10
	blobBaseFeePerGas *= 10
	// resubmit and check that the gas fee has been adjusted accordingly
	newTx, err := s.resubmitTransaction(tx, baseFeePerGas, blobBaseFeePerGas)
	assert.NoError(t, err)

	maxGasPrice := new(big.Int).SetUint64(s.config.MaxGasPrice)
	expectedGasFeeCap := getGasFeeCap(new(big.Int).SetUint64(baseFeePerGas), tx.GasTipCap())
	if expectedGasFeeCap.Cmp(maxGasPrice) > 0 {
		expectedGasFeeCap = maxGasPrice
	}

	maxBlobGasPrice := new(big.Int).SetUint64(s.config.MaxBlobGasPrice)
	expectedBlobGasFeeCap := getBlobGasFeeCap(new(big.Int).SetUint64(blobBaseFeePerGas))
	if expectedBlobGasFeeCap.Cmp(maxBlobGasPrice) > 0 {
		expectedBlobGasFeeCap = maxBlobGasPrice
	}

	assert.Equal(t, expectedGasFeeCap.Uint64(), newTx.GasFeeCap().Uint64())
	assert.Equal(t, expectedBlobGasFeeCap.Uint64(), newTx.BlobGasFeeCap().Uint64())
	s.Stop()
}

func testCheckPendingTransactionTxConfirmed(t *testing.T) {
	for _, txType := range txTypes {
		sqlDB, err := db.DB()
		assert.NoError(t, err)
		assert.NoError(t, migrate.ResetDB(sqlDB))

		cfgCopy := *cfg.L2Config.RelayerConfig.SenderConfig
		cfgCopy.TxType = txType
		s, err := NewSender(context.Background(), &cfgCopy, privateKey, "test", "test", types.SenderTypeCommitBatch, db, nil)
		assert.NoError(t, err)

		patchGuard1 := gomonkey.ApplyMethodFunc(s.client, "SendTransaction", func(_ context.Context, _ *gethTypes.Transaction) error {
			return nil
		})

		_, err = s.SendTransaction("test", &common.Address{}, nil, randBlob(), 0)
		assert.NoError(t, err)

		txs, err := s.pendingTransactionOrm.GetPendingOrReplacedTransactionsBySenderType(context.Background(), s.senderType, 1)
		assert.NoError(t, err)
		assert.Len(t, txs, 1)
		assert.Equal(t, types.TxStatusPending, txs[0].Status)
		assert.Equal(t, types.SenderTypeCommitBatch, txs[0].SenderType)

		patchGuard2 := gomonkey.ApplyMethodFunc(s.client, "TransactionReceipt", func(_ context.Context, hash common.Hash) (*gethTypes.Receipt, error) {
			return &gethTypes.Receipt{TxHash: hash, BlockNumber: big.NewInt(0), Status: gethTypes.ReceiptStatusSuccessful}, nil
		})

		s.checkPendingTransaction()
		assert.NoError(t, err)

		txs, err = s.pendingTransactionOrm.GetPendingOrReplacedTransactionsBySenderType(context.Background(), s.senderType, 1)
		assert.NoError(t, err)
		assert.Len(t, txs, 0)

		s.Stop()
		patchGuard1.Reset()
		patchGuard2.Reset()
	}
}

func testCheckPendingTransactionResubmitTxConfirmed(t *testing.T) {
	for _, txType := range txTypes {
		sqlDB, err := db.DB()
		assert.NoError(t, err)
		assert.NoError(t, migrate.ResetDB(sqlDB))

		cfgCopy := *cfg.L2Config.RelayerConfig.SenderConfig
		cfgCopy.TxType = txType
		cfgCopy.EscalateBlocks = 0
		s, err := NewSender(context.Background(), &cfgCopy, privateKey, "test", "test", types.SenderTypeFinalizeBatch, db, nil)
		assert.NoError(t, err)

		patchGuard1 := gomonkey.ApplyMethodFunc(s.client, "SendTransaction", func(_ context.Context, _ *gethTypes.Transaction) error {
			return nil
		})

		originTxHash, err := s.SendTransaction("test", &common.Address{}, nil, randBlob(), 0)
		assert.NoError(t, err)

		txs, err := s.pendingTransactionOrm.GetPendingOrReplacedTransactionsBySenderType(context.Background(), s.senderType, 1)
		assert.NoError(t, err)
		assert.Len(t, txs, 1)
		assert.Equal(t, types.TxStatusPending, txs[0].Status)
		assert.Equal(t, types.SenderTypeFinalizeBatch, txs[0].SenderType)

		patchGuard2 := gomonkey.ApplyMethodFunc(s.client, "TransactionReceipt", func(_ context.Context, hash common.Hash) (*gethTypes.Receipt, error) {
			if hash == originTxHash {
				return nil, fmt.Errorf("simulated transaction receipt error")
			}
			return &gethTypes.Receipt{TxHash: hash, BlockNumber: big.NewInt(0), Status: gethTypes.ReceiptStatusSuccessful}, nil
		})

		// Attempt to resubmit the transaction.
		s.checkPendingTransaction()
		assert.NoError(t, err)

		status, err := s.pendingTransactionOrm.GetTxStatusByTxHash(context.Background(), originTxHash)
		assert.NoError(t, err)
		assert.Equal(t, types.TxStatusReplaced, status)

		txs, err = s.pendingTransactionOrm.GetPendingOrReplacedTransactionsBySenderType(context.Background(), s.senderType, 2)
		assert.NoError(t, err)
		assert.Len(t, txs, 2)
		assert.Equal(t, types.TxStatusReplaced, txs[0].Status)
		assert.Equal(t, types.TxStatusPending, txs[1].Status)

		// Check the pending transactions again after attempting to resubmit.
		s.checkPendingTransaction()
		assert.NoError(t, err)

		txs, err = s.pendingTransactionOrm.GetPendingOrReplacedTransactionsBySenderType(context.Background(), s.senderType, 1)
		assert.NoError(t, err)
		assert.Len(t, txs, 0)

		s.Stop()
		patchGuard1.Reset()
		patchGuard2.Reset()
	}
}

func testCheckPendingTransactionReplacedTxConfirmed(t *testing.T) {
	for _, txType := range txTypes {
		sqlDB, err := db.DB()
		assert.NoError(t, err)
		assert.NoError(t, migrate.ResetDB(sqlDB))

		cfgCopy := *cfg.L2Config.RelayerConfig.SenderConfig
		cfgCopy.TxType = txType
		cfgCopy.EscalateBlocks = 0
		s, err := NewSender(context.Background(), &cfgCopy, privateKey, "test", "test", types.SenderTypeL1GasOracle, db, nil)
		assert.NoError(t, err)

		patchGuard1 := gomonkey.ApplyMethodFunc(s.client, "SendTransaction", func(_ context.Context, _ *gethTypes.Transaction) error {
			return nil
		})

		txHash, err := s.SendTransaction("test", &common.Address{}, nil, randBlob(), 0)
		assert.NoError(t, err)

		txs, err := s.pendingTransactionOrm.GetPendingOrReplacedTransactionsBySenderType(context.Background(), s.senderType, 1)
		assert.NoError(t, err)
		assert.Len(t, txs, 1)
		assert.Equal(t, types.TxStatusPending, txs[0].Status)
		assert.Equal(t, types.SenderTypeL1GasOracle, txs[0].SenderType)

		patchGuard2 := gomonkey.ApplyMethodFunc(s.client, "TransactionReceipt", func(_ context.Context, hash common.Hash) (*gethTypes.Receipt, error) {
			var status types.TxStatus
			status, err = s.pendingTransactionOrm.GetTxStatusByTxHash(context.Background(), hash)
			if err != nil {
				return nil, fmt.Errorf("failed to get transaction status, hash: %s, err: %w", hash.String(), err)
			}
			// If the transaction status is 'replaced', return a successful receipt.
			if status == types.TxStatusReplaced {
				return &gethTypes.Receipt{
					TxHash:      hash,
					BlockNumber: big.NewInt(0),
					Status:      gethTypes.ReceiptStatusSuccessful,
				}, nil
			}
			return nil, fmt.Errorf("simulated transaction receipt error")
		})

		// Attempt to resubmit the transaction.
		s.checkPendingTransaction()
		assert.NoError(t, err)

		status, err := s.pendingTransactionOrm.GetTxStatusByTxHash(context.Background(), txHash)
		assert.NoError(t, err)
		assert.Equal(t, types.TxStatusReplaced, status)

		txs, err = s.pendingTransactionOrm.GetPendingOrReplacedTransactionsBySenderType(context.Background(), s.senderType, 2)
		assert.NoError(t, err)
		assert.Len(t, txs, 2)
		assert.Equal(t, types.TxStatusReplaced, txs[0].Status)
		assert.Equal(t, types.TxStatusPending, txs[1].Status)

		// Check the pending transactions again after attempting to resubmit.
		s.checkPendingTransaction()
		assert.NoError(t, err)

		txs, err = s.pendingTransactionOrm.GetPendingOrReplacedTransactionsBySenderType(context.Background(), s.senderType, 1)
		assert.NoError(t, err)
		assert.Len(t, txs, 0)

		s.Stop()
		patchGuard1.Reset()
		patchGuard2.Reset()
	}
}

func testCheckPendingTransactionTxMultipleTimesWithOnlyOneTxPending(t *testing.T) {
	for _, txType := range txTypes {
		sqlDB, err := db.DB()
		assert.NoError(t, err)
		assert.NoError(t, migrate.ResetDB(sqlDB))

		cfgCopy := *cfg.L2Config.RelayerConfig.SenderConfig
		cfgCopy.TxType = txType
		cfgCopy.EscalateBlocks = 0
		s, err := NewSender(context.Background(), &cfgCopy, privateKey, "test", "test", types.SenderTypeCommitBatch, db, nil)
		assert.NoError(t, err)

		patchGuard1 := gomonkey.ApplyMethodFunc(s.client, "SendTransaction", func(_ context.Context, _ *gethTypes.Transaction) error {
			return nil
		})

		_, err = s.SendTransaction("test", &common.Address{}, nil, randBlob(), 0)
		assert.NoError(t, err)

		txs, err := s.pendingTransactionOrm.GetPendingOrReplacedTransactionsBySenderType(context.Background(), s.senderType, 1)
		assert.NoError(t, err)
		assert.Len(t, txs, 1)
		assert.Equal(t, types.TxStatusPending, txs[0].Status)
		assert.Equal(t, types.SenderTypeCommitBatch, txs[0].SenderType)

		patchGuard2 := gomonkey.ApplyMethodFunc(s.client, "TransactionReceipt", func(_ context.Context, hash common.Hash) (*gethTypes.Receipt, error) {
			return nil, fmt.Errorf("simulated transaction receipt error")
		})

		for i := 1; i <= 6; i++ {
			s.checkPendingTransaction()
			assert.NoError(t, err)

			txs, err = s.pendingTransactionOrm.GetPendingOrReplacedTransactionsBySenderType(context.Background(), s.senderType, 100)
			assert.NoError(t, err)
			assert.Len(t, txs, i+1)
			for j := 0; j < i; j++ {
				assert.Equal(t, types.TxStatusReplaced, txs[j].Status)
			}
			assert.Equal(t, types.TxStatusPending, txs[i].Status)
		}

		s.Stop()
		patchGuard1.Reset()
		patchGuard2.Reset()
	}
}

func testBlobTransactionWithBlobhashOpContractCall(t *testing.T) {
	sqlDB, err := db.DB()
	assert.NoError(t, err)
	assert.NoError(t, migrate.ResetDB(sqlDB))

	blob := randBlob()
	sideCar, err := makeSidecar(blob)
	assert.NoError(t, err)
	versionedHash := sideCar.BlobHashes()[0]
	blsModulo, ok := new(big.Int).SetString("52435875175126190479447740508185965837690552500527637822603658699938581184513", 10)
	assert.True(t, ok)
	pointHash := crypto.Keccak256Hash(versionedHash.Bytes())
	pointBigInt := new(big.Int).SetBytes(pointHash.Bytes())
	point := kzg4844.Point(new(big.Int).Mod(pointBigInt, blsModulo).Bytes())
	commitment := sideCar.Commitments[0]
	proof, claim, err := kzg4844.ComputeProof(*blob, point)
	assert.NoError(t, err)

	var claimArray [32]byte
	copy(claimArray[:], claim[:])

	demoContractMetaData := &bind.MetaData{ABI: "[{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"claim\",\"type\":\"bytes32\"},{\"internalType\":\"bytes\",\"name\":\"commitment\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"proof\",\"type\":\"bytes\"}],\"name\":\"verifyProof\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"}]"}
	demoContractABI, err := demoContractMetaData.GetAbi()
	assert.NoError(t, err)

	data, err := demoContractABI.Pack(
		"verifyProof",
		claimArray,
		commitment[:],
		proof[:],
	)
	assert.NoError(t, err)

	cfgCopy := *cfg.L2Config.RelayerConfig.SenderConfig
	cfgCopy.TxType = DynamicFeeTxType
	s, err := NewSender(context.Background(), &cfgCopy, privateKey, "test", "test", types.SenderTypeL1GasOracle, db, nil)
	assert.NoError(t, err)
	defer s.Stop()

	_, err = s.SendTransaction("0", &testContractsAddress, data, blob, 0)
	assert.NoError(t, err)

	var txHash common.Hash
	assert.Eventually(t, func() bool {
		txs, err := s.pendingTransactionOrm.GetConfirmedTransactionsBySenderType(context.Background(), s.senderType, 100)
		assert.NoError(t, err)
		if len(txs) == 1 {
			txHash = common.HexToHash(txs[0].Hash)
			return true
		}
		return false
	}, 30*time.Second, time.Second)

	assert.Eventually(t, func() bool {
		receipt, err := s.client.TransactionReceipt(context.Background(), txHash)
		return err == nil && receipt.Status == gethTypes.ReceiptStatusSuccessful
	}, 30*time.Second, time.Second)
}

func randBlob() *kzg4844.Blob {
	var blob kzg4844.Blob
	for i := 0; i < len(blob); i += gokzg4844.SerializedScalarSize {
		fieldElementBytes := randFieldElement()
		copy(blob[i:i+gokzg4844.SerializedScalarSize], fieldElementBytes[:])
	}
	return &blob
}

func randFieldElement() [32]byte {
	bytes := make([]byte, 32)
	_, err := rand.Read(bytes)
	if err != nil {
		panic("failed to get random field element")
	}
	var r fr.Element
	r.SetBytes(bytes)

	return gokzg4844.SerializeScalar(r)
}
