// Copyright 2026 The Taiko Authors
// This file is part of the go-ethereum library.
//
// Licensed under the GNU Lesser General Public License v3 or later.

package core

import (
	"errors"
	"math"
	"math/big"
	"testing"

	"github.com/ethereum/go-ethereum/common"
	"github.com/ethereum/go-ethereum/core/state"
	"github.com/ethereum/go-ethereum/core/tracing"
	"github.com/ethereum/go-ethereum/core/types"
	"github.com/ethereum/go-ethereum/core/vm"
	"github.com/ethereum/go-ethereum/crypto"
	"github.com/ethereum/go-ethereum/params"
	"github.com/holiman/uint256"
)

// CHANGE(taiko): unzenTestChainConfig returns a Taiko chain config with Unzen (and
// all prior forks) active from genesis. Used by Unzen truncation parity tests.
func unzenTestChainConfig(t *testing.T) *params.ChainConfig {
	t.Helper()
	zero := uint64(0)
	cfg := *params.MergedTestChainConfig
	cfg.Taiko = true
	cfg.ChainID = big.NewInt(167000)
	cfg.UnzenTime = &zero
	cfg.OsakaTime = &zero
	return &cfg
}

// CHANGE(taiko): unzenTestScheduleExhausting returns a zk gas schedule whose
// BlockLimit is zero, so the very first opcode that would consume non-zero
// zk gas exhausts the budget.
func unzenTestScheduleExhausting() *vm.ZkGasSchedule {
	s := &vm.ZkGasSchedule{BlockLimit: 0}
	for i := range s.OpcodeMultipliers {
		s.OpcodeMultipliers[i] = math.MaxUint16
	}
	return s
}

// CHANGE(taiko): TestApplyTransactionWithEVM_ZkGasExhausted_RevertsAndReturnsError
// documents the expected consensus zk-gas parity behavior: when the interpreter
// surfaces a zk-gas-limit error via result.Err, ApplyTransactionWithEVM should
// revert all tx-level state mutations (nonce, balance, gas pool) and return
// vm.ErrZkGasLimitExceeded as a Go error. Without the fix in Task 2, this test
// fails because the function builds a failed-status receipt and returns nil err.
func TestApplyTransactionWithEVM_ZkGasExhausted_RevertsAndReturnsError(t *testing.T) {
	var (
		chainConfig    = unzenTestChainConfig(t)
		senderKey, _   = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
		sender         = crypto.PubkeyToAddress(senderKey.PublicKey)
		callee         = common.HexToAddress("0x000000000000000000000000000000000000c0de")
		initialBalance = new(big.Int).SetUint64(1_000_000_000_000_000_000) // 1 ETH
		initialNonce   = uint64(5)
	)

	// A minimal contract body: ADD; STOP. Any opcode that would charge non-zero
	// zk gas exhausts under the zero-budget schedule.
	calleeCode := []byte{0x60, 0x01, 0x60, 0x01, 0x01, 0x00} // PUSH1 1 PUSH1 1 ADD STOP

	statedb, _ := state.New(types.EmptyRootHash, state.NewDatabaseForTesting())
	statedb.SetBalance(sender, uint256.MustFromBig(initialBalance), 0)
	statedb.SetNonce(sender, initialNonce, 0)
	statedb.SetCode(callee, calleeCode, tracing.CodeChangeGenesis)
	statedb.Finalise(true)
	stateRootBefore := statedb.IntermediateRoot(true)

	gp := NewGasPool(30_000_000)
	gpBefore := gp.Gas()

	tx, err := types.SignTx(
		types.NewTransaction(initialNonce, callee, big.NewInt(0), 100_000, big.NewInt(1_000_000_000), nil),
		types.LatestSigner(chainConfig),
		senderKey,
	)
	if err != nil {
		t.Fatalf("sign tx: %v", err)
	}
	msg, err := TransactionToMessage(tx, types.LatestSigner(chainConfig), big.NewInt(1_000_000_000))
	if err != nil {
		t.Fatalf("to message: %v", err)
	}

	blockCtx := vm.BlockContext{
		CanTransfer: CanTransfer,
		Transfer:    Transfer,
		GetHash:     func(uint64) common.Hash { return common.Hash{} },
		Coinbase:    common.Address{},
		BlockNumber: big.NewInt(1),
		Time:        1,
		Difficulty:  big.NewInt(0),
		BaseFee:     big.NewInt(1_000_000_000),
		GasLimit:    30_000_000,
		Random:      &common.Hash{},
		BlobBaseFee: big.NewInt(1),
	}
	vmConfig := vm.Config{ZkGasMeter: vm.NewZkGasMeter(unzenTestScheduleExhausting())}
	evm := vm.NewEVM(blockCtx, statedb, chainConfig, vmConfig)

	receipt, applyErr := ApplyTransactionWithEVM(
		msg, gp, statedb, big.NewInt(1), common.Hash{}, 1, tx, evm,
	)

	if !errors.Is(applyErr, vm.ErrZkGasLimitExceeded) {
		t.Fatalf("expected ErrZkGasLimitExceeded, got err=%v receipt=%+v", applyErr, receipt)
	}
	if receipt != nil {
		t.Fatalf("expected nil receipt on zk-gas exhaustion, got %+v", receipt)
	}
	if got := statedb.GetNonce(sender); got != initialNonce {
		t.Fatalf("sender nonce changed: got %d want %d", got, initialNonce)
	}
	if got := statedb.GetBalance(sender).ToBig(); got.Cmp(initialBalance) != 0 {
		t.Fatalf("sender balance changed: got %s want %s", got, initialBalance)
	}
	if got := gp.Gas(); got != gpBefore {
		t.Fatalf("gas pool changed: got %d want %d", got, gpBefore)
	}
	statedb.Finalise(true)
	if got := statedb.IntermediateRoot(true); got != stateRootBefore {
		t.Fatalf("state root changed: got %s want %s", got.Hex(), stateRootBefore.Hex())
	}
}

