package bridgesync

import (
	"context"
	"errors"
	"fmt"
	"math/big"
	"path"
	"testing"
	"time"

	mocksbridgesync "github.com/agglayer/aggkit/bridgesync/mocks"
	cfgtypes "github.com/agglayer/aggkit/config/types"
	"github.com/agglayer/aggkit/log"
	"github.com/agglayer/aggkit/reorgdetector"
	"github.com/agglayer/aggkit/sync"
	aggkittypes "github.com/agglayer/aggkit/types"
	mocksethclient "github.com/agglayer/aggkit/types/mocks"
	"github.com/ethereum/go-ethereum/common"
	"github.com/stretchr/testify/mock"
	"github.com/stretchr/testify/require"
)

func TestNewLx(t *testing.T) {
	const (
		syncBlockChunkSize         = uint64(100)
		initialBlock               = uint64(0)
		waitForNewBlocksPeriod     = time.Second * 10
		retryAfterErrorPeriod      = time.Second * 5
		maxRetryAttemptsAfterError = 3
		originNetwork              = uint32(1)
	)

	var (
		blockFinalityType = aggkittypes.SafeBlock
		ctx               = context.Background()
		dbPath            = path.Join(t.TempDir(), "TestNewLx.sqlite")
		bridge            = common.HexToAddress("0x1234567890abcdef1234567890abcdef12345678")
	)

	mockEthClient := mocksethclient.NewEthClienter(t)
	mockEthClient.EXPECT().CallContract(mock.Anything, mock.Anything, mock.Anything).Return(
		common.FromHex("0x000000000000000000000000000000000000000000000000000000000000002a"), nil).Times(2)
	mockEthClient.EXPECT().
		CallContract(
			mock.Anything,
			mock.Anything,
			mock.Anything,
		).
		Return(common.LeftPadBytes(common.HexToAddress("0x3c351e10").Bytes(), 32), nil).
		Maybe()
	mockReorgDetector := mocksbridgesync.NewReorgDetector(t)

	mockReorgDetector.EXPECT().Subscribe(mock.Anything).Return(nil, nil)
	mockReorgDetector.EXPECT().GetFinalizedBlockType().Return(blockFinalityType)
	mockReorgDetector.EXPECT().String().Return("mockReorgDetector")

	dbQueryTimeout := 30 * time.Second

	bridgeSyncL1Cfg := Config{
		DBPath:                             dbPath,
		BridgeAddr:                         bridge,
		BlockFinality:                      aggkittypes.LatestBlock,
		SyncBlockChunkSize:                 syncBlockChunkSize,
		InitialBlockNum:                    initialBlock,
		WaitForNewBlocksPeriod:             cfgtypes.NewDuration(waitForNewBlocksPeriod),
		RetryAfterErrorPeriod:              cfgtypes.NewDuration(retryAfterErrorPeriod),
		MaxRetryAttemptsAfterError:         maxRetryAttemptsAfterError,
		RequireStorageContentCompatibility: true,
		DBQueryTimeout:                     cfgtypes.NewDuration(dbQueryTimeout),
	}
	l1BridgeSync, err := NewL1(
		ctx,
		bridgeSyncL1Cfg,
		blockFinalityType,
		mockEthClient,
		originNetwork,
		false,
	)

	require.NoError(t, err)
	require.NotNil(t, l1BridgeSync)
	require.Equal(t, originNetwork, l1BridgeSync.OriginNetwork())

	bridgeSyncL2Cfg := Config{
		DBPath:                             dbPath,
		BridgeAddr:                         bridge,
		BlockFinality:                      aggkittypes.SafeBlock,
		SyncBlockChunkSize:                 syncBlockChunkSize,
		InitialBlockNum:                    initialBlock,
		WaitForNewBlocksPeriod:             cfgtypes.NewDuration(waitForNewBlocksPeriod),
		RetryAfterErrorPeriod:              cfgtypes.NewDuration(retryAfterErrorPeriod),
		MaxRetryAttemptsAfterError:         maxRetryAttemptsAfterError,
		RequireStorageContentCompatibility: true,
		DBQueryTimeout:                     cfgtypes.NewDuration(dbQueryTimeout),
	}
	l2BridgdeSync, err := NewL2(
		ctx,
		bridgeSyncL2Cfg,
		mockReorgDetector,
		mockEthClient,
		originNetwork,
		false,
	)

	require.NoError(t, err)
	require.NotNil(t, l1BridgeSync)
	require.Equal(t, originNetwork, l2BridgdeSync.OriginNetwork())

	// Fails the sanity check of the contract address
	mockEthClient = mocksethclient.NewEthClienter(t)
	mockEthClient.EXPECT().CallContract(mock.Anything, mock.Anything, mock.Anything).Return(nil, nil).Once()
	mockEthClient.EXPECT().CodeAt(mock.Anything, mock.Anything, mock.Anything).Return(nil, nil).Once()

	l2BridgeSyncer, err := NewL2(
		ctx,
		bridgeSyncL2Cfg,
		mockReorgDetector,
		mockEthClient,
		originNetwork,
		false,
	)
	require.Error(t, err)
	require.Nil(t, l2BridgeSyncer)
}

