package stategen

import (
	"testing"

	"github.com/OffchainLabs/prysm/v6/beacon-chain/core/blocks"
	"github.com/OffchainLabs/prysm/v6/beacon-chain/core/helpers"
	"github.com/OffchainLabs/prysm/v6/beacon-chain/db"
	testDB "github.com/OffchainLabs/prysm/v6/beacon-chain/db/testing"
	doublylinkedtree "github.com/OffchainLabs/prysm/v6/beacon-chain/forkchoice/doubly-linked-tree"
	stateTesting "github.com/OffchainLabs/prysm/v6/beacon-chain/state/testing"
	"github.com/OffchainLabs/prysm/v6/config/params"
	consensusblocks "github.com/OffchainLabs/prysm/v6/consensus-types/blocks"
	"github.com/OffchainLabs/prysm/v6/consensus-types/interfaces"
	"github.com/OffchainLabs/prysm/v6/consensus-types/primitives"
	"github.com/OffchainLabs/prysm/v6/crypto/bls"
	"github.com/OffchainLabs/prysm/v6/encoding/bytesutil"
	ethpb "github.com/OffchainLabs/prysm/v6/proto/prysm/v1alpha1"
	"github.com/OffchainLabs/prysm/v6/runtime/version"
	"github.com/OffchainLabs/prysm/v6/testing/assert"
	"github.com/OffchainLabs/prysm/v6/testing/require"
	"github.com/OffchainLabs/prysm/v6/testing/util"
	"github.com/ethereum/go-ethereum/common/hexutil"
	"google.golang.org/protobuf/proto"
)

func TestReplayBlocks_AllSkipSlots(t *testing.T) {
	beaconDB := testDB.SetupDB(t)

	beaconState, _ := util.DeterministicGenesisState(t, 32)
	genesisBlock := blocks.NewGenesisBlock([]byte{})
	bodyRoot, err := genesisBlock.Block.HashTreeRoot()
	require.NoError(t, err)
	err = beaconState.SetLatestBlockHeader(&ethpb.BeaconBlockHeader{
		Slot:       genesisBlock.Block.Slot,
		ParentRoot: genesisBlock.Block.ParentRoot,
		StateRoot:  params.BeaconConfig().ZeroHash[:],
		BodyRoot:   bodyRoot[:],
	})
	require.NoError(t, err)
	require.NoError(t, beaconState.SetSlashings(make([]uint64, params.BeaconConfig().EpochsPerSlashingsVector)))
	cp := beaconState.CurrentJustifiedCheckpoint()
	var mockRoot [32]byte
	copy(mockRoot[:], "hello-world")
	cp.Root = mockRoot[:]
	require.NoError(t, beaconState.SetCurrentJustifiedCheckpoint(cp))
	require.NoError(t, beaconState.AppendCurrentEpochAttestations(&ethpb.PendingAttestation{}))

	service := New(beaconDB, doublylinkedtree.New())
	targetSlot := params.BeaconConfig().SlotsPerEpoch - 1
	newState, err := service.replayBlocks(t.Context(), beaconState, []interfaces.ReadOnlySignedBeaconBlock{}, targetSlot)
	require.NoError(t, err)
	assert.Equal(t, targetSlot, newState.Slot(), "Did not advance slots")
}

func TestReplayBlocks_SameSlot(t *testing.T) {
	beaconDB := testDB.SetupDB(t)

	beaconState, _ := util.DeterministicGenesisState(t, 32)
	genesisBlock := blocks.NewGenesisBlock([]byte{})
	bodyRoot, err := genesisBlock.Block.HashTreeRoot()
	require.NoError(t, err)
	err = beaconState.SetLatestBlockHeader(&ethpb.BeaconBlockHeader{
		Slot:       genesisBlock.Block.Slot,
		ParentRoot: genesisBlock.Block.ParentRoot,
		StateRoot:  params.BeaconConfig().ZeroHash[:],
		BodyRoot:   bodyRoot[:],
	})
	require.NoError(t, err)
	require.NoError(t, beaconState.SetSlashings(make([]uint64, params.BeaconConfig().EpochsPerSlashingsVector)))
	cp := beaconState.CurrentJustifiedCheckpoint()
	var mockRoot [32]byte
	copy(mockRoot[:], "hello-world")
	cp.Root = mockRoot[:]
	require.NoError(t, beaconState.SetCurrentJustifiedCheckpoint(cp))
	require.NoError(t, beaconState.AppendCurrentEpochAttestations(&ethpb.PendingAttestation{}))

	service := New(beaconDB, doublylinkedtree.New())
	targetSlot := beaconState.Slot()
	newState, err := service.replayBlocks(t.Context(), beaconState, []interfaces.ReadOnlySignedBeaconBlock{}, targetSlot)
	require.NoError(t, err)
	assert.Equal(t, targetSlot, newState.Slot(), "Did not advance slots")
}

