package state

import (
	"testing"

	"github.com/holiman/uint256"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"github.com/erigontech/erigon/common"
	"github.com/erigontech/erigon/execution/types/accounts"
)

// These tests pin the observable behavior of the versioned read across its
// distinct branches (non-generic core + per-path read* wrappers) so a
// restructure can be validated against an identical surface.

// ------------------------------------------------------------------
// Section A: versionMap == nil (legacy / serial path)
// ------------------------------------------------------------------

// legacy path with a backing stateObject returns the storage value.
func TestVersionedRead_A1_LegacyWithStorage(t *testing.T) {
	t.Parallel()
	addr := accounts.InternAddress([20]byte{0xa1})

	// IBS without versionMap → legacy/serial path
	ibs := New(&emptyReader{})
	defer ibs.Release(false)
	bal, err := ibs.GetBalance(addr)
	require.NoError(t, err)
	// emptyReader returns nil account → zero balance, no error
	assert.True(t, bal.IsZero(), "legacy path with empty reader returns zero")
}

// legacy path, GetCode triggers the readStorage==nil branch.
// (When versionMap is nil, getStateObject is consulted directly; readStorage
// closure on CodePath is non-nil so this exercises the storage branch too.)
func TestVersionedRead_A2_LegacyGetCodeReturnsEmpty(t *testing.T) {
	t.Parallel()
	addr := accounts.InternAddress([20]byte{0xa2})

	ibs := New(&emptyReader{})
	defer ibs.Release(false)
	code, err := ibs.GetCode(addr)
	require.NoError(t, err)
	assert.Empty(t, code, "empty reader => empty code")
}

// ------------------------------------------------------------------
// Section B: deleted stateObject short-circuit
// ------------------------------------------------------------------

// B: a deleted stateObject in the local map short-circuits to defaultV.
// We trigger this by Selfdestruct-ing the address inside the same IBS,
// then reading another field (which sees so.deleted in the local map).
func TestVersionedRead_B_DeletedStateObjectReturnsDefault(t *testing.T) {
	t.Parallel()
	_, tx, domains := NewTestRwTx(t)
	mvhm := NewVersionMap(nil)
	reader := NewReaderV3(domains.AsGetter(tx))
	ibs := NewWithVersionMap(reader, mvhm)
	defer ibs.Release(false)
	ibs.SetTxContext(1, 0)

	addr := accounts.InternAddress(common.HexToAddress("0xdead"))
	// Create + selfdestruct → state object marked deleted at FinalizeTx
	ibs.CreateAccount(addr, true)
	err := ibs.SetBalance(addr, *uint256.NewInt(50), 0)
	require.NoError(t, err)
	_, err = ibs.Selfdestruct(addr, false)
	require.NoError(t, err)
	// After Selfdestruct, GetBalance on the same address returns zero per
	// EIP semantics (deleted in this tx).
	bal, err := ibs.GetBalance(addr)
	require.NoError(t, err)
	assert.True(t, bal.IsZero(), "balance after selfdestruct is zero")
}

// ------------------------------------------------------------------
// Section C: SelfDestruct active in versionMap
// ------------------------------------------------------------------

// a prior tx marked the address selfdestructed (in versionMap), then
// GetCommittedState (committed=true) must return zero immediately.
func TestVersionedRead_C5_DestructedCommittedReturnsZero(t *testing.T) {
	t.Parallel()
	_, tx, domains := NewTestRwTx(t)
	mvhm := NewVersionMap(nil)
	reader := NewReaderV3(domains.AsGetter(tx))
	ibs := NewWithVersionMap(reader, mvhm)
	defer ibs.Release(false)
	ibs.SetTxContext(1, 5)

	addr := accounts.InternAddress([20]byte{0xc5})
	key := accounts.InternKey([32]byte{0x01})
	// tx 1 wrote a slot value
	mvhm.WriteStorage(addr, key, Version{TxIndex: 1, Incarnation: 0}, *uint256.NewInt(99), true)
	// tx 2 selfdestructed the account
	mvhm.WriteSelfDestruct(addr, Version{TxIndex: 2, Incarnation: 0}, true, true)

	// Read at tx 5 with committed=true: must return zero, ignoring slot value.
	v, err := ibs.GetCommittedState(addr, key)
	require.NoError(t, err)
	assert.True(t, v.IsZero(), "committed read past selfdestruct returns zero")
}