func TestGetLastProcessedBlock(t *testing.T) {
	s := BridgeSync{processor: &processor{
		halted: true,
		log:    log.WithFields("module", "L2BridgeSyncer"),
	}}
	_, err := s.GetLastProcessedBlock(context.Background())
	require.ErrorIs(t, err, sync.ErrInconsistentState)
}

func TestGetLatestNetworkBlock(t *testing.T) {
	ctx := context.Background()
	mockEthClient := mocksethclient.NewEthClienter(t)

	t.Run("successful block number retrieval", func(t *testing.T) {
		expectedBlockNumber := uint64(12345678)
		mockEthClient.EXPECT().BlockNumber(mock.Anything).Return(expectedBlockNumber, nil).Once()

		s := BridgeSync{
			processor: &processor{
				halted: false,
				log:    log.WithFields("module", "L2BridgeSyncer"),
			},
			ethClient: mockEthClient,
		}

		blockNumber, err := s.GetLatestNetworkBlock(ctx)
		require.NoError(t, err)
		require.Equal(t, expectedBlockNumber, blockNumber)
	})

	t.Run("error from eth client", func(t *testing.T) {
		expectedError := errors.New("network error")
		mockEthClient.EXPECT().BlockNumber(mock.Anything).Return(uint64(0), expectedError).Once()

		s := BridgeSync{
			processor: &processor{
				halted: false,
				log:    log.WithFields("module", "L2BridgeSyncer"),
			},
			ethClient: mockEthClient,
		}

		blockNumber, err := s.GetLatestNetworkBlock(ctx)
		require.Error(t, err)
		require.Contains(t, err.Error(), "failed to get latest block number")
		require.Equal(t, uint64(0), blockNumber)
	})

	t.Run("processor halted", func(t *testing.T) {
		s := BridgeSync{processor: &processor{
			halted: true,
			log:    log.WithFields("module", "L2BridgeSyncer"),
		}}

		blockNumber, err := s.GetLatestNetworkBlock(ctx)
		require.ErrorIs(t, err, sync.ErrInconsistentState)
		require.Equal(t, uint64(0), blockNumber)
	})
}

func TestIsActive(t *testing.T) {
	ctx := context.Background()

	t.Run("active syncer", func(t *testing.T) {
		s := BridgeSync{processor: &processor{
			halted: false,
			log:    log.WithFields("module", "L2BridgeSyncer"),
		}}

		isActive := s.IsActive(ctx)
		require.True(t, isActive)
	})

	t.Run("inactive syncer", func(t *testing.T) {
		s := BridgeSync{processor: &processor{
			halted: true,
			log:    log.WithFields("module", "L2BridgeSyncer"),
		}}

		isActive := s.IsActive(ctx)
		require.False(t, isActive)
	})
}

