package sync

import (
	"context"
	"errors"
	"sync"
	"testing"
	"time"

	"github.com/agglayer/aggkit/db/compatibility"
	compmocks "github.com/agglayer/aggkit/db/compatibility/mocks"
	"github.com/agglayer/aggkit/log"
	"github.com/agglayer/aggkit/reorgdetector"
	"github.com/ethereum/go-ethereum/common"
	"github.com/stretchr/testify/mock"
	"github.com/stretchr/testify/require"
)

var (
	reorgDetectorID = "foo"
	errUnittest     = errors.New("unittest error")
)

func TestSync(t *testing.T) {
	rh := &RetryHandler{
		MaxRetryAttemptsAfterError: 5,
		RetryAfterErrorPeriod:      time.Millisecond * 100,
	}
	ctx := t.Context()
	rdm := NewReorgDetectorMock(t)
	pm := NewProcessorMock(t)
	dm := NewDownloaderMock(t)
	compatibilityCheckerMock := compmocks.NewCompatibilityChecker(t)
	compatibilityCheckerMock.EXPECT().Check(ctx, mock.Anything).Return(nil)

	firstReorgedBlock := make(chan uint64)
	reorgProcessed := make(chan bool)
	rdm.EXPECT().Subscribe(reorgDetectorID).
		Return(
			&reorgdetector.Subscription{
				ReorgedBlock:   firstReorgedBlock,
				ReorgProcessed: reorgProcessed,
			}, nil)
	driver, err := NewEVMDriver(rdm, pm, dm, reorgDetectorID, 10, rh, compatibilityCheckerMock)
	require.NoError(t, err)
	expectedBlock1 := EVMBlock{
		EVMBlockHeader: EVMBlockHeader{
			Num:  3,
			Hash: common.HexToHash("03"),
		},
	}
	expectedBlock2 := EVMBlock{
		EVMBlockHeader: EVMBlockHeader{
			Num:  9,
			Hash: common.HexToHash("09"),
		},
	}
	type reorgSemaphore struct {
		mu    sync.Mutex
		green bool
	}
	reorg1Completed := reorgSemaphore{}
	dm.EXPECT().Download(mock.Anything, mock.Anything, mock.Anything).
		Run(func(ctx context.Context, _ uint64, downloadedCh chan EVMBlock) {
			log.Info("entering mock loop")
			for {
				select {
				case <-ctx.Done():
					log.Info("closing channel")
					close(downloadedCh)
					return
				default:
				}
				reorg1Completed.mu.Lock()
				green := reorg1Completed.green
				reorg1Completed.mu.Unlock()
				if green {
					downloadedCh <- expectedBlock2
				} else {
					downloadedCh <- expectedBlock1
				}
				time.Sleep(100 * time.Millisecond)
			}
		})

	// Mocking this actions, the driver should "store" all the blocks from the downloader
	pm.EXPECT().GetLastProcessedBlock(ctx).Return(uint64(3), nil)
	rdm.EXPECT().AddBlockToTrack(mock.Anything, reorgDetectorID, expectedBlock1.Num, expectedBlock1.Hash).Return(nil)
	pm.EXPECT().ProcessBlock(mock.Anything, Block{Num: expectedBlock1.Num, Events: expectedBlock1.Events, Hash: expectedBlock1.Hash}).
		Return(nil)
	rdm.EXPECT().AddBlockToTrack(mock.Anything, reorgDetectorID, expectedBlock2.Num, expectedBlock2.Hash).
		Return(nil)
	pm.EXPECT().
		ProcessBlock(mock.Anything, Block{Num: expectedBlock2.Num, Events: expectedBlock2.Events, Hash: expectedBlock2.Hash}).
		Return(nil)
	go driver.Sync(ctx)
	time.Sleep(time.Millisecond * 200) // time to download expectedBlock1

	// Trigger reorg 1
	reorgedBlock1 := uint64(5)
	pm.EXPECT().Reorg(ctx, reorgedBlock1).Return(nil)
	firstReorgedBlock <- reorgedBlock1
	ok := <-reorgProcessed
	require.True(t, ok)
	reorg1Completed.mu.Lock()
	reorg1Completed.green = true
	reorg1Completed.mu.Unlock()
	time.Sleep(time.Millisecond * 200) // time to download expectedBlock2

	// Trigger reorg 2: syncer restarts the process
	reorgedBlock2 := uint64(7)
	pm.EXPECT().Reorg(ctx, reorgedBlock2).Return(nil)
	firstReorgedBlock <- reorgedBlock2
	ok = <-reorgProcessed
	require.True(t, ok)
}