// a prior tx marked the address selfdestructed; non-committed read
// at a path != CodePath records the SelfDestructPath dependency in the
// readSet and returns zero.
func TestVersionedRead_C6_DestructedRecordsDepAndReturnsZero(t *testing.T) {
	t.Parallel()
	_, tx, domains := NewTestRwTx(t)
	mvhm := NewVersionMap(nil)
	reader := NewReaderV3(domains.AsGetter(tx))
	ibs := NewWithVersionMap(reader, mvhm)
	defer ibs.Release(false)
	ibs.SetTxContext(1, 5)

	addr := accounts.InternAddress([20]byte{0xc6})
	mvhm.WriteSelfDestruct(addr, Version{TxIndex: 2, Incarnation: 0}, true, true)

	bal, err := ibs.GetBalance(addr)
	require.NoError(t, err)
	assert.True(t, bal.IsZero(), "non-committed balance read past selfdestruct returns zero")

	// SelfDestructPath dep must be recorded in readSet.
	_, ok := ibs.versionedReads.getHeader(addr, SelfDestructPath, accounts.NilKey)
	assert.True(t, ok, "SelfDestructPath dependency must be recorded")
}

// CodePath is exempt from the SelfDestruct short-circuit. Even if SD
// is active, a CodePath read must fall through to the actual code-read
// branches rather than returning zero.
func TestVersionedRead_C4_CodePathBypassesSelfDestruct(t *testing.T) {
	t.Parallel()
	_, tx, domains := NewTestRwTx(t)
	mvhm := NewVersionMap(nil)
	reader := NewReaderV3(domains.AsGetter(tx))
	ibs := NewWithVersionMap(reader, mvhm)
	defer ibs.Release(false)
	ibs.SetTxContext(1, 5)

	addr := accounts.InternAddress([20]byte{0xc4})
	code := []byte{0x60, 0x42, 0x60, 0x00, 0x52, 0x60, 0x20, 0x60, 0x00, 0xf3}
	mvhm.WriteCode(addr, Version{TxIndex: 2, Incarnation: 0}, accounts.NewCode(code), true)
	// Selfdestruct at an EARLIER tx than the code write so the SD doesn't
	// trump the code (the CodePath+SD trump check uses sdres.DepIdx).
	mvhm.WriteSelfDestruct(addr, Version{TxIndex: 1, Incarnation: 0}, true, true)

	got, err := ibs.GetCode(addr)
	require.NoError(t, err)
	assert.Equal(t, code, got, "CodePath should bypass SelfDestruct short-circuit")
}

// revival. After SelfDestruct, a later write to Balance / Nonce /
// CodeHash at a higher txIndex revives the account. Subsequent reads must
// see the revived value, not zero.
func TestVersionedRead_C1_RevivalViaBalance(t *testing.T) {
	t.Parallel()
	_, tx, domains := NewTestRwTx(t)
	mvhm := NewVersionMap(nil)
	reader := NewReaderV3(domains.AsGetter(tx))
	ibs := NewWithVersionMap(reader, mvhm)
	defer ibs.Release(false)
	ibs.SetTxContext(1, 10)

	addr := accounts.InternAddress([20]byte{0xc1})
	revivedBalance := uint256.NewInt(777)
	mvhm.WriteSelfDestruct(addr, Version{TxIndex: 2, Incarnation: 0}, true, true)
	mvhm.WriteBalance(addr, Version{TxIndex: 5, Incarnation: 0}, *revivedBalance, true)

	bal, err := ibs.GetBalance(addr)
	require.NoError(t, err)
	assert.Equal(t, *revivedBalance, bal, "balance after revival must be the revived value, not zero")
}