func TestGetExitRootByHash(t *testing.T) {
	s := BridgeSync{processor: &processor{halted: true}}
	_, err := s.GetExitRootByHash(context.Background(), common.Hash{})
	require.ErrorIs(t, err, sync.ErrInconsistentState)
}

func TestGetBridges(t *testing.T) {
	s := BridgeSync{processor: &processor{halted: true}}
	_, err := s.GetBridges(context.Background(), 0, 0)
	require.ErrorIs(t, err, sync.ErrInconsistentState)
}

func TestGetProof(t *testing.T) {
	s := BridgeSync{processor: &processor{halted: true}}
	_, err := s.GetProof(context.Background(), 0, common.Hash{})
	require.ErrorIs(t, err, sync.ErrInconsistentState)
}

func TestGetBlockByLER(t *testing.T) {
	s := BridgeSync{processor: &processor{halted: true}}
	_, err := s.GetBlockByLER(context.Background(), common.Hash{})
	require.ErrorIs(t, err, sync.ErrInconsistentState)
}

func TestGetRootByLER(t *testing.T) {
	s := BridgeSync{processor: &processor{halted: true}}
	_, err := s.GetRootByLER(context.Background(), common.Hash{})
	require.ErrorIs(t, err, sync.ErrInconsistentState)
}

func TestGetExitRootByIndex(t *testing.T) {
	s := BridgeSync{processor: &processor{halted: true}}
	_, err := s.GetExitRootByIndex(context.Background(), 0)
	require.ErrorIs(t, err, sync.ErrInconsistentState)
}

func TestGetClaims(t *testing.T) {
	s := BridgeSync{processor: &processor{halted: true}}
	_, err := s.GetClaims(context.Background(), 0, 0)
	require.ErrorIs(t, err, sync.ErrInconsistentState)
}

func TestGetClaimsByGlobalIndex(t *testing.T) {
	s := BridgeSync{processor: &processor{halted: true}}
	_, err := s.GetClaimsByGlobalIndex(context.Background(), new(big.Int))
	require.ErrorIs(t, err, sync.ErrInconsistentState)
}