// CHANGE(taiko): deployExhaustingContract installs a tight loop at `addr` that
// burns opcode gas (and therefore zk gas) quickly. The body is:
//
//	JUMPDEST; PUSH1 0; PUSH1 0; ADD; POP; PUSH1 0; JUMP
//
// It loops until out of gas, which under a tight zk gas budget means the
// block-wide zk gas limit trips first.
func deployExhaustingContract(t *testing.T, statedb *state.StateDB, addr common.Address) {
	t.Helper()
	statedb.SetCode(addr, []byte{0x5b, 0x60, 0x00, 0x60, 0x00, 0x01, 0x50, 0x60, 0x00, 0x56}, tracing.CodeChangeGenesis)
}

// CHANGE(taiko): TestApplyTransactionWithEVM_UnzenCommitThenTruncate drives two
// transactions against a shared ZkGasMeter. Tx A (a small value transfer)
// succeeds and is committed. Tx B (a call to a tight-loop contract) exhausts
// the remaining block budget and must be reverted with no state imprint.
// Mirrors the Rust reference block-executor truncation contract
// (crates/block/src/executor.rs:261-291).
func TestApplyTransactionWithEVM_UnzenCommitThenTruncate(t *testing.T) {
	var (
		chainConfig = unzenTestChainConfig(t)
		key1, _     = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
		addr1       = crypto.PubkeyToAddress(key1.PublicKey)
		// Mirror of core/state_processor_test.go:51 — the literal "0020" in the
		// middle is load-bearing (matches an existing fixture key used across
		// the repo), not a typo.
		key2, _        = crypto.HexToECDSA("0202020202020202020202020202020202020202020202020202002020202020")
		addr2          = crypto.PubkeyToAddress(key2.PublicKey)
		heavyContract  = common.HexToAddress("0x000000000000000000000000000000000000beef")
		initialBalance = new(big.Int).SetUint64(1_000_000_000_000_000_000)
	)

	statedb, _ := state.New(types.EmptyRootHash, state.NewDatabaseForTesting())
	statedb.SetBalance(addr1, uint256.MustFromBig(initialBalance), 0)
	statedb.SetBalance(addr2, uint256.MustFromBig(initialBalance), 0)
	deployExhaustingContract(t, statedb, heavyContract)
	statedb.Finalise(true)

	signer := types.LatestSigner(chainConfig)
	txA, err := types.SignTx(types.NewTransaction(0, addr2, big.NewInt(1), params.TxGas, big.NewInt(1_000_000_000), nil), signer, key1)
	if err != nil {
		t.Fatal(err)
	}
	txB, err := types.SignTx(types.NewTransaction(0, heavyContract, big.NewInt(0), 5_000_000, big.NewInt(1_000_000_000), nil), signer, key2)
	if err != nil {
		t.Fatal(err)
	}

	// Tight budget that admits tx A but not tx B.
	schedule := &vm.ZkGasSchedule{BlockLimit: 50_000}
	for i := range schedule.OpcodeMultipliers {
		schedule.OpcodeMultipliers[i] = 10
	}

	gp := NewGasPool(30_000_000)
	meter := vm.NewZkGasMeter(schedule)
	vmConfig := vm.Config{ZkGasMeter: meter}
	blockCtx := vm.BlockContext{
		CanTransfer: CanTransfer,
		Transfer:    Transfer,
		GetHash:     func(uint64) common.Hash { return common.Hash{} },
		Coinbase:    common.Address{},
		BlockNumber: big.NewInt(1),
		Time:        1,
		Difficulty:  big.NewInt(0),
		BaseFee:     big.NewInt(1_000_000_000),
		GasLimit:    30_000_000,
		Random:      &common.Hash{},
		BlobBaseFee: big.NewInt(1),
	}
	evm := vm.NewEVM(blockCtx, statedb, chainConfig, vmConfig)

	// Apply txA — must succeed.
	msgA, _ := TransactionToMessage(txA, signer, blockCtx.BaseFee)
	meter.ResetTransaction()
	receiptA, err := ApplyTransactionWithEVM(msgA, gp, statedb, blockCtx.BlockNumber, common.Hash{}, blockCtx.Time, txA, evm)
	if err != nil {
		t.Fatalf("txA unexpectedly failed: %v", err)
	}
	if receiptA == nil || receiptA.Status != types.ReceiptStatusSuccessful {
		t.Fatalf("txA unexpected receipt: %+v", receiptA)
	}
	if err := meter.CommitTransaction(); err != nil {
		t.Fatalf("commit txA meter: %v", err)
	}
	// Capture addr2's state after txA commits — any change after txB runs must
	// be a txB-origin mutation that the snapshot/revert path failed to undo.
	preNonce2 := statedb.GetNonce(addr2)
	preBalance2 := new(big.Int).Set(statedb.GetBalance(addr2).ToBig())
	postTxARoot := statedb.IntermediateRoot(true)
	postTxAGp := gp.Gas()

	// Apply txB — must return ErrZkGasLimitExceeded and leave addr2 untouched.
	msgB, _ := TransactionToMessage(txB, signer, blockCtx.BaseFee)
	meter.ResetTransaction()
	receiptB, errB := ApplyTransactionWithEVM(msgB, gp, statedb, blockCtx.BlockNumber, common.Hash{}, blockCtx.Time, txB, evm)
	if !errors.Is(errB, vm.ErrZkGasLimitExceeded) {
		t.Fatalf("txB expected ErrZkGasLimitExceeded, got err=%v receipt=%+v", errB, receiptB)
	}
	if receiptB != nil {
		t.Fatalf("txB expected nil receipt, got %+v", receiptB)
	}
	if got := statedb.GetNonce(addr2); got != preNonce2 {
		t.Fatalf("addr2 nonce mutated after exhausted tx: got %d want %d", got, preNonce2)
	}
	if got := statedb.GetBalance(addr2).ToBig(); got.Cmp(preBalance2) != 0 {
		t.Fatalf("addr2 balance mutated after exhausted tx: got %s want %s", got, preBalance2)
	}
	statedb.Finalise(true)
	if got := statedb.IntermediateRoot(true); got != postTxARoot {
		t.Fatalf("state root changed after exhausted tx: got %s want %s", got.Hex(), postTxARoot.Hex())
	}
	if got := gp.Gas(); got != postTxAGp {
		t.Fatalf("gas pool changed after exhausted tx: got %d want %d", got, postTxAGp)
	}
}