// Regression for the refresh* wrappers' missing outcomeReturnZero case: after a
// self-destruct, the per-field refresh (refreshBalance/refreshNonce/
// refreshIncarnation/refreshCodeHash, used by refreshVersionedAccount to rebuild
// an account) must return the typed ZERO, not the caller's stale pre-destruct
// value. Returning the stale value gives the rebuilt account a phantom pre-SD
// codeHash/balance, so the codeHash-change check compares the stale value
// against itself and validation never catches the divergence — a silent wrong
// state root.
func TestVersionedRead_C7_RefreshReturnsZeroPastSelfDestruct(t *testing.T) {
	t.Parallel()

	r := &refreshReader{
		account: &accounts.Account{
			Balance:     *uint256.NewInt(1234),
			Nonce:       7,
			Incarnation: 3,
			CodeHash:    accounts.InternCodeHash([32]byte{0xab}),
		},
	}
	mvhm := NewVersionMap(nil)
	ibs := NewWithVersionMap(r, mvhm)
	defer ibs.Release(false)
	ibs.SetTxContext(1, 5)

	addr := accounts.InternAddress([20]byte{0xc7})
	mvhm.WriteSelfDestruct(addr, Version{TxIndex: 2, Incarnation: 0}, true, true)

	bal, _, _, err := refreshBalance(ibs, addr, *uint256.NewInt(1234))
	require.NoError(t, err)
	assert.True(t, bal.IsZero(), "refreshBalance past SD must return zero, not the stale 1234")

	nonce, _, _, err := refreshNonce(ibs, addr, 7)
	require.NoError(t, err)
	assert.Zero(t, nonce, "refreshNonce past SD must return zero, not the stale 7")

	inc, _, _, err := refreshIncarnation(ibs, addr, 3)
	require.NoError(t, err)
	assert.Zero(t, inc, "refreshIncarnation past SD must return zero, not the stale 3")

	ch, _, _, err := refreshCodeHash(ibs, addr, accounts.InternCodeHash([32]byte{0xab}))
	require.NoError(t, err)
	assert.Equal(t, accounts.NilCodeHash, ch, "refreshCodeHash past SD must return the zero codeHash, not the phantom 0xab")
}

// ------------------------------------------------------------------
// Section D: writeSet hit (intra-tx writes)
// ------------------------------------------------------------------

// a write in the current tx is read back via versionedWrites.
func TestVersionedRead_D2_WriteSetHit(t *testing.T) {
	t.Parallel()
	_, tx, domains := NewTestRwTx(t)
	mvhm := NewVersionMap(nil)
	reader := NewReaderV3(domains.AsGetter(tx))
	ibs := NewWithVersionMap(reader, mvhm)
	defer ibs.Release(false)
	ibs.SetTxContext(1, 0)

	addr := accounts.InternAddress([20]byte{0xd2})
	target := uint256.NewInt(123)
	err := ibs.SetBalance(addr, *target, 0)
	require.NoError(t, err)

	bal, err := ibs.GetBalance(addr)
	require.NoError(t, err)
	assert.Equal(t, *target, bal, "writeSet hit returns the intra-tx written value")
}

// ------------------------------------------------------------------
// Section E: MVReadResultDone (versionMap has a definite value)
// ------------------------------------------------------------------

// versionMap MapRead hit on first call; second read at same tx hits
// the readSet (via the pr.Version == vr.Version branch).
func TestVersionedRead_E1_MapHitThenReadSetSameVersion(t *testing.T) {
	t.Parallel()
	_, tx, domains := NewTestRwTx(t)
	mvhm := NewVersionMap(nil)
	reader := NewReaderV3(domains.AsGetter(tx))
	ibs := NewWithVersionMap(reader, mvhm)
	defer ibs.Release(false)
	ibs.SetTxContext(1, 5)

	addr := accounts.InternAddress([20]byte{0xe1})
	target := uint256.NewInt(42)
	mvhm.WriteBalance(addr, Version{TxIndex: 1, Incarnation: 0}, *target, true)

	// First read populates the readSet via MapRead.
	bal1, err := ibs.GetBalance(addr)
	require.NoError(t, err)
	assert.Equal(t, *target, bal1)

	// Second read: same tx, same version → readSet hit.
	bal2, err := ibs.GetBalance(addr)
	require.NoError(t, err)
	assert.Equal(t, *target, bal2)
}