func TestReplayBlocks_LowerSlotBlock(t *testing.T) {
	beaconDB := testDB.SetupDB(t)

	beaconState, _ := util.DeterministicGenesisState(t, 32)
	require.NoError(t, beaconState.SetSlot(1))
	genesisBlock := blocks.NewGenesisBlock([]byte{})
	bodyRoot, err := genesisBlock.Block.HashTreeRoot()
	require.NoError(t, err)
	err = beaconState.SetLatestBlockHeader(&ethpb.BeaconBlockHeader{
		Slot:       genesisBlock.Block.Slot,
		ParentRoot: genesisBlock.Block.ParentRoot,
		StateRoot:  params.BeaconConfig().ZeroHash[:],
		BodyRoot:   bodyRoot[:],
	})
	require.NoError(t, err)
	require.NoError(t, beaconState.SetSlashings(make([]uint64, params.BeaconConfig().EpochsPerSlashingsVector)))
	cp := beaconState.CurrentJustifiedCheckpoint()
	var mockRoot [32]byte
	copy(mockRoot[:], "hello-world")
	cp.Root = mockRoot[:]
	require.NoError(t, beaconState.SetCurrentJustifiedCheckpoint(cp))
	require.NoError(t, beaconState.AppendCurrentEpochAttestations(&ethpb.PendingAttestation{}))

	service := New(beaconDB, doublylinkedtree.New())
	targetSlot := beaconState.Slot()
	b := util.NewBeaconBlock()
	b.Block.Slot = beaconState.Slot() - 1
	wsb, err := consensusblocks.NewSignedBeaconBlock(b)
	require.NoError(t, err)
	newState, err := service.replayBlocks(t.Context(), beaconState, []interfaces.ReadOnlySignedBeaconBlock{wsb}, targetSlot)
	require.NoError(t, err)
	assert.Equal(t, targetSlot, newState.Slot(), "Did not advance slots")
}

func TestReplayBlocks_ThroughForkBoundary(t *testing.T) {
	params.SetupTestConfigCleanup(t)
	bCfg := params.BeaconConfig().Copy()
	bCfg.AltairForkEpoch = 1
	bCfg.ForkVersionSchedule[bytesutil.ToBytes4(bCfg.AltairForkVersion)] = 1
	params.OverrideBeaconConfig(bCfg)

	beaconState, _ := util.DeterministicGenesisState(t, 32)
	genesisBlock := blocks.NewGenesisBlock([]byte{})
	bodyRoot, err := genesisBlock.Block.HashTreeRoot()
	require.NoError(t, err)
	err = beaconState.SetLatestBlockHeader(&ethpb.BeaconBlockHeader{
		Slot:       genesisBlock.Block.Slot,
		ParentRoot: genesisBlock.Block.ParentRoot,
		StateRoot:  params.BeaconConfig().ZeroHash[:],
		BodyRoot:   bodyRoot[:],
	})
	require.NoError(t, err)

	service := New(testDB.SetupDB(t), doublylinkedtree.New())
	targetSlot := params.BeaconConfig().SlotsPerEpoch
	newState, err := service.replayBlocks(t.Context(), beaconState, []interfaces.ReadOnlySignedBeaconBlock{}, targetSlot)
	require.NoError(t, err)

	// Verify state is version Altair.
	assert.Equal(t, version.Altair, newState.Version())
}