// CHANGE(taiko): TestApplyTransactionWithEVM_Unzen_IncludesTxIntrinsicInBlockZkGas
// confirms the per-tx intrinsic zk gas (taikoxyz/taiko-mono#21669) flows from
// the meter into the finalized block total when a transaction commits on the
// default Unzen schedule. A pure value transfer to a code-less destination
// charges no opcode zk gas, so the finalized block total equals exactly the
// intrinsic charge — pinning that the wiring in ApplyTransactionWithEVM
// reaches the commit path.
func TestApplyTransactionWithEVM_Unzen_IncludesTxIntrinsicInBlockZkGas(t *testing.T) {
	chainConfig := unzenTestChainConfig(t)
	schedule := &vm.UnzenZkGasSchedule
	if schedule.TxIntrinsicZkGas != vm.TxIntrinsicZkGas {
		t.Fatalf("test prerequisite: default schedule TxIntrinsicZkGas = %d, want %d",
			schedule.TxIntrinsicZkGas, vm.TxIntrinsicZkGas)
	}

	senderKey, _ := crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
	sender := crypto.PubkeyToAddress(senderKey.PublicKey)
	recipient := common.HexToAddress("0x000000000000000000000000000000000000c0de")
	initialBalance := new(big.Int).SetUint64(1_000_000_000_000_000_000)

	statedb, _ := state.New(types.EmptyRootHash, state.NewDatabaseForTesting())
	statedb.SetBalance(sender, uint256.MustFromBig(initialBalance), 0)
	statedb.Finalise(true)

	tx, err := types.SignTx(
		types.NewTransaction(0, recipient, big.NewInt(1), params.TxGas, big.NewInt(1_000_000_000), nil),
		types.LatestSigner(chainConfig),
		senderKey,
	)
	if err != nil {
		t.Fatalf("sign tx: %v", err)
	}
	msg, err := TransactionToMessage(tx, types.LatestSigner(chainConfig), big.NewInt(1_000_000_000))
	if err != nil {
		t.Fatalf("to message: %v", err)
	}

	blockCtx := vm.BlockContext{
		CanTransfer: CanTransfer,
		Transfer:    Transfer,
		GetHash:     func(uint64) common.Hash { return common.Hash{} },
		Coinbase:    common.Address{},
		BlockNumber: big.NewInt(1),
		Time:        1,
		Difficulty:  big.NewInt(0),
		BaseFee:     big.NewInt(1_000_000_000),
		GasLimit:    30_000_000,
		Random:      &common.Hash{},
		BlobBaseFee: big.NewInt(1),
	}
	gp := NewGasPool(30_000_000)
	meter := vm.NewZkGasMeter(schedule)
	vmConfig := vm.Config{ZkGasMeter: meter}
	evm := vm.NewEVM(blockCtx, statedb, chainConfig, vmConfig)

	receipt, err := ApplyTransactionWithEVM(msg, gp, statedb, blockCtx.BlockNumber, common.Hash{}, blockCtx.Time, tx, evm)
	if err != nil {
		t.Fatalf("ApplyTransactionWithEVM: %v", err)
	}
	if receipt == nil || receipt.Status != types.ReceiptStatusSuccessful {
		t.Fatalf("expected successful receipt, got %+v", receipt)
	}
	if err := meter.CommitTransaction(); err != nil {
		t.Fatalf("CommitTransaction: %v", err)
	}
	if got := meter.BlockZkGasUsed(); got != vm.TxIntrinsicZkGas {
		t.Fatalf("BlockZkGasUsed after value transfer = %d, want %d (intrinsic only)", got, vm.TxIntrinsicZkGas)
	}
}