// a Done MVReadResult for CodePath is trumped if a SelfDestruct at
// a >= DepIdx is also Done — return defaultV (nil code).
func TestVersionedRead_E3a_CodePathTrumpedBySelfDestruct(t *testing.T) {
	t.Parallel()
	_, tx, domains := NewTestRwTx(t)
	mvhm := NewVersionMap(nil)
	reader := NewReaderV3(domains.AsGetter(tx))
	ibs := NewWithVersionMap(reader, mvhm)
	defer ibs.Release(false)
	ibs.SetTxContext(1, 10)

	addr := accounts.InternAddress([20]byte{0xe3})
	code := []byte{0xfe, 0xfe}
	// Code at tx 2; SelfDestruct at tx 3 (strictly later → trumps code).
	mvhm.WriteCode(addr, Version{TxIndex: 2, Incarnation: 0}, accounts.NewCode(code), true)
	mvhm.WriteSelfDestruct(addr, Version{TxIndex: 3, Incarnation: 0}, true, true)

	got, err := ibs.GetCode(addr)
	require.NoError(t, err)
	assert.Empty(t, got, "CodePath trumped by SelfDestruct returns nil/empty code")
}

// ------------------------------------------------------------------
// Section G: MVReadResultNone (no entry in versionMap for this key)
// ------------------------------------------------------------------

// readSet hit at MVReadResultNone (ReadSetRead source). Second read
// of an unwritten slot hits readSet from the first read's record.
func TestVersionedRead_G1_ReadSetReadOnSecondCall(t *testing.T) {
	t.Parallel()
	_, tx, domains := NewTestRwTx(t)
	mvhm := NewVersionMap(nil)
	reader := NewReaderV3(domains.AsGetter(tx))
	ibs := NewWithVersionMap(reader, mvhm)
	defer ibs.Release(false)
	ibs.SetTxContext(1, 5)

	addr := accounts.InternAddress([20]byte{0x91})
	key := accounts.InternKey([32]byte{0x99})

	// First read of an unwritten slot: versionMap None → storage fallback →
	// records defaultV in readSet.
	v1, err := ibs.GetState(addr, key)
	require.NoError(t, err)
	assert.True(t, v1.IsZero())

	// Second read: same tx, same key → readSet hit.
	v2, err := ibs.GetState(addr, key)
	require.NoError(t, err)
	assert.True(t, v2.IsZero())

	// readSet must hold the StoragePath entry.
	_, ok := ibs.versionedReads.getHeader(addr, StoragePath, key)
	assert.True(t, ok, "StoragePath read must be recorded")
}

// StoragePath read on an unwritten slot with IncarnationPath written
// by a prior tx → returns zero (account was created/destroyed this block,
// all unwritten slots must be zero) and records the IncarnationPath dep.
func TestVersionedRead_G6_StorageZeroOnIncarnationWritten(t *testing.T) {
	t.Parallel()
	_, tx, domains := NewTestRwTx(t)
	mvhm := NewVersionMap(nil)
	reader := NewReaderV3(domains.AsGetter(tx))
	ibs := NewWithVersionMap(reader, mvhm)
	defer ibs.Release(false)
	ibs.SetTxContext(1, 5)

	addr := accounts.InternAddress([20]byte{0x96})
	key := accounts.InternKey([32]byte{0xab})
	// Prior tx wrote IncarnationPath (CreateAccount / Selfdestruct event)
	mvhm.WriteIncarnation(addr, Version{TxIndex: 2, Incarnation: 0}, 1, true)

	got, err := ibs.GetState(addr, key)
	require.NoError(t, err)
	assert.True(t, got.IsZero(), "unwritten slot reads zero after Incarnation rewrite")

	// IncarnationPath dep must be recorded.
	_, ok := ibs.versionedReads.getHeader(addr, IncarnationPath, accounts.NilKey)
	assert.True(t, ok, "IncarnationPath dependency must be recorded")
}