func TestBridgeSync_GetTokenMappings(t *testing.T) {
	const (
		syncBlockChunkSize         = uint64(100)
		initialBlock               = uint64(0)
		waitForNewBlocksPeriod     = time.Second * 10
		retryAfterErrorPeriod      = time.Second * 5
		maxRetryAttemptsAfterError = 3
		originNetwork              = uint32(1)
		tokenMappingsCount         = 20
		blockNum                   = uint64(1)
	)

	var (
		blockFinalityType = aggkittypes.SafeBlock
		ctx               = context.Background()
		dbPath            = path.Join(t.TempDir(), "TestGetTokenMappings.sqlite")
		bridge            = common.HexToAddress("0x1234567890abcdef1234567890abcdef12345678")
	)

	mockEthClient := mocksethclient.NewEthClienter(t)
	mockEthClient.EXPECT().CallContract(mock.Anything, mock.Anything, mock.Anything).Return(
		common.FromHex("0x000000000000000000000000000000000000000000000000000000000000002a"), nil).Once()
	mockEthClient.EXPECT().
		CallContract(
			mock.Anything,
			mock.Anything,
			mock.Anything,
		).
		Return(common.LeftPadBytes(common.HexToAddress("0x3c351e10").Bytes(), 32), nil).
		Maybe()
	mockReorgDetector := mocksbridgesync.NewReorgDetector(t)

	mockReorgDetector.EXPECT().Subscribe(mock.Anything).Return(nil, nil)
	mockReorgDetector.EXPECT().GetFinalizedBlockType().Return(blockFinalityType)
	mockReorgDetector.EXPECT().String().Return("mockReorgDetector")

	dbQueryTimeout := 30 * time.Second

	bridgeSyncCfg := Config{
		DBPath:                             dbPath,
		BridgeAddr:                         bridge,
		BlockFinality:                      aggkittypes.LatestBlock,
		SyncBlockChunkSize:                 syncBlockChunkSize,
		InitialBlockNum:                    initialBlock,
		WaitForNewBlocksPeriod:             cfgtypes.NewDuration(waitForNewBlocksPeriod),
		RetryAfterErrorPeriod:              cfgtypes.NewDuration(retryAfterErrorPeriod),
		MaxRetryAttemptsAfterError:         maxRetryAttemptsAfterError,
		RequireStorageContentCompatibility: false,
		DBQueryTimeout:                     cfgtypes.NewDuration(dbQueryTimeout),
	}
	s, err := NewL2(
		ctx,
		bridgeSyncCfg,
		mockReorgDetector,
		mockEthClient,
		originNetwork,
		false,
	)
	require.NoError(t, err)

	allTokenMappings := make([]*TokenMapping, 0, tokenMappingsCount)
	genericEvts := make([]interface{}, 0, tokenMappingsCount)

	for i := tokenMappingsCount - 1; i >= 0; i-- {
		tokenMappingEvt := &TokenMapping{
			BlockNum:            blockNum,
			BlockPos:            uint64(i),
			OriginNetwork:       uint32(i),
			OriginTokenAddress:  common.HexToAddress(fmt.Sprintf("%d", i)),
			WrappedTokenAddress: common.HexToAddress(fmt.Sprintf("%d", i+1)),
		}

		allTokenMappings = append(allTokenMappings, tokenMappingEvt)
		genericEvts = append(genericEvts, Event{TokenMapping: tokenMappingEvt})
	}

	block := sync.Block{
		Num:    blockNum,
		Events: genericEvts,
	}

	err = s.processor.ProcessBlock(context.Background(), block)
	require.NoError(t, err)

	t.Run("retrieve all mappings", func(t *testing.T) {
		tokenMappings, totalTokenMappings, err := s.GetTokenMappings(context.Background(), 1, tokenMappingsCount, "")
		require.NoError(t, err)
		require.Equal(t, tokenMappingsCount, totalTokenMappings)
		require.Equal(t, allTokenMappings, tokenMappings)
	})

	t.Run("retrieve paginated mappings", func(t *testing.T) {
		pageSize := uint32(5)

		for page := uint32(1); page <= 4; page++ {
			tokenMappings, totalTokenMappings, err := s.GetTokenMappings(context.Background(), page, pageSize, "")
			require.NoError(t, err)
			require.Equal(t, tokenMappingsCount, totalTokenMappings)

			startIndex := (page - 1) * pageSize
			endIndex := startIndex + pageSize
			require.Equal(t, allTokenMappings[startIndex:endIndex], tokenMappings)
		}
	})

	t.Run("retrieve non-existent page", func(t *testing.T) {
		pageSize := uint32(5)
		pageNum := uint32(5)

		tokenMappings, totalTokenMappings, err := s.GetTokenMappings(context.Background(), pageNum, pageSize, "")
		require.ErrorContains(t, err, "invalid page number for given page size and total number of token mappings")
		require.Equal(t, 0, totalTokenMappings)
		require.Nil(t, tokenMappings)
	})

	t.Run("provide invalid page number", func(t *testing.T) {
		pageSize := uint32(0)
		pageNum := uint32(0)

		_, _, err := s.GetTokenMappings(context.Background(), pageNum, pageSize, "")
		require.ErrorIs(t, err, ErrInvalidPageNumber)
	})

	t.Run("provide invalid page size", func(t *testing.T) {
		pageSize := uint32(0)
		pageNum := uint32(4)

		_, _, err := s.GetTokenMappings(context.Background(), pageNum, pageSize, "")
		require.ErrorIs(t, err, ErrInvalidPageSize)
	})

	t.Run("filter by valid origin token address", func(t *testing.T) {
		s.processor.halted = false

		targetOriginAddress := common.HexToAddress("5").Hex()
		tokenMappings, totalTokenMappings, err := s.GetTokenMappings(context.Background(), 1, tokenMappingsCount, targetOriginAddress)
		require.NoError(t, err)

		require.Equal(t, 1, totalTokenMappings)
		require.Len(t, tokenMappings, 1)
		require.Equal(t, common.HexToAddress("5"), tokenMappings[0].OriginTokenAddress)
		require.Equal(t, uint32(5), tokenMappings[0].OriginNetwork)
		require.Equal(t, common.HexToAddress("6"), tokenMappings[0].WrappedTokenAddress)
	})

	t.Run("filter by non-existent origin token address", func(t *testing.T) {
		nonExistentAddress := common.HexToAddress("999").Hex()
		tokenMappings, totalTokenMappings, err := s.GetTokenMappings(context.Background(), 1, tokenMappingsCount, nonExistentAddress)
		require.NoError(t, err)

		require.Equal(t, 0, totalTokenMappings)
		require.Empty(t, tokenMappings)
	})

	t.Run("inconsistent state", func(t *testing.T) {
		s.processor.halted = true
		_, _, err := s.GetTokenMappings(context.Background(), 0, 0, "")
		require.ErrorIs(t, err, sync.ErrInconsistentState)
	})
}