// CHANGE(taiko): taikoSystemCallTestBlockContext returns a minimal block
// context for exercising the system-call helpers directly.
func taikoSystemCallTestBlockContext() vm.BlockContext {
	return vm.BlockContext{
		CanTransfer: CanTransfer,
		Transfer:    Transfer,
		GetHash:     func(uint64) common.Hash { return common.Hash{} },
		BlockNumber: big.NewInt(1),
		Time:        1,
		Difficulty:  big.NewInt(0),
		BaseFee:     big.NewInt(0),
		GasLimit:    30_000_000,
		Random:      &common.Hash{},
		BlobBaseFee: big.NewInt(1),
	}
}

// CHANGE(taiko): the reference execution client never runs the EIP-7002/7251
// request-queue system calls for Taiko blocks — the requests hash is pinned
// empty — so the queue processors must skip execution even when code exists at
// the queue addresses.
func TestTaikoSkipsRequestQueueSystemCalls(t *testing.T) {
	// Returns a single byte, so a call that does execute appends one request.
	queueCode := common.Hex2Bytes("60016000526001601ff3")
	for _, tt := range []struct {
		name         string
		taiko        bool
		wantRequests int
	}{
		{"taiko skips queue calls", true, 0},
		{"upstream still processes queue calls", false, 2},
	} {
		t.Run(tt.name, func(t *testing.T) {
			config := unzenTestChainConfig(t)
			if !tt.taiko {
				cfg := *params.MergedTestChainConfig
				config = &cfg
			}
			statedb, _ := state.New(types.EmptyRootHash, state.NewDatabaseForTesting())
			statedb.SetCode(params.WithdrawalQueueAddress, queueCode, tracing.CodeChangeGenesis)
			statedb.SetCode(params.ConsolidationQueueAddress, queueCode, tracing.CodeChangeGenesis)
			statedb.Finalise(true)

			evm := vm.NewEVM(taikoSystemCallTestBlockContext(), statedb, config, vm.Config{})
			requests := [][]byte{}
			if err := ProcessWithdrawalQueue(&requests, evm); err != nil {
				t.Fatalf("ProcessWithdrawalQueue: %v", err)
			}
			if err := ProcessConsolidationQueue(&requests, evm); err != nil {
				t.Fatalf("ProcessConsolidationQueue: %v", err)
			}
			if len(requests) != tt.wantRequests {
				t.Fatalf("requests = %d, want %d", len(requests), tt.wantRequests)
			}
		})
	}
}