// BalancePath read with no map/writeSet/readSet entry but a prior tx
// wrote an AddressPath account → use that account's balance.
func TestVersionedRead_G7_BalanceViaResolvedAddressPath(t *testing.T) {
	t.Parallel()
	_, tx, domains := NewTestRwTx(t)
	mvhm := NewVersionMap(nil)
	reader := NewReaderV3(domains.AsGetter(tx))
	ibs := NewWithVersionMap(reader, mvhm)
	defer ibs.Release(false)
	ibs.SetTxContext(1, 5)

	addr := accounts.InternAddress([20]byte{0x97})
	priorAcc := &accounts.Account{Balance: *uint256.NewInt(555), Nonce: 3}
	mvhm.WriteAddress(addr, Version{TxIndex: 2, Incarnation: 0}, priorAcc, true)

	bal, err := ibs.GetBalance(addr)
	require.NoError(t, err)
	assert.Equal(t, *uint256.NewInt(555), bal, "balance resolved via AddressPath account")
}

// storage fallback. Empty reader + no prior writes/reads → falls through
// to the readStorage callback (returns zero from emptyReader) and records
// defaultV in readSet.
func TestVersionedRead_G8_StorageFallbackEmptyReader(t *testing.T) {
	t.Parallel()
	mvhm := NewVersionMap(nil)
	ibs := NewWithVersionMap(&emptyReader{}, mvhm)
	defer ibs.Release(false)
	ibs.SetTxContext(1, 5)

	addr := accounts.InternAddress([20]byte{0x98})
	bal, err := ibs.GetBalance(addr)
	require.NoError(t, err)
	assert.True(t, bal.IsZero(), "empty reader storage fallback returns zero")
}

// refresh path: the legacy readStorage==nil branch for
// BalancePath/NoncePath/CodeHashPath/IncarnationPath records the
// caller-supplied defaultV (the "current" account field) into the
// versionedReads ReadSet, with vr.Source = StorageRead.  ValidateVersion
// and tiebreaker logic depend on the RECORDED value matching what
// downstream tx readers will see — recording the typed zero instead of
// the caller's currentBalance is a behavioral divergence that breaks
// integration tests (wrong trie root in execution/engineapi).
//
// This test calls refreshVersionedAccount-style scenarios by going
// through getVersionedAccount which triggers refreshBalance/refreshNonce/
// refreshCodeHash/refreshIncarnation with the storage-read account's
// fields as defaultV.  Asserts that subsequent reads through the
// versionedReads ReadSet see the same typed defaultV that the legacy
// code recorded.
func TestVersionedRead_G4_RefreshRecordsTypedDefaultInReadSet(t *testing.T) {
	t.Parallel()

	// emptyReader returns nil account — refreshVersionedAccount won't
	// run for nil-account paths.  We need a reader that returns a
	// non-nil account so refreshVersionedAccount's per-field refresh*
	// calls run with non-zero defaultV.
	r := &refreshReader{
		account: &accounts.Account{
			Balance:     *uint256.NewInt(1234),
			Nonce:       7,
			Incarnation: 3,
			CodeHash:    accounts.InternCodeHash([32]byte{0xab}),
		},
	}
	mvhm := NewVersionMap(nil)
	ibs := NewWithVersionMap(r, mvhm)
	defer ibs.Release(false)
	ibs.SetTxContext(1, 5)

	addr := accounts.InternAddress([20]byte{0xa4})

	// Reading the balance triggers getStateObject → getVersionedAccount
	// → refreshVersionedAccount → refreshBalance/refreshNonce/...
	bal, err := ibs.GetBalance(addr)
	require.NoError(t, err)
	assert.Equal(t, *uint256.NewInt(1234), bal,
		"GetBalance returns the storage-read account's balance after refresh")

	// Each refresh* path that ran in refreshVersionedAccount with
	// no versionMap entry should have recorded its defaultV (the
	// account's field) into the readSet.  Verify:
	balRead, ok := ibs.versionedReads.GetBalance(addr)
	require.True(t, ok, "BalancePath read must be recorded")
	assert.Equal(t, *uint256.NewInt(1234), balRead.Val,
		"recorded BalancePath value must be the refresh defaultV (currentBalance), not zero")

	nonceRead, ok := ibs.versionedReads.GetNonce(addr)
	require.True(t, ok, "NoncePath read must be recorded")
	assert.Equal(t, uint64(7), nonceRead.Val,
		"recorded NoncePath value must be the refresh defaultV (currentNonce), not zero")

	incRead, ok := ibs.versionedReads.GetIncarnation(addr)
	require.True(t, ok, "IncarnationPath read must be recorded")
	assert.Equal(t, uint64(3), incRead.Val,
		"recorded IncarnationPath value must be the refresh defaultV (currentIncarnation), not zero")

	chRead, ok := ibs.versionedReads.GetCodeHash(addr)
	require.True(t, ok, "CodeHashPath read must be recorded")
	assert.Equal(t, accounts.InternCodeHash([32]byte{0xab}), chRead.Val,
		"recorded CodeHashPath value must be the refresh defaultV (currentCodeHash), not zero")
}