func TestBridgeSync_GetLegacyTokenMigrations(t *testing.T) {
	const (
		syncBlockChunkSize         = uint64(100)
		initialBlock               = uint64(0)
		waitForNewBlocksPeriod     = time.Second * 10
		retryAfterErrorPeriod      = time.Second * 5
		maxRetryAttemptsAfterError = 3
		originNetwork              = uint32(1)
		tokenMigrationsCount       = 20
		blockNum                   = uint64(1)
	)

	var (
		blockFinalityType = aggkittypes.SafeBlock
		ctx               = context.Background()
		dbPath            = path.Join(t.TempDir(), "TestGetTokenMigrations.sqlite")
		bridge            = common.HexToAddress("0x123456")
	)

	mockEthClient := mocksethclient.NewEthClienter(t)
	mockEthClient.EXPECT().CallContract(mock.Anything, mock.Anything, mock.Anything).Return(
		common.FromHex("0x000000000000000000000000000000000000000000000000000000000000002a"), nil).Once()
	mockEthClient.EXPECT().
		CallContract(
			mock.Anything,
			mock.Anything,
			mock.Anything,
		).
		Return(common.LeftPadBytes(common.HexToAddress("0x3c351e10").Bytes(), 32), nil).
		Maybe()
	mockReorgDetector := mocksbridgesync.NewReorgDetector(t)

	mockReorgDetector.EXPECT().Subscribe(mock.Anything).Return(nil, nil)
	mockReorgDetector.EXPECT().GetFinalizedBlockType().Return(blockFinalityType)
	mockReorgDetector.EXPECT().String().Return("mockReorgDetector")

	dbQueryTimeout := 30 * time.Second

	bridgeSyncCfg := Config{
		DBPath:                             dbPath,
		BridgeAddr:                         bridge,
		BlockFinality:                      aggkittypes.LatestBlock,
		SyncBlockChunkSize:                 syncBlockChunkSize,
		InitialBlockNum:                    initialBlock,
		WaitForNewBlocksPeriod:             cfgtypes.NewDuration(waitForNewBlocksPeriod),
		RetryAfterErrorPeriod:              cfgtypes.NewDuration(retryAfterErrorPeriod),
		MaxRetryAttemptsAfterError:         maxRetryAttemptsAfterError,
		RequireStorageContentCompatibility: false,
		DBQueryTimeout:                     cfgtypes.NewDuration(dbQueryTimeout),
	}
	s, err := NewL2(
		ctx,
		bridgeSyncCfg,
		mockReorgDetector,
		mockEthClient,
		originNetwork,
		false,
	)
	require.NoError(t, err)

	allTokenMirgations := make([]*LegacyTokenMigration, 0, tokenMigrationsCount)
	genericEvts := make([]any, 0, tokenMigrationsCount)

	for i := tokenMigrationsCount - 1; i >= 0; i-- {
		tokenMigrationEvt := &LegacyTokenMigration{
			BlockNum:            blockNum,
			BlockPos:            uint64(i),
			LegacyTokenAddress:  common.HexToAddress(fmt.Sprintf("%d", i+1)),
			UpdatedTokenAddress: common.HexToAddress(fmt.Sprintf("%d", i+2)),
			Amount:              big.NewInt(int64(i * 10)),
		}

		allTokenMirgations = append(allTokenMirgations, tokenMigrationEvt)
		genericEvts = append(genericEvts, Event{LegacyTokenMigration: tokenMigrationEvt})
	}

	block := sync.Block{
		Num:    blockNum,
		Events: genericEvts,
	}

	err = s.processor.ProcessBlock(context.Background(), block)
	require.NoError(t, err)

	t.Run("retrieve all token migrations", func(t *testing.T) {
		tokenMigrations, totalTokenMigrations, err := s.GetLegacyTokenMigrations(context.Background(), 1, tokenMigrationsCount)
		require.NoError(t, err)
		require.Equal(t, tokenMigrationsCount, totalTokenMigrations)
		require.Equal(t, allTokenMirgations, tokenMigrations)
	})

	t.Run("retrieve paginated token migrations", func(t *testing.T) {
		pageSize := uint32(5)

		for page := uint32(1); page <= 4; page++ {
			tokenMigrations, totalTokenMigrations, err := s.GetLegacyTokenMigrations(context.Background(), page, pageSize)
			require.NoError(t, err)
			require.Equal(t, tokenMigrationsCount, totalTokenMigrations)

			startIndex := (page - 1) * pageSize
			endIndex := startIndex + pageSize
			require.Equal(t, allTokenMirgations[startIndex:endIndex], tokenMigrations)
		}
	})

	t.Run("retrieve non-existent page", func(t *testing.T) {
		pageSize := uint32(5)
		pageNum := uint32(5)

		tokenMigrations, totalTokenMigrations, err := s.GetLegacyTokenMigrations(context.Background(), pageNum, pageSize)
		require.ErrorContains(t, err,
			"invalid page number for given page size and total number of legacy token migrations")
		require.Equal(t, 0, totalTokenMigrations)
		require.Nil(t, tokenMigrations)
	})

	t.Run("provide invalid page number", func(t *testing.T) {
		pageSize := uint32(0)
		pageNum := uint32(0)

		_, _, err := s.GetLegacyTokenMigrations(context.Background(), pageNum, pageSize)
		require.ErrorIs(t, err, ErrInvalidPageNumber)
	})

	t.Run("provide invalid page size", func(t *testing.T) {
		pageSize := uint32(0)
		pageNum := uint32(4)

		_, _, err := s.GetTokenMappings(context.Background(), pageNum, pageSize, "")
		require.ErrorIs(t, err, ErrInvalidPageSize)
	})

	t.Run("inconsistent state", func(t *testing.T) {
		s.processor.halted = true
		_, _, err := s.GetTokenMappings(context.Background(), 0, 0, "")
		require.ErrorIs(t, err, sync.ErrInconsistentState)
	})
}