// CHANGE(taiko): the reference execution client caps every system call at the
// EIP-7825 transaction gas cap; pin go-ethereum's Taiko system calls to the
// same cap. (Only the cap is aligned — see systemCallGasLimit.)
func TestTaikoSystemCallGasLimitMatchesReference(t *testing.T) {
	// GAS PUSH0 SSTORE STOP: stores the observed gas into slot 0.
	gasProbe := common.Hex2Bytes("5a5f5500")
	for _, tt := range []struct {
		name  string
		taiko bool
		want  uint64
	}{
		{"taiko caps at the eip-7825 limit", true, params.MaxTxGas - 2},
		{"upstream keeps 30m", false, 30_000_000 - 2},
	} {
		t.Run(tt.name, func(t *testing.T) {
			config := unzenTestChainConfig(t)
			if !tt.taiko {
				cfg := *params.MergedTestChainConfig
				config = &cfg
			}
			statedb, _ := state.New(types.EmptyRootHash, state.NewDatabaseForTesting())
			statedb.SetCode(params.BeaconRootsAddress, gasProbe, tracing.CodeChangeGenesis)
			statedb.SetCode(params.HistoryStorageAddress, gasProbe, tracing.CodeChangeGenesis)
			statedb.Finalise(true)

			evm := vm.NewEVM(taikoSystemCallTestBlockContext(), statedb, config, vm.Config{})
			ProcessBeaconBlockRoot(common.Hash{}, evm)
			if got := statedb.GetState(params.BeaconRootsAddress, common.Hash{}).Big().Uint64(); got != tt.want {
				t.Fatalf("beacon-root system call observed gas = %d, want %d", got, tt.want)
			}
			ProcessParentBlockHash(common.Hash{}, evm)
			if got := statedb.GetState(params.HistoryStorageAddress, common.Hash{}).Big().Uint64(); got != tt.want {
				t.Fatalf("parent-hash system call observed gas = %d, want %d", got, tt.want)
			}
		})
	}
}