func TestSync_ReorgCancelsRetryHandlerInHandleNewBlock(t *testing.T) {
	rh := &RetryHandler{
		MaxRetryAttemptsAfterError: -1, // infinite retries
		RetryAfterErrorPeriod:      100 * time.Millisecond,
	}
	ctx, cancel := context.WithCancel(t.Context())
	defer cancel()

	rdm := NewReorgDetectorMock(t)
	pm := NewProcessorMock(t)
	dm := NewDownloaderMock(t)
	compatibilityCheckerMock := compmocks.NewCompatibilityChecker(t)
	compatibilityCheckerMock.EXPECT().Check(ctx, mock.Anything).Return(nil)

	var (
		reorgedBlockCh   = make(chan uint64)
		reorgProcessedCh = make(chan bool)
	)

	rdm.EXPECT().Subscribe(reorgDetectorID).
		Return(&reorgdetector.Subscription{
			ReorgedBlock:   reorgedBlockCh,
			ReorgProcessed: reorgProcessedCh,
		}, nil)

	driver, err := NewEVMDriver(rdm, pm, dm, reorgDetectorID, 10, rh, compatibilityCheckerMock)
	require.NoError(t, err)

	reorgedBlock := uint64(5)

	expectedBlock := EVMBlock{
		EVMBlockHeader: EVMBlockHeader{
			Num:  10,
			Hash: common.HexToHash("a"),
		},
	}

	cancelObserved := make(chan struct{})

	// infinite loop that keeps feeding the same block
	dm.EXPECT().Download(mock.Anything, mock.Anything, mock.Anything).
		Run(func(ctx context.Context, _ uint64, ch chan EVMBlock) {
			for {
				ch <- expectedBlock
				select {
				case <-ctx.Done():
					close(ch)
					return
				case <-time.After(50 * time.Millisecond):
					continue
				}
			}
		})

	pm.EXPECT().GetLastProcessedBlock(ctx).Return(uint64(3), nil)

	// AddBlockToTrack always returns nil
	rdm.EXPECT().AddBlockToTrack(mock.Anything, reorgDetectorID, expectedBlock.Num, expectedBlock.Hash).
		Return(nil)

	// ProcessBlock always errors, until cancelled
	pm.EXPECT().
		ProcessBlock(mock.Anything, Block{Num: expectedBlock.Num, Hash: expectedBlock.Hash}).
		RunAndReturn(func(ctx context.Context, _ Block) error {
			select {
			case <-ctx.Done():
				close(cancelObserved)
				return ctx.Err()
			case <-time.After(50 * time.Millisecond):
				return errors.New("processing failed")
			}
		})

	go driver.Sync(ctx)

	time.Sleep(300 * time.Millisecond) // Let it retry a few times

	// trigger reorg while it's retrying
	pm.EXPECT().Reorg(ctx, reorgedBlock).Return(nil)
	reorgedBlockCh <- reorgedBlock

	ok := <-reorgProcessedCh
	require.True(t, ok)
}

func TestHandleNewBlock(t *testing.T) {
	type call struct {
		addBlockErrs []error
		processErrs  []error
		expectedErr  error
	}

	tests := []struct {
		name  string
		block EVMBlock
		calls call
	}{
		{
			name: "happy path",
			block: EVMBlock{
				EVMBlockHeader: EVMBlockHeader{
					Num:  1,
					Hash: common.HexToHash("f00"),
				},
			},
			calls: call{
				addBlockErrs: []error{nil},
				processErrs:  []error{nil},
			},
		},
		{
			name: "reorg detector fails once",
			block: EVMBlock{
				EVMBlockHeader: EVMBlockHeader{
					Num:  2,
					Hash: common.HexToHash("f00"),
				},
			},
			calls: call{
				addBlockErrs: []error{errors.New("foo"), nil},
				processErrs:  []error{nil},
			},
		},
		{
			name: "processor fails once",
			block: EVMBlock{
				EVMBlockHeader: EVMBlockHeader{
					Num:  3,
					Hash: common.HexToHash("f00"),
				},
			},
			calls: call{
				addBlockErrs: []error{nil},
				processErrs:  []error{errors.New("foo"), nil},
			},
		},
		{
			name: "processor returns ErrInconsistentState",
			block: EVMBlock{
				EVMBlockHeader: EVMBlockHeader{
					Num:  4,
					Hash: common.HexToHash("f00"),
				},
			},
			calls: call{
				addBlockErrs: []error{nil},
				processErrs:  []error{ErrInconsistentState},
				expectedErr:  ErrInconsistentState,
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			rh := &RetryHandler{
				MaxRetryAttemptsAfterError: 5,
				RetryAfterErrorPeriod:      100 * time.Millisecond,
			}

			ctx := t.Context()
			rdm := NewReorgDetectorMock(t)
			pm := NewProcessorMock(t)
			dm := NewDownloaderMock(t)
			compatibilityCheckerMock := compmocks.NewCompatibilityChecker(t)

			rdm.EXPECT().Subscribe(reorgDetectorID).Return(&reorgdetector.Subscription{}, nil)
			driver, err := NewEVMDriver(rdm, pm, dm, reorgDetectorID, 10, rh, compatibilityCheckerMock)
			require.NoError(t, err)

			// Expectations for AddBlockToTrack
			for _, err := range tt.calls.addBlockErrs {
				rdm.EXPECT().
					AddBlockToTrack(ctx, reorgDetectorID, tt.block.Num, tt.block.Hash).
					Return(err).Once()
			}

			// Expectations for ProcessBlock
			for _, err := range tt.calls.processErrs {
				pm.EXPECT().
					ProcessBlock(ctx, Block{Num: tt.block.Num, Events: tt.block.Events, Hash: tt.block.Hash}).
					Return(err).Once()
			}

			err = driver.handleNewBlock(ctx, tt.block)

			if tt.calls.expectedErr != nil {
				require.ErrorIs(t, err, tt.calls.expectedErr)
			} else {
				require.NoError(t, err)
			}
		})
	}
}