func TestReplayBlocks_ThroughFutureForkBoundaries(t *testing.T) {
	params.SetupTestConfigCleanup(t)
	bCfg := params.BeaconConfig().Copy()
	bCfg.AltairForkEpoch = 1
	bCfg.ForkVersionSchedule[bytesutil.ToBytes4(bCfg.AltairForkVersion)] = 1
	bCfg.BellatrixForkEpoch = 2
	bCfg.ForkVersionSchedule[bytesutil.ToBytes4(bCfg.BellatrixForkVersion)] = 2
	bCfg.CapellaForkEpoch = 3
	bCfg.ForkVersionSchedule[bytesutil.ToBytes4(bCfg.CapellaForkVersion)] = 3
	bCfg.DenebForkEpoch = 4
	bCfg.ForkVersionSchedule[bytesutil.ToBytes4(bCfg.DenebForkVersion)] = 4
	bCfg.ElectraForkEpoch = 5
	bCfg.ForkVersionSchedule[bytesutil.ToBytes4(bCfg.ElectraForkVersion)] = 5
	bCfg.FuluForkEpoch = 6
	bCfg.ForkVersionSchedule[bytesutil.ToBytes4(bCfg.FuluForkVersion)] = 6
	params.OverrideBeaconConfig(bCfg)

	beaconState, _ := util.DeterministicGenesisState(t, 32)
	genesisBlock := blocks.NewGenesisBlock([]byte{})
	bodyRoot, err := genesisBlock.Block.HashTreeRoot()
	require.NoError(t, err)
	err = beaconState.SetLatestBlockHeader(&ethpb.BeaconBlockHeader{
		Slot:       genesisBlock.Block.Slot,
		ParentRoot: genesisBlock.Block.ParentRoot,
		StateRoot:  params.BeaconConfig().ZeroHash[:],
		BodyRoot:   bodyRoot[:],
	})
	require.NoError(t, err)

	service := New(testDB.SetupDB(t), doublylinkedtree.New())
	targetSlot := params.BeaconConfig().SlotsPerEpoch * 2
	newState, err := service.replayBlocks(t.Context(), beaconState, []interfaces.ReadOnlySignedBeaconBlock{}, targetSlot)
	require.NoError(t, err)

	// Verify state is version Bellatrix.
	assert.Equal(t, version.Bellatrix, newState.Version())

	targetSlot = params.BeaconConfig().SlotsPerEpoch * 3
	newState, err = service.replayBlocks(t.Context(), newState, []interfaces.ReadOnlySignedBeaconBlock{}, targetSlot)
	require.NoError(t, err)

	// Verify state is version Capella.
	assert.Equal(t, version.Capella, newState.Version())

	targetSlot = params.BeaconConfig().SlotsPerEpoch * 4
	newState, err = service.replayBlocks(t.Context(), newState, []interfaces.ReadOnlySignedBeaconBlock{}, targetSlot)
	require.NoError(t, err)

	// Verify state is version Deneb.
	assert.Equal(t, version.Deneb, newState.Version())

	targetSlot = params.BeaconConfig().SlotsPerEpoch * 5
	newState, err = service.replayBlocks(t.Context(), newState, []interfaces.ReadOnlySignedBeaconBlock{}, targetSlot)
	require.NoError(t, err)

	// Verify state is version Electra.
	assert.Equal(t, version.Electra, newState.Version())
}

func TestReplayBlocks_ProcessEpoch_Electra(t *testing.T) {
	params.SetupTestConfigCleanup(t)
	bCfg := params.BeaconConfig().Copy()
	bCfg.ElectraForkEpoch = 1
	bCfg.ForkVersionSchedule[bytesutil.ToBytes4(bCfg.ElectraForkVersion)] = 1
	params.OverrideBeaconConfig(bCfg)

	beaconState, _ := util.DeterministicGenesisStateElectra(t, 1)
	require.NoError(t, beaconState.SetDepositBalanceToConsume(100))
	amountAvailForProcessing := helpers.ActivationExitChurnLimit(1_000 * 1e9)
	genesisBlock := util.NewBeaconBlockElectra()

	sk, err := bls.RandKey()
	require.NoError(t, err)
	ethAddress, err := hexutil.Decode("0x967646dCD8d34F4E02204faeDcbAe0cC96fB9245")
	require.NoError(t, err)
	newCredentials := make([]byte, 12)
	newCredentials[0] = params.BeaconConfig().ETH1AddressWithdrawalPrefixByte
	withdrawalCredentials := append(newCredentials, ethAddress...)
	ffe := params.BeaconConfig().FarFutureEpoch
	require.NoError(t, beaconState.SetValidators([]*ethpb.Validator{
		{
			PublicKey:             sk.PublicKey().Marshal(),
			WithdrawalCredentials: withdrawalCredentials,
			ExitEpoch:             ffe,
			EffectiveBalance:      params.BeaconConfig().MinActivationBalance,
		},
	}))

	require.NoError(t, beaconState.SetPendingDeposits([]*ethpb.PendingDeposit{
		stateTesting.GeneratePendingDeposit(t, sk, uint64(amountAvailForProcessing)/10, bytesutil.ToBytes32(withdrawalCredentials), genesisBlock.Block.Slot),
	}))

	bodyRoot, err := genesisBlock.Block.HashTreeRoot()
	require.NoError(t, err)
	err = beaconState.SetLatestBlockHeader(&ethpb.BeaconBlockHeader{
		Slot:       genesisBlock.Block.Slot,
		ParentRoot: genesisBlock.Block.ParentRoot,
		StateRoot:  params.BeaconConfig().ZeroHash[:],
		BodyRoot:   bodyRoot[:],
	})
	require.NoError(t, err)

	assert.Equal(t, version.Electra, beaconState.Version())
	require.Equal(t, params.BeaconConfig().MinActivationBalance, beaconState.Balances()[0])
	service := New(testDB.SetupDB(t), doublylinkedtree.New())
	targetSlot := (params.BeaconConfig().SlotsPerEpoch * 2) - 1
	newState, err := service.replayBlocks(t.Context(), beaconState, []interfaces.ReadOnlySignedBeaconBlock{}, targetSlot)
	require.NoError(t, err)

	require.Equal(t, version.Electra, newState.Version())
	res, err := newState.DepositBalanceToConsume()
	require.NoError(t, err)
	require.Equal(t, primitives.Gwei(0), res)

	remaining, err := newState.PendingDeposits()
	require.NoError(t, err)
	require.Equal(t, 0, len(remaining))

	require.Equal(t, params.BeaconConfig().MinActivationBalance+(uint64(amountAvailForProcessing)/10), newState.Balances()[0])
}