func TestGetBridgePaged(t *testing.T) {
	s := BridgeSync{processor: &processor{halted: true}}
	_, _, err := s.GetBridgesPaged(context.Background(), 0, 0, nil, nil, "")
	require.ErrorIs(t, err, sync.ErrInconsistentState)
}

func TestGetClaimPaged(t *testing.T) {
	s := BridgeSync{processor: &processor{
		halted: true,
		log:    log.WithFields("module", "L2BridgeSyncer"),
	}}
	_, _, err := s.GetClaimsPaged(context.Background(), 0, 0, nil, "", nil)
	require.ErrorIs(t, err, sync.ErrInconsistentState)
}

func TestBridgeSync_GetLastReorgEvent(t *testing.T) {
	expectedReorgEvent := reorgdetector.ReorgEvent{
		DetectedAt: int64(1710000000),
		FromBlock:  uint64(100),
		ToBlock:    uint64(150),
	}
	ctx := context.Background()
	mockReorgDetector := mocksbridgesync.NewReorgDetector(t)
	s := BridgeSync{
		reorgDetector: mockReorgDetector,
		processor: &processor{
			log: log.WithFields("module", "L2BridgeSyncer"),
		},
	}

	t.Run("retrieve last reorg event successfully", func(t *testing.T) {
		mockReorgDetector.EXPECT().GetLastReorgEvent(mock.Anything).Return(expectedReorgEvent, nil).Once()

		reorgEvent, err := s.GetLastReorgEvent(ctx)
		require.NoError(t, err)
		require.NotNil(t, reorgEvent)
		require.Equal(t, expectedReorgEvent.DetectedAt, reorgEvent.DetectedAt)
		require.Equal(t, expectedReorgEvent.FromBlock, reorgEvent.FromBlock)
		require.Equal(t, expectedReorgEvent.ToBlock, reorgEvent.ToBlock)
	})

	t.Run("error retrieving last reorg event", func(t *testing.T) {
		mockReorgDetector.EXPECT().GetLastReorgEvent(mock.Anything).Return(reorgdetector.ReorgEvent{}, errors.New("reorg event not found")).Once()

		reorgEvent, err := s.GetLastReorgEvent(ctx)
		require.Error(t, err)
		require.Nil(t, reorgEvent)
	})
}