// refreshReader returns a fixed account for ReadAccountData and zero/empty
// for everything else.  Used by TestVersionedRead_G4 to exercise the
// refresh path with non-zero typed defaultVs.
type refreshReader struct {
	account *accounts.Account
}

func (r *refreshReader) ReadAccountData(accounts.Address) (*accounts.Account, error) {
	return r.account, nil
}
func (r *refreshReader) ReadAccountDataForDebug(accounts.Address) (*accounts.Account, error) {
	return r.account, nil
}
func (r *refreshReader) ReadAccountStorage(accounts.Address, accounts.StorageKey) (uint256.Int, bool, error) {
	return uint256.Int{}, false, nil
}
func (r *refreshReader) HasStorage(accounts.Address) (bool, error)               { return false, nil }
func (r *refreshReader) ReadAccountCode(accounts.Address) ([]byte, error)        { return nil, nil }
func (r *refreshReader) ReadAccountCodeSize(accounts.Address) (int, error)       { return 0, nil }
func (r *refreshReader) ReadAccountIncarnation(accounts.Address) (uint64, error) { return 0, nil }
func (r *refreshReader) SetTrace(bool, string)                                   {}
func (r *refreshReader) Trace() bool                                             { return false }
func (r *refreshReader) TracePrefix() string                                     { return "" }

// revival via NoncePath rewrite at a higher TxIdx than the SD.
func TestVersionedRead_C2_RevivalViaNonce(t *testing.T) {
	t.Parallel()
	_, tx, domains := NewTestRwTx(t)
	mvhm := NewVersionMap(nil)
	reader := NewReaderV3(domains.AsGetter(tx))
	ibs := NewWithVersionMap(reader, mvhm)
	defer ibs.Release(false)
	ibs.SetTxContext(1, 10)

	addr := accounts.InternAddress([20]byte{0xc2})
	mvhm.WriteSelfDestruct(addr, Version{TxIndex: 2, Incarnation: 0}, true, true)
	mvhm.WriteNonce(addr, Version{TxIndex: 5, Incarnation: 0}, 7, true)

	n, err := ibs.GetNonce(addr)
	require.NoError(t, err)
	assert.Equal(t, uint64(7), n, "nonce after revival via NoncePath rewrite")
}

// revival via CodeHashPath rewrite at a higher TxIdx than the SD.
func TestVersionedRead_C3_RevivalViaCodeHash(t *testing.T) {
	t.Parallel()
	_, tx, domains := NewTestRwTx(t)
	mvhm := NewVersionMap(nil)
	reader := NewReaderV3(domains.AsGetter(tx))
	ibs := NewWithVersionMap(reader, mvhm)
	defer ibs.Release(false)
	ibs.SetTxContext(1, 10)

	addr := accounts.InternAddress([20]byte{0xc3})
	revivedHash := accounts.InternCodeHash([32]byte{0xab, 0xcd, 0xef})
	mvhm.WriteSelfDestruct(addr, Version{TxIndex: 2, Incarnation: 0}, true, true)
	mvhm.WriteCodeHash(addr, Version{TxIndex: 5, Incarnation: 0}, revivedHash, true)

	got, err := ibs.GetCodeHash(addr)
	require.NoError(t, err)
	assert.Equal(t, revivedHash, got, "codehash after revival via CodeHashPath rewrite")
}