func TestLoadBlocks_FirstBranch(t *testing.T) {
	beaconDB := testDB.SetupDB(t)
	ctx := t.Context()
	s := &State{
		beaconDB: beaconDB,
	}

	roots, savedBlocks, err := tree1(t, beaconDB, bytesutil.PadTo([]byte{'A'}, 32))
	require.NoError(t, err)

	filteredBlocks, err := s.loadBlocks(ctx, 0, 8, roots[len(roots)-1])
	require.NoError(t, err)

	wanted := []*ethpb.SignedBeaconBlock{
		savedBlocks[0],
		savedBlocks[1],
		savedBlocks[2],
		savedBlocks[4],
		savedBlocks[6],
		savedBlocks[8],
	}
	require.Equal(t, len(wanted), len(filteredBlocks))

	for i, block := range wanted {
		filteredBlocksPb, err := filteredBlocks[i].Proto()
		require.NoError(t, err)
		if !proto.Equal(block, filteredBlocksPb) {
			t.Error("Did not get wanted blocks")
		}
	}
}

func TestLoadBlocks_SecondBranch(t *testing.T) {
	beaconDB := testDB.SetupDB(t)
	ctx := t.Context()
	s := &State{
		beaconDB: beaconDB,
	}

	roots, savedBlocks, err := tree1(t, beaconDB, bytesutil.PadTo([]byte{'A'}, 32))
	require.NoError(t, err)

	filteredBlocks, err := s.loadBlocks(ctx, 0, 5, roots[5])
	require.NoError(t, err)

	wanted := []*ethpb.SignedBeaconBlock{
		savedBlocks[0],
		savedBlocks[1],
		savedBlocks[3],
		savedBlocks[5],
	}

	for i, block := range wanted {
		filteredBlocksPb, err := filteredBlocks[i].Proto()
		require.NoError(t, err)
		if !proto.Equal(block, filteredBlocksPb) {
			t.Error("Did not get wanted blocks")
		}
	}
}

func TestLoadBlocks_ThirdBranch(t *testing.T) {
	beaconDB := testDB.SetupDB(t)
	ctx := t.Context()
	s := &State{
		beaconDB: beaconDB,
	}

	roots, savedBlocks, err := tree1(t, beaconDB, bytesutil.PadTo([]byte{'A'}, 32))
	require.NoError(t, err)

	filteredBlocks, err := s.loadBlocks(ctx, 0, 7, roots[7])
	require.NoError(t, err)

	wanted := []*ethpb.SignedBeaconBlock{
		savedBlocks[0],
		savedBlocks[1],
		savedBlocks[2],
		savedBlocks[4],
		savedBlocks[6],
		savedBlocks[7],
	}

	require.Equal(t, len(wanted), len(filteredBlocks))

	for i, block := range wanted {
		filteredBlocksPb, err := filteredBlocks[i].Proto()
		require.NoError(t, err)
		if !proto.Equal(block, filteredBlocksPb) {
			t.Error("Did not get wanted blocks")
		}
	}
}