func TestBridgeSync_GetLastRoot(t *testing.T) {
	const (
		syncBlockChunkSize         = uint64(100)
		initialBlock               = uint64(0)
		waitForNewBlocksPeriod     = time.Second * 10
		retryAfterErrorPeriod      = time.Second * 5
		maxRetryAttemptsAfterError = 3
		originNetwork              = uint32(1)
	)

	var (
		blockFinalityType = aggkittypes.SafeBlock
		ctx               = context.Background()
		dbPath            = path.Join(t.TempDir(), "TestGetLastRoot.sqlite")
		bridge            = common.HexToAddress("0x1234567890abcdef1234567890abcdef12345678")
	)

	mockEthClient := mocksethclient.NewEthClienter(t)
	mockEthClient.EXPECT().CallContract(mock.Anything, mock.Anything, mock.Anything).Return(
		common.FromHex("0x000000000000000000000000000000000000000000000000000000000000002a"), nil).Once()
	mockEthClient.EXPECT().
		CallContract(
			mock.Anything,
			mock.Anything,
			mock.Anything,
		).
		Return(common.LeftPadBytes(common.HexToAddress("0x3c351e10").Bytes(), 32), nil).
		Maybe()
	mockReorgDetector := mocksbridgesync.NewReorgDetector(t)

	mockReorgDetector.EXPECT().Subscribe(mock.Anything).Return(nil, nil)
	mockReorgDetector.EXPECT().GetFinalizedBlockType().Return(blockFinalityType)
	mockReorgDetector.EXPECT().String().Return("mockReorgDetector")

	dbQueryTimeout := 30 * time.Second

	bridgeSyncCfg := Config{
		DBPath:                             dbPath,
		BridgeAddr:                         bridge,
		BlockFinality:                      aggkittypes.LatestBlock,
		SyncBlockChunkSize:                 syncBlockChunkSize,
		InitialBlockNum:                    initialBlock,
		WaitForNewBlocksPeriod:             cfgtypes.NewDuration(waitForNewBlocksPeriod),
		RetryAfterErrorPeriod:              cfgtypes.NewDuration(retryAfterErrorPeriod),
		MaxRetryAttemptsAfterError:         maxRetryAttemptsAfterError,
		RequireStorageContentCompatibility: false,
		DBQueryTimeout:                     cfgtypes.NewDuration(dbQueryTimeout),
	}
	s, err := NewL2(
		ctx,
		bridgeSyncCfg,
		mockReorgDetector,
		mockEthClient,
		originNetwork,
		false,
	)
	require.NoError(t, err)

	t.Run("get last root when no roots exist", func(t *testing.T) {
		root, err := s.GetLastRoot(ctx)
		require.Error(t, err)
		require.Nil(t, root)
		// The error should be db.ErrNotFound from the tree package
		require.Contains(t, err.Error(), "not found")
	})

	t.Run("get last root after processing bridge events", func(t *testing.T) {
		// Create some bridge events to process
		bridgeEvents := []interface{}{
			Event{Bridge: &Bridge{
				BlockNum:           1,
				BlockPos:           0,
				FromAddress:        common.HexToAddress("0x1111111111111111111111111111111111111111"),
				TxHash:             common.HexToHash("0x2222222222222222222222222222222222222222222222222222222222222222"),
				Calldata:           []byte{0x01, 0x02, 0x03},
				BlockTimestamp:     1234567890,
				LeafType:           1,
				OriginNetwork:      1,
				OriginAddress:      common.HexToAddress("0x3333333333333333333333333333333333333333"),
				DestinationNetwork: 2,
				DestinationAddress: common.HexToAddress("0x4444444444444444444444444444444444444444"),
				Amount:             big.NewInt(1000000),
				Metadata:           []byte{0x04, 0x05, 0x06},
				DepositCount:       0,
			}},
			Event{Bridge: &Bridge{
				BlockNum:           1,
				BlockPos:           1,
				FromAddress:        common.HexToAddress("0x5555555555555555555555555555555555555555"),
				TxHash:             common.HexToHash("0x6666666666666666666666666666666666666666666666666666666666666666"),
				Calldata:           []byte{0x07, 0x08, 0x09},
				BlockTimestamp:     1234567890,
				LeafType:           1,
				OriginNetwork:      1,
				OriginAddress:      common.HexToAddress("0x7777777777777777777777777777777777777777"),
				DestinationNetwork: 2,
				DestinationAddress: common.HexToAddress("0x8888888888888888888888888888888888888888"),
				Amount:             big.NewInt(2000000),
				Metadata:           []byte{0x0a, 0x0b, 0x0c},
				DepositCount:       1,
			}},
		}

		block := sync.Block{
			Num:    1,
			Events: bridgeEvents,
		}

		err = s.processor.ProcessBlock(ctx, block)
		require.NoError(t, err)

		root, err := s.GetLastRoot(ctx)
		require.NoError(t, err)
		require.NotNil(t, root)
		require.Equal(t, uint64(1), root.BlockNum)
		require.Equal(t, uint64(1), root.BlockPosition)
		require.Equal(t, uint32(1), root.Index)
		require.NotEqual(t, common.Hash{}, root.Hash)
	})

	t.Run("get last root when processor is halted", func(t *testing.T) {
		s.processor.halted = true
		root, err := s.GetLastRoot(ctx)
		require.ErrorIs(t, err, sync.ErrInconsistentState)
		require.Nil(t, root)
	})

	t.Run("get last root after multiple blocks", func(t *testing.T) {
		// Reset halted state
		s.processor.halted = false

		// Process another block with more bridge events
		bridgeEvents := []interface{}{
			Event{Bridge: &Bridge{
				BlockNum:           2,
				BlockPos:           0,
				FromAddress:        common.HexToAddress("0x9999999999999999999999999999999999999999"),
				TxHash:             common.HexToHash("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"),
				Calldata:           []byte{0x0d, 0x0e, 0x0f},
				BlockTimestamp:     1234567891,
				LeafType:           1,
				OriginNetwork:      1,
				OriginAddress:      common.HexToAddress("0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"),
				DestinationNetwork: 2,
				DestinationAddress: common.HexToAddress("0xcccccccccccccccccccccccccccccccccccccccc"),
				Amount:             big.NewInt(3000000),
				Metadata:           []byte{0x10, 0x11, 0x12},
				DepositCount:       2,
			}},
		}

		block := sync.Block{
			Num:    2,
			Events: bridgeEvents,
		}

		err = s.processor.ProcessBlock(ctx, block)
		require.NoError(t, err)

		root, err := s.GetLastRoot(ctx)
		require.NoError(t, err)
		require.NotNil(t, root)
		require.Equal(t, uint64(2), root.BlockNum)
		require.Equal(t, uint64(0), root.BlockPosition)
		require.Equal(t, uint32(2), root.Index)
		require.NotEqual(t, common.Hash{}, root.Hash)
	})
}