// when the readSet already records a different Version than the current
// versionMap value at MapRead, versionedRead panics with ErrDependency so
// the executor knows to re-execute. Captured via recover().
func TestVersionedRead_E2_MapReadDifferentVersionPanics(t *testing.T) {
	t.Parallel()
	_, tx, domains := NewTestRwTx(t)
	mvhm := NewVersionMap(nil)
	reader := NewReaderV3(domains.AsGetter(tx))
	ibs := NewWithVersionMap(reader, mvhm)
	defer ibs.Release(false)
	ibs.SetTxContext(1, 10)

	addr := accounts.InternAddress([20]byte{0xe2})
	// Two writes at different versions; the later version wins in versionMap.
	mvhm.WriteBalance(addr, Version{TxIndex: 2, Incarnation: 0}, *uint256.NewInt(10), true)

	// Manually seed readSet with a stale version (TxIndex 1) so that the
	// pr.Version != vr.Version branch fires.
	ibs.versionedReads.SetBalance(addr, VersionedRead[uint256.Int]{
		ReadHeader: ReadHeader{Source: MapRead, Version: Version{TxIndex: 1, Incarnation: 0}},
		Val:        *uint256.NewInt(99),
	})

	defer func() {
		r := recover()
		require.NotNil(t, r, "must panic on version mismatch")
		err, ok := r.(error)
		require.True(t, ok, "panic must carry an error")
		assert.ErrorIs(t, err, ErrDependency, "panic carries ErrDependency")
	}()
	_, _ = ibs.GetBalance(addr)
}

// F: MVReadResultDependency status (versionMap saw an in-progress dep at a
// higher TxIdx than the current tx) → panic ErrDependency. Triggered by
// writing at our own txIndex's Done-but-Estimate state. Simplest reliable
// trigger: an Estimate-status entry (complete=false) at a TxIdx <= ours.
func TestVersionedRead_F_MVReadResultDependencyPanics(t *testing.T) {
	t.Parallel()
	_, tx, domains := NewTestRwTx(t)
	mvhm := NewVersionMap(nil)
	reader := NewReaderV3(domains.AsGetter(tx))
	ibs := NewWithVersionMap(reader, mvhm)
	defer ibs.Release(false)
	ibs.SetTxContext(1, 5)

	addr := accounts.InternAddress([20]byte{0xf0})
	// Estimate entry (complete=false) at txIndex 2 < 5 → Dependency.
	mvhm.WriteBalance(addr, Version{TxIndex: 2, Incarnation: 0}, *uint256.NewInt(50), false)

	defer func() {
		r := recover()
		require.NotNil(t, r, "must panic on Dependency status")
		err, ok := r.(error)
		require.True(t, ok)
		assert.ErrorIs(t, err, ErrDependency)
	}()
	_, _ = ibs.GetBalance(addr)
}

// writeSet hit at MVReadResultDone, but readSet has a stale version
// → panic ErrDependency (a write was based on a stale read). The current
// tx wrote, but the readSet for the same path holds a version older than
// the versionMap's Done entry.
func TestVersionedRead_D1_WriteSetHitWithStaleReadSetPanics(t *testing.T) {
	t.Parallel()
	_, tx, domains := NewTestRwTx(t)
	mvhm := NewVersionMap(nil)
	reader := NewReaderV3(domains.AsGetter(tx))
	ibs := NewWithVersionMap(reader, mvhm)
	defer ibs.Release(false)
	ibs.SetTxContext(1, 5)

	addr := accounts.InternAddress([20]byte{0xd1})
	// versionMap Done at tx 3 — higher than the readSet's stale tx-1 entry.
	mvhm.WriteBalance(addr, Version{TxIndex: 3, Incarnation: 0}, *uint256.NewInt(30), true)

	// Seed a stale readSet entry at a lower version.
	ibs.versionedReads.SetBalance(addr, VersionedRead[uint256.Int]{
		ReadHeader: ReadHeader{Source: MapRead, Version: Version{TxIndex: 1, Incarnation: 0}},
		Val:        *uint256.NewInt(99),
	})
	// Seed a current-tx writeSet entry (intra-tx write) so the writeSet
	// branch fires.
	err := ibs.SetBalance(addr, *uint256.NewInt(77), 0)
	require.NoError(t, err)

	defer func() {
		r := recover()
		require.NotNil(t, r, "must panic when writeSet hit conflicts with stale readSet at versionMap Done")
		err, ok := r.(error)
		require.True(t, ok)
		assert.ErrorIs(t, err, ErrDependency)
	}()
	_, _ = ibs.GetBalance(addr)
}