func TestLoadBlocks_SameSlots(t *testing.T) {
	beaconDB := testDB.SetupDB(t)
	ctx := t.Context()
	s := &State{
		beaconDB: beaconDB,
	}

	roots, savedBlocks, err := tree2(t, beaconDB, bytesutil.PadTo([]byte{'A'}, 32))
	require.NoError(t, err)

	filteredBlocks, err := s.loadBlocks(ctx, 0, 3, roots[6])
	require.NoError(t, err)

	wanted := []*ethpb.SignedBeaconBlock{
		savedBlocks[0],
		savedBlocks[1],
		savedBlocks[5],
		savedBlocks[6],
	}
	require.Equal(t, len(wanted), len(filteredBlocks))

	for i, block := range wanted {
		filteredBlocksPb, err := filteredBlocks[i].Proto()
		require.NoError(t, err)
		if !proto.Equal(block, filteredBlocksPb) {
			t.Error("Did not get wanted blocks")
		}
	}
}

func TestLoadBlocks_SameEndSlots(t *testing.T) {
	beaconDB := testDB.SetupDB(t)
	ctx := t.Context()
	s := &State{
		beaconDB: beaconDB,
	}

	roots, savedBlocks, err := tree3(t, beaconDB, bytesutil.PadTo([]byte{'A'}, 32))
	require.NoError(t, err)

	filteredBlocks, err := s.loadBlocks(ctx, 0, 2, roots[2])
	require.NoError(t, err)

	wanted := []*ethpb.SignedBeaconBlock{
		savedBlocks[0],
		savedBlocks[1],
		savedBlocks[2],
	}
	require.Equal(t, len(wanted), len(filteredBlocks))

	for i, block := range wanted {
		filteredBlocksPb, err := filteredBlocks[i].Proto()
		require.NoError(t, err)
		if !proto.Equal(block, filteredBlocksPb) {
			t.Error("Did not get wanted blocks")
		}
	}
}

func TestLoadBlocks_SameEndSlotsWith2blocks(t *testing.T) {
	beaconDB := testDB.SetupDB(t)
	ctx := t.Context()
	s := &State{
		beaconDB: beaconDB,
	}

	roots, savedBlocks, err := tree4(t, beaconDB, bytesutil.PadTo([]byte{'A'}, 32))
	require.NoError(t, err)

	filteredBlocks, err := s.loadBlocks(ctx, 0, 2, roots[1])
	require.NoError(t, err)

	wanted := []*ethpb.SignedBeaconBlock{
		savedBlocks[0],
		savedBlocks[1],
	}
	require.Equal(t, len(wanted), len(filteredBlocks))

	for i, block := range wanted {
		filteredBlocksPb, err := filteredBlocks[i].Proto()
		require.NoError(t, err)
		if !proto.Equal(block, filteredBlocksPb) {
			t.Error("Did not get wanted blocks")
		}
	}
}