func TestHandleReorg(t *testing.T) {
	tests := []struct {
		name              string
		firstReorgedBlock uint64
		reorgReturns      []error
	}{
		{
			name:              "happy path",
			firstReorgedBlock: 5,
			reorgReturns:      []error{nil},
		},
		{
			name:              "processor fails twice then succeeds",
			firstReorgedBlock: 7,
			reorgReturns: []error{
				errors.New("first failure"),
				errors.New("second failure"),
				nil,
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			rh := &RetryHandler{
				MaxRetryAttemptsAfterError: 5,
				RetryAfterErrorPeriod:      10 * time.Millisecond,
			}

			rdm := NewReorgDetectorMock(t)
			pm := NewProcessorMock(t)
			dm := NewDownloaderMock(t)
			compatChecker := compmocks.NewCompatibilityChecker(t)

			reorgProcessed := make(chan bool, 1) // buffer to avoid blocking

			rdm.EXPECT().Subscribe(reorgDetectorID).
				Return(&reorgdetector.Subscription{ReorgProcessed: reorgProcessed}, nil)

			ctx := t.Context()
			driver, err := NewEVMDriver(rdm, pm, dm, reorgDetectorID, 10, rh, compatChecker)
			require.NoError(t, err)

			// Set expectations for Reorg calls
			for _, ret := range tt.reorgReturns {
				pm.EXPECT().
					Reorg(mock.Anything, tt.firstReorgedBlock).
					Return(ret).
					Once()
			}

			// Call the method
			err = driver.handleReorg(ctx, tt.firstReorgedBlock)
			require.NoError(t, err)

			// Wait for signal with timeout
			select {
			case ok := <-reorgProcessed:
				require.True(t, ok, "expected true on reorgProcessed channel")
			case <-time.After(100 * time.Millisecond):
				t.Fatal("timeout waiting for ReorgProcessed signal")
			}
		})
	}
}

func TestCheckCompatibility(t *testing.T) {
	reorgDetectorMock := NewReorgDetectorMock(t)
	processorMock := NewProcessorMock(t)
	downloaderMock := NewDownloaderMock(t)
	retryHandler := &RetryHandler{
		MaxRetryAttemptsAfterError: 1,
		RetryAfterErrorPeriod:      time.Millisecond * 1,
	}
	compatibilityCheckerMock := compmocks.NewCompatibilityChecker(t)

	reorgDetectorMock.EXPECT().Subscribe(reorgDetectorID).Return(&reorgdetector.Subscription{}, nil)

	driver, err := NewEVMDriver(reorgDetectorMock, processorMock, downloaderMock, reorgDetectorID, 10, retryHandler, compatibilityCheckerMock)
	require.NoError(t, err)
	driver.compatibilityChecker = compatibilityCheckerMock
	t.Run("pass compatibility check", func(t *testing.T) {
		compatibilityCheckerMock.EXPECT().Check(context.Background(), nil).Return(nil)
		processorMock.EXPECT().GetLastProcessedBlock(context.Background()).Return(uint64(1), errUnittest)
		LogFatalf = func(format string, args ...any) {
			panic("should not call log.Fatalf")
		}
		require.Panics(t, func() {
			driver.Sync(context.Background())
		}, "should stop because GetLastProcessedBlock failed")
	})
	t.Run("fails compatibility check ", func(t *testing.T) {
		compatibilityCheckerMock.EXPECT().Check(context.Background(), nil).Return(errUnittest)
		LogFatalf = func(format string, args ...any) {
			panic("should not call log.Fatalf")
		}
		require.Panics(t, func() {
			driver.Sync(context.Background())
		}, "should stop because GetLastProcessedBlock failed")
	})
}

func TestEVMDriver_Sync(t *testing.T) {
	tests := []struct {
		name string // description of this test case
		// Named input parameters for receiver constructor.
		reorgDetector        ReorgDetector
		processor            processorInterface
		downloader           Downloader
		reorgDetectorID      string
		downloadBufferSize   int
		rh                   *RetryHandler
		compatibilityChecker compatibility.CompatibilityChecker
	}{
		// TODO: Add test cases.
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			d, err := NewEVMDriver(tt.reorgDetector, tt.processor, tt.downloader, tt.reorgDetectorID, tt.downloadBufferSize, tt.rh, tt.compatibilityChecker)
			if err != nil {
				t.Fatalf("could not construct receiver type: %v", err)
			}
			d.Sync(context.Background())
		})
	}
}