// tree1 constructs the following tree:
// B0 - B1 - - B3 -- B5
//
//	\- B2 -- B4 -- B6 ----- B8
//	                 \- B7
func tree1(t *testing.T, beaconDB db.Database, genesisRoot []byte) ([][32]byte, []*ethpb.SignedBeaconBlock, error) {
	b0 := util.NewBeaconBlock()
	b0.Block.Slot = 0
	b0.Block.ParentRoot = genesisRoot
	r0, err := b0.Block.HashTreeRoot()
	if err != nil {
		return nil, nil, err
	}
	b1 := util.NewBeaconBlock()
	b1.Block.Slot = 1
	b1.Block.ParentRoot = r0[:]
	r1, err := b1.Block.HashTreeRoot()
	if err != nil {
		return nil, nil, err
	}
	b2 := util.NewBeaconBlock()
	b2.Block.Slot = 2
	b2.Block.ParentRoot = r1[:]
	r2, err := b2.Block.HashTreeRoot()
	if err != nil {
		return nil, nil, err
	}
	b3 := util.NewBeaconBlock()
	b3.Block.Slot = 3
	b3.Block.ParentRoot = r1[:]
	r3, err := b3.Block.HashTreeRoot()
	if err != nil {
		return nil, nil, err
	}
	b4 := util.NewBeaconBlock()
	b4.Block.Slot = 4
	b4.Block.ParentRoot = r2[:]
	r4, err := b4.Block.HashTreeRoot()
	if err != nil {
		return nil, nil, err
	}
	b5 := util.NewBeaconBlock()
	b5.Block.Slot = 5
	b5.Block.ParentRoot = r3[:]
	r5, err := b5.Block.HashTreeRoot()
	if err != nil {
		return nil, nil, err
	}
	b6 := util.NewBeaconBlock()
	b6.Block.Slot = 6
	b6.Block.ParentRoot = r4[:]
	r6, err := b6.Block.HashTreeRoot()
	if err != nil {
		return nil, nil, err
	}
	b7 := util.NewBeaconBlock()
	b7.Block.Slot = 7
	b7.Block.ParentRoot = r6[:]
	r7, err := b7.Block.HashTreeRoot()
	if err != nil {
		return nil, nil, err
	}
	b8 := util.NewBeaconBlock()
	b8.Block.Slot = 8
	b8.Block.ParentRoot = r6[:]
	r8, err := b8.Block.HashTreeRoot()
	if err != nil {
		return nil, nil, err
	}
	st, err := util.NewBeaconState()
	require.NoError(t, err)

	returnedBlocks := make([]*ethpb.SignedBeaconBlock, 0)
	for _, b := range []*ethpb.SignedBeaconBlock{b0, b1, b2, b3, b4, b5, b6, b7, b8} {
		beaconBlock := util.NewBeaconBlock()
		beaconBlock.Block.Slot = b.Block.Slot
		beaconBlock.Block.ParentRoot = bytesutil.PadTo(b.Block.ParentRoot, 32)
		wsb, err := consensusblocks.NewSignedBeaconBlock(beaconBlock)
		require.NoError(t, err)
		if err := beaconDB.SaveBlock(t.Context(), wsb); err != nil {
			return nil, nil, err
		}
		if err := beaconDB.SaveState(t.Context(), st.Copy(), bytesutil.ToBytes32(beaconBlock.Block.ParentRoot)); err != nil {
			return nil, nil, err
		}
		returnedBlocks = append(returnedBlocks, beaconBlock)
	}
	return [][32]byte{r0, r1, r2, r3, r4, r5, r6, r7, r8}, returnedBlocks, nil
}

// tree2 constructs the following tree:
// B0 - B1
//
//	\- B2
//	\- B2
//	\- B2
//	\- B2 -- B3
func tree2(t *testing.T, beaconDB db.Database, genesisRoot []byte) ([][32]byte, []*ethpb.SignedBeaconBlock, error) {
	b0 := util.NewBeaconBlock()
	b0.Block.Slot = 0
	b0.Block.ParentRoot = genesisRoot
	r0, err := b0.Block.HashTreeRoot()
	if err != nil {
		return nil, nil, err
	}
	b1 := util.NewBeaconBlock()
	b1.Block.Slot = 1
	b1.Block.ParentRoot = r0[:]
	r1, err := b1.Block.HashTreeRoot()
	if err != nil {
		return nil, nil, err
	}
	b21 := util.NewBeaconBlock()
	b21.Block.Slot = 2
	b21.Block.ParentRoot = r1[:]
	b21.Block.StateRoot = bytesutil.PadTo([]byte{'A'}, 32)
	r21, err := b21.Block.HashTreeRoot()
	if err != nil {
		return nil, nil, err
	}
	b22 := util.NewBeaconBlock()
	b22.Block.Slot = 2
	b22.Block.ParentRoot = r1[:]
	b22.Block.StateRoot = bytesutil.PadTo([]byte{'B'}, 32)
	r22, err := b22.Block.HashTreeRoot()
	if err != nil {
		return nil, nil, err
	}
	b23 := util.NewBeaconBlock()
	b23.Block.Slot = 2
	b23.Block.ParentRoot = r1[:]
	b23.Block.StateRoot = bytesutil.PadTo([]byte{'C'}, 32)
	r23, err := b23.Block.HashTreeRoot()
	if err != nil {
		return nil, nil, err
	}
	b24 := util.NewBeaconBlock()
	b24.Block.Slot = 2
	b24.Block.ParentRoot = r1[:]
	b24.Block.StateRoot = bytesutil.PadTo([]byte{'D'}, 32)
	r24, err := b24.Block.HashTreeRoot()
	if err != nil {
		return nil, nil, err
	}
	b3 := util.NewBeaconBlock()
	b3.Block.Slot = 3
	b3.Block.ParentRoot = r24[:]
	r3, err := b3.Block.HashTreeRoot()
	if err != nil {
		return nil, nil, err
	}
	st, err := util.NewBeaconState()
	require.NoError(t, err)

	returnedBlocks := make([]*ethpb.SignedBeaconBlock, 0)
	for _, b := range []*ethpb.SignedBeaconBlock{b0, b1, b21, b22, b23, b24, b3} {
		beaconBlock := util.NewBeaconBlock()
		beaconBlock.Block.Slot = b.Block.Slot
		beaconBlock.Block.ParentRoot = bytesutil.PadTo(b.Block.ParentRoot, 32)
		beaconBlock.Block.StateRoot = bytesutil.PadTo(b.Block.StateRoot, 32)
		wsb, err := consensusblocks.NewSignedBeaconBlock(beaconBlock)
		require.NoError(t, err)
		if err := beaconDB.SaveBlock(t.Context(), wsb); err != nil {
			return nil, nil, err
		}
		if err := beaconDB.SaveState(t.Context(), st.Copy(), bytesutil.ToBytes32(beaconBlock.Block.ParentRoot)); err != nil {
			return nil, nil, err
		}
		returnedBlocks = append(returnedBlocks, beaconBlock)
	}
	return [][32]byte{r0, r1, r21, r22, r23, r24, r3}, returnedBlocks, nil
}

// tree3 constructs the following tree:
// B0 - B1
//
//	\- B2
//	\- B2
//	\- B2
//	\- B2
func tree3(t *testing.T, beaconDB db.Database, genesisRoot []byte) ([][32]byte, []*ethpb.SignedBeaconBlock, error) {
	b0 := util.NewBeaconBlock()
	b0.Block.Slot = 0
	b0.Block.ParentRoot = genesisRoot
	r0, err := b0.Block.HashTreeRoot()
	if err != nil {
		return nil, nil, err
	}
	b1 := util.NewBeaconBlock()
	b1.Block.Slot = 1
	b1.Block.ParentRoot = r0[:]
	r1, err := b1.Block.HashTreeRoot()
	if err != nil {
		return nil, nil, err
	}
	b21 := util.NewBeaconBlock()
	b21.Block.Slot = 2
	b21.Block.ParentRoot = r1[:]
	b21.Block.StateRoot = bytesutil.PadTo([]byte{'A'}, 32)
	r21, err := b21.Block.HashTreeRoot()
	if err != nil {
		return nil, nil, err
	}
	b22 := util.NewBeaconBlock()
	b22.Block.Slot = 2
	b22.Block.ParentRoot = r1[:]
	b22.Block.StateRoot = bytesutil.PadTo([]byte{'B'}, 32)
	r22, err := b22.Block.HashTreeRoot()
	if err != nil {
		return nil, nil, err
	}
	b23 := util.NewBeaconBlock()
	b23.Block.Slot = 2
	b23.Block.ParentRoot = r1[:]
	b23.Block.StateRoot = bytesutil.PadTo([]byte{'C'}, 32)
	r23, err := b23.Block.HashTreeRoot()
	if err != nil {
		return nil, nil, err
	}
	b24 := util.NewBeaconBlock()
	b24.Block.Slot = 2
	b24.Block.ParentRoot = r1[:]
	b24.Block.StateRoot = bytesutil.PadTo([]byte{'D'}, 32)
	r24, err := b24.Block.HashTreeRoot()
	if err != nil {
		return nil, nil, err
	}
	st, err := util.NewBeaconState()
	require.NoError(t, err)

	returnedBlocks := make([]*ethpb.SignedBeaconBlock, 0)
	for _, b := range []*ethpb.SignedBeaconBlock{b0, b1, b21, b22, b23, b24} {
		beaconBlock := util.NewBeaconBlock()
		beaconBlock.Block.Slot = b.Block.Slot
		beaconBlock.Block.ParentRoot = bytesutil.PadTo(b.Block.ParentRoot, 32)
		beaconBlock.Block.StateRoot = bytesutil.PadTo(b.Block.StateRoot, 32)
		wsb, err := consensusblocks.NewSignedBeaconBlock(beaconBlock)
		require.NoError(t, err)
		if err := beaconDB.SaveBlock(t.Context(), wsb); err != nil {
			return nil, nil, err
		}
		if err := beaconDB.SaveState(t.Context(), st.Copy(), bytesutil.ToBytes32(beaconBlock.Block.ParentRoot)); err != nil {
			return nil, nil, err
		}
		returnedBlocks = append(returnedBlocks, beaconBlock)
	}

	return [][32]byte{r0, r1, r21, r22, r23, r24}, returnedBlocks, nil
}

// tree4 constructs the following tree:
// B0
//
//	\- B2
//	\- B2
//	\- B2
//	\- B2
func tree4(t *testing.T, beaconDB db.Database, genesisRoot []byte) ([][32]byte, []*ethpb.SignedBeaconBlock, error) {
	b0 := util.NewBeaconBlock()
	b0.Block.Slot = 0
	b0.Block.ParentRoot = genesisRoot
	r0, err := b0.Block.HashTreeRoot()
	if err != nil {
		return nil, nil, err
	}
	b21 := util.NewBeaconBlock()
	b21.Block.Slot = 2
	b21.Block.ParentRoot = r0[:]
	b21.Block.StateRoot = bytesutil.PadTo([]byte{'A'}, 32)
	r21, err := b21.Block.HashTreeRoot()
	if err != nil {
		return nil, nil, err
	}
	b22 := util.NewBeaconBlock()
	b22.Block.Slot = 2
	b22.Block.ParentRoot = r0[:]
	b22.Block.StateRoot = bytesutil.PadTo([]byte{'B'}, 32)
	r22, err := b22.Block.HashTreeRoot()
	if err != nil {
		return nil, nil, err
	}
	b23 := util.NewBeaconBlock()
	b23.Block.Slot = 2
	b23.Block.ParentRoot = r0[:]
	b23.Block.StateRoot = bytesutil.PadTo([]byte{'C'}, 32)
	r23, err := b23.Block.HashTreeRoot()
	if err != nil {
		return nil, nil, err
	}
	b24 := util.NewBeaconBlock()
	b24.Block.Slot = 2
	b24.Block.ParentRoot = r0[:]
	b24.Block.StateRoot = bytesutil.PadTo([]byte{'D'}, 32)
	r24, err := b24.Block.HashTreeRoot()
	if err != nil {
		return nil, nil, err
	}
	st, err := util.NewBeaconState()
	require.NoError(t, err)

	returnedBlocks := make([]*ethpb.SignedBeaconBlock, 0)
	for _, b := range []*ethpb.SignedBeaconBlock{b0, b21, b22, b23, b24} {
		beaconBlock := util.NewBeaconBlock()
		beaconBlock.Block.Slot = b.Block.Slot
		beaconBlock.Block.ParentRoot = bytesutil.PadTo(b.Block.ParentRoot, 32)
		beaconBlock.Block.StateRoot = bytesutil.PadTo(b.Block.StateRoot, 32)
		wsb, err := consensusblocks.NewSignedBeaconBlock(beaconBlock)
		require.NoError(t, err)
		if err := beaconDB.SaveBlock(t.Context(), wsb); err != nil {
			return nil, nil, err
		}
		if err := beaconDB.SaveState(t.Context(), st.Copy(), bytesutil.ToBytes32(beaconBlock.Block.ParentRoot)); err != nil {
			return nil, nil, err
		}
		returnedBlocks = append(returnedBlocks, beaconBlock)
	}

	return [][32]byte{r0, r21, r22, r23, r24}, returnedBlocks, nil
}

func TestLoadFinalizedBlocks(t *testing.T) {
	beaconDB := testDB.SetupDB(t)
	ctx := t.Context()
	s := &State{
		beaconDB: beaconDB,
	}
	gBlock := util.NewBeaconBlock()
	gRoot, err := gBlock.Block.HashTreeRoot()
	require.NoError(t, err)
	util.SaveBlock(t, ctx, beaconDB, gBlock)
	require.NoError(t, beaconDB.SaveGenesisBlockRoot(ctx, [32]byte{}))
	roots, _, err := tree1(t, beaconDB, gRoot[:])
	require.NoError(t, err)

	filteredBlocks, err := s.loadFinalizedBlocks(ctx, 0, 8)
	require.NoError(t, err)
	require.Equal(t, 0, len(filteredBlocks))
	require.NoError(t, beaconDB.SaveStateSummary(ctx, &ethpb.StateSummary{Root: roots[8][:]}))

	require.NoError(t, s.beaconDB.SaveFinalizedCheckpoint(ctx, &ethpb.Checkpoint{Root: roots[8][:]}))
	filteredBlocks, err = s.loadFinalizedBlocks(ctx, 0, 8)
	require.NoError(t, err)
	require.Equal(t, 10, len(filteredBlocks))
}
