package finality

import (
	"context"
	"errors"
	"math/rand" // nosemgrep
	"testing"

	"github.com/stretchr/testify/require"

	"github.com/ethereum/go-ethereum/common"
	"github.com/ethereum/go-ethereum/log"

	"github.com/exchain/go-exchain/op-node/rollup"
	"github.com/exchain/go-exchain/op-node/rollup/derive"
	"github.com/exchain/go-exchain/op-node/rollup/engine"
	"github.com/exchain/go-exchain/op-service/eth"
	"github.com/exchain/go-exchain/op-service/testlog"
	"github.com/exchain/go-exchain/op-service/testutils"
)

func TestEngineQueue_Finalize(t *testing.T) {
	rng := rand.New(rand.NewSource(1234))

	l1Time := uint64(2)
	refA := testutils.RandomBlockRef(rng)

	refB := eth.L1BlockRef{
		Hash:       testutils.RandomHash(rng),
		Number:     refA.Number + 1,
		ParentHash: refA.Hash,
		Time:       refA.Time + l1Time,
	}
	refC := eth.L1BlockRef{
		Hash:       testutils.RandomHash(rng),
		Number:     refB.Number + 1,
		ParentHash: refB.Hash,
		Time:       refB.Time + l1Time,
	}
	refD := eth.L1BlockRef{
		Hash:       testutils.RandomHash(rng),
		Number:     refC.Number + 1,
		ParentHash: refC.Hash,
		Time:       refC.Time + l1Time,
	}
	refE := eth.L1BlockRef{
		Hash:       testutils.RandomHash(rng),
		Number:     refD.Number + 1,
		ParentHash: refD.Hash,
		Time:       refD.Time + l1Time,
	}
	refF := eth.L1BlockRef{
		Hash:       testutils.RandomHash(rng),
		Number:     refE.Number + 1,
		ParentHash: refE.Hash,
		Time:       refE.Time + l1Time,
	}
	refG := eth.L1BlockRef{
		Hash:       testutils.RandomHash(rng),
		Number:     refF.Number + 1,
		ParentHash: refF.Hash,
		Time:       refF.Time + l1Time,
	}
	refH := eth.L1BlockRef{
		Hash:       testutils.RandomHash(rng),
		Number:     refG.Number + 1,
		ParentHash: refG.Hash,
		Time:       refG.Time + l1Time,
	}
	//refI := eth.L1BlockRef{
	//	Hash:       testutils.RandomHash(rng),
	//	Number:     refH.Number + 1,
	//	ParentHash: refH.Hash,
	//	Time:       refH.Time + l1Time,
	//}

	refA0 := eth.L2BlockRef{
		Hash:           testutils.RandomHash(rng),
		Number:         0,
		ParentHash:     common.Hash{},
		Time:           refA.Time,
		L1Origin:       refA.ID(),
		SequenceNumber: 0,
	}
	cfg := &rollup.Config{
		Genesis: rollup.Genesis{
			L1:     refA.ID(),
			L2:     refA0.ID(),
			L2Time: refA0.Time,
		},
		BlockTime:     1,
		SeqWindowSize: 2,
	}
	refA1 := eth.L2BlockRef{
		Hash:           testutils.RandomHash(rng),
		Number:         refA0.Number + 1,
		ParentHash:     refA0.Hash,
		Time:           refA0.Time + cfg.BlockTime,
		L1Origin:       refA.ID(),
		SequenceNumber: 1,
	}
	refB0 := eth.L2BlockRef{
		Hash:           testutils.RandomHash(rng),
		Number:         refA1.Number + 1,
		ParentHash:     refA1.Hash,
		Time:           refA1.Time + cfg.BlockTime,
		L1Origin:       refB.ID(),
		SequenceNumber: 0,
	}
	refB1 := eth.L2BlockRef{
		Hash:           testutils.RandomHash(rng),
		Number:         refB0.Number + 1,
		ParentHash:     refB0.Hash,
		Time:           refB0.Time + cfg.BlockTime,
		L1Origin:       refB.ID(),
		SequenceNumber: 1,
	}
	refC0 := eth.L2BlockRef{
		Hash:           testutils.RandomHash(rng),
		Number:         refB1.Number + 1,
		ParentHash:     refB1.Hash,
		Time:           refB1.Time + cfg.BlockTime,
		L1Origin:       refC.ID(),
		SequenceNumber: 0,
	}
	refC1 := eth.L2BlockRef{
		Hash:           testutils.RandomHash(rng),
		Number:         refC0.Number + 1,
		ParentHash:     refC0.Hash,
		Time:           refC0.Time + cfg.BlockTime,
		L1Origin:       refC.ID(),
		SequenceNumber: 1,
	}
	refD0 := eth.L2BlockRef{
		Hash:           testutils.RandomHash(rng),
		Number:         refC1.Number + 1,
		ParentHash:     refC1.Hash,
		Time:           refC1.Time + cfg.BlockTime,
		L1Origin:       refD.ID(),
		SequenceNumber: 0,
	}
	refD1 := eth.L2BlockRef{
		Hash:           testutils.RandomHash(rng),
		Number:         refD0.Number + 1,
		ParentHash:     refD0.Hash,
		Time:           refD0.Time + cfg.BlockTime,
		L1Origin:       refD.ID(),
		SequenceNumber: 1,
	}
	refE0 := eth.L2BlockRef{
		Hash:           testutils.RandomHash(rng),
		Number:         refD1.Number + 1,
		ParentHash:     refD1.Hash,
		Time:           refD1.Time + cfg.BlockTime,
		L1Origin:       refE.ID(),
		SequenceNumber: 0,
	}
	refE1 := eth.L2BlockRef{
		Hash:           testutils.RandomHash(rng),
		Number:         refE0.Number + 1,
		ParentHash:     refE0.Hash,
		Time:           refE0.Time + cfg.BlockTime,
		L1Origin:       refE.ID(),
		SequenceNumber: 1,
	}
	refF0 := eth.L2BlockRef{
		Hash:           testutils.RandomHash(rng),
		Number:         refE1.Number + 1,
		ParentHash:     refE1.Hash,
		Time:           refE1.Time + cfg.BlockTime,
		L1Origin:       refF.ID(),
		SequenceNumber: 0,
	}
	refF1 := eth.L2BlockRef{
		Hash:           testutils.RandomHash(rng),
		Number:         refF0.Number + 1,
		ParentHash:     refF0.Hash,
		Time:           refF0.Time + cfg.BlockTime,
		L1Origin:       refF.ID(),
		SequenceNumber: 1,
	}
	_ = refF1

	// We expect the L1 block that the finalized L2 data was derived from to be checked,
	// to be sure it is part of the canonical chain, after the finalization signal.
	t.Run("basic", func(t *testing.T) {
		logger := testlog.Logger(t, log.LevelInfo)
		l1F := &testutils.MockL1Source{}
		defer l1F.AssertExpectations(t)
		l1F.ExpectL1BlockRefByNumber(refD.Number, refD, nil)
		l1F.ExpectL1BlockRefByNumber(refD.Number, refD, nil)

		emitter := &testutils.MockEmitter{}
		fi := NewFinalizer(context.Background(), logger, &rollup.Config{}, l1F)
		fi.AttachEmitter(emitter)

		// now say C1 was included in D and became the new safe head
		fi.OnEvent(engine.SafeDerivedEvent{Safe: refC1, DerivedFrom: refD})
		fi.OnEvent(derive.DeriverIdleEvent{Origin: refD})
		emitter.AssertExpectations(t)

		// now say D0 was included in E and became the new safe head
		fi.OnEvent(engine.SafeDerivedEvent{Safe: refD0, DerivedFrom: refE})
		fi.OnEvent(derive.DeriverIdleEvent{Origin: refE})
		emitter.AssertExpectations(t)

		// Let's finalize D from which we fully derived C1, but not D0
		// This will trigger an attempt of L2 finalization.
		emitter.ExpectOnce(TryFinalizeEvent{})
		fi.OnEvent(FinalizeL1Event{FinalizedL1: refD})
		emitter.AssertExpectations(t)

		// C1 was included in finalized D, and should now be finalized
		emitter.ExpectOnce(engine.PromoteFinalizedEvent{Ref: refC1})
		fi.OnEvent(TryFinalizeEvent{})
		emitter.AssertExpectations(t)
	})

	// Finality signal is received, but couldn't immediately be checked
	t.Run("retry", func(t *testing.T) {
		logger := testlog.Logger(t, log.LevelInfo)
		l1F := &testutils.MockL1Source{}
		defer l1F.AssertExpectations(t)
		l1F.ExpectL1BlockRefByNumber(refD.Number, refD, errors.New("fake error"))
		l1F.ExpectL1BlockRefByNumber(refD.Number, refD, nil) // to check finality signal
		l1F.ExpectL1BlockRefByNumber(refD.Number, refD, nil) // to check what was derived from (same in this case)

		emitter := &testutils.MockEmitter{}
		fi := NewFinalizer(context.Background(), logger, &rollup.Config{}, l1F)
		fi.AttachEmitter(emitter)

		// now say C1 was included in D and became the new safe head
		fi.OnEvent(engine.SafeDerivedEvent{Safe: refC1, DerivedFrom: refD})
		fi.OnEvent(derive.DeriverIdleEvent{Origin: refD})
		emitter.AssertExpectations(t)

		// now say D0 was included in E and became the new safe head
		fi.OnEvent(engine.SafeDerivedEvent{Safe: refD0, DerivedFrom: refE})
		fi.OnEvent(derive.DeriverIdleEvent{Origin: refE})
		emitter.AssertExpectations(t)

		// let's finalize D from which we fully derived C1, but not D0
		emitter.ExpectOnce(TryFinalizeEvent{})
		fi.OnEvent(FinalizeL1Event{FinalizedL1: refD})
		emitter.AssertExpectations(t)
		// C1 was included in finalized D, but finality could not be verified yet, due to temporary test error
		emitter.ExpectOnceType("L1TemporaryErrorEvent")
		fi.OnEvent(TryFinalizeEvent{})
		emitter.AssertExpectations(t)

		// upon the next signal we should schedule a finalization re-attempt
		emitter.ExpectOnce(TryFinalizeEvent{})
		fi.OnEvent(derive.DeriverIdleEvent{Origin: refF})
		emitter.AssertExpectations(t)

		// C1 was included in finalized D, and should now be finalized, as check can succeed when revisited
		emitter.ExpectOnce(engine.PromoteFinalizedEvent{Ref: refC1})
		fi.OnEvent(TryFinalizeEvent{})
		emitter.AssertExpectations(t)
	})

	// Test that finality progression can repeat a few times.
	t.Run("repeat", func(t *testing.T) {
		logger := testlog.Logger(t, log.LevelInfo)
		l1F := &testutils.MockL1Source{}
		defer l1F.AssertExpectations(t)

		emitter := &testutils.MockEmitter{}
		fi := NewFinalizer(context.Background(), logger, &rollup.Config{}, l1F)
		fi.AttachEmitter(emitter)

		fi.OnEvent(engine.SafeDerivedEvent{Safe: refC1, DerivedFrom: refD})
		fi.OnEvent(derive.DeriverIdleEvent{Origin: refD})
		emitter.AssertExpectations(t)

		fi.OnEvent(engine.SafeDerivedEvent{Safe: refD0, DerivedFrom: refE})
		fi.OnEvent(derive.DeriverIdleEvent{Origin: refE})
		emitter.AssertExpectations(t)

		// L1 finality signal will trigger L2 finality attempt
		emitter.ExpectOnce(TryFinalizeEvent{})
		fi.OnEvent(FinalizeL1Event{FinalizedL1: refD})
		emitter.AssertExpectations(t)

		// C1 was included in D, and should be finalized now
		emitter.ExpectOnce(engine.PromoteFinalizedEvent{Ref: refC1})
		l1F.ExpectL1BlockRefByNumber(refD.Number, refD, nil)
		l1F.ExpectL1BlockRefByNumber(refD.Number, refD, nil)
		fi.OnEvent(TryFinalizeEvent{})
		emitter.AssertExpectations(t)
		l1F.AssertExpectations(t)

		// Another L1 finality event, trigger L2 finality attempt again
		emitter.ExpectOnce(TryFinalizeEvent{})
		fi.OnEvent(FinalizeL1Event{FinalizedL1: refE})
		emitter.AssertExpectations(t)

		// D0 was included in E, and should be finalized now
		emitter.ExpectOnce(engine.PromoteFinalizedEvent{Ref: refD0})
		l1F.ExpectL1BlockRefByNumber(refE.Number, refE, nil)
		l1F.ExpectL1BlockRefByNumber(refE.Number, refE, nil)
		fi.OnEvent(TryFinalizeEvent{})
		emitter.AssertExpectations(t)
		l1F.AssertExpectations(t)

		// D0 is still there in the buffer, and may be finalized again, if it were not for the latest forkchoice update.
		fi.OnEvent(engine.ForkchoiceUpdateEvent{FinalizedL2Head: refD0})
		emitter.AssertExpectations(t) // should trigger no events

		// we expect a finality attempt, since we have not idled on something yet
		emitter.ExpectOnce(TryFinalizeEvent{})
		fi.OnEvent(derive.DeriverIdleEvent{Origin: refG})
		emitter.AssertExpectations(t)

		fi.OnEvent(engine.SafeDerivedEvent{Safe: refD1, DerivedFrom: refH})
		fi.OnEvent(engine.SafeDerivedEvent{Safe: refE0, DerivedFrom: refH})
		fi.OnEvent(engine.SafeDerivedEvent{Safe: refE1, DerivedFrom: refH})
		fi.OnEvent(engine.SafeDerivedEvent{Safe: refF0, DerivedFrom: refH})
		fi.OnEvent(engine.SafeDerivedEvent{Safe: refF1, DerivedFrom: refH})
		emitter.AssertExpectations(t) // above updates add data, but no attempt is made until idle or L1 signal

		// We recently finalized already, and there is no new L1 finality data
		fi.OnEvent(derive.DeriverIdleEvent{Origin: refH})
		emitter.AssertExpectations(t)

		// D1-F1 were included in L1 blocks that have not been finalized yet.
		// D0 is known to be finalized already.
		fi.OnEvent(TryFinalizeEvent{})
		emitter.AssertExpectations(t)

		// Now L1 block H is actually finalized, and we can proceed with another attempt
		emitter.ExpectOnce(TryFinalizeEvent{})
		fi.OnEvent(FinalizeL1Event{FinalizedL1: refH})
		emitter.AssertExpectations(t)

		// F1 should be finalized now, since it was included in H
		emitter.ExpectOnce(engine.PromoteFinalizedEvent{Ref: refF1})
		l1F.ExpectL1BlockRefByNumber(refH.Number, refH, nil)
		l1F.ExpectL1BlockRefByNumber(refH.Number, refH, nil)
		fi.OnEvent(TryFinalizeEvent{})
		emitter.AssertExpectations(t)
		l1F.AssertExpectations(t)
	})

	// In this test the finality signal is for a block more than
	// 1 L1 block later than what the L2 data was included in.
	t.Run("older-data", func(t *testing.T) {
		logger := testlog.Logger(t, log.LevelInfo)
		l1F := &testutils.MockL1Source{}
		defer l1F.AssertExpectations(t)
		l1F.ExpectL1BlockRefByNumber(refD.Number, refD, nil) // check the signal
		l1F.ExpectL1BlockRefByNumber(refC.Number, refC, nil) // check what we derived the L2 block from

		emitter := &testutils.MockEmitter{}
		fi := NewFinalizer(context.Background(), logger, &rollup.Config{}, l1F)
		fi.AttachEmitter(emitter)

		// now say B1 was included in C and became the new safe head
		fi.OnEvent(engine.SafeDerivedEvent{Safe: refB1, DerivedFrom: refC})
		fi.OnEvent(derive.DeriverIdleEvent{Origin: refC})
		emitter.AssertExpectations(t)

		// now say C0 was included in E and became the new safe head
		fi.OnEvent(engine.SafeDerivedEvent{Safe: refC0, DerivedFrom: refE})
		fi.OnEvent(derive.DeriverIdleEvent{Origin: refE})
		emitter.AssertExpectations(t)

		// let's finalize D, from which we fully derived B1, but not C0 (referenced L1 origin in L2 block != inclusion of L2 block in L1 chain)
		emitter.ExpectOnce(TryFinalizeEvent{})
		fi.OnEvent(FinalizeL1Event{FinalizedL1: refD})
		emitter.AssertExpectations(t)

		// B1 was included in finalized D, and should now be finalized
		emitter.ExpectOnce(engine.PromoteFinalizedEvent{Ref: refB1})
		fi.OnEvent(TryFinalizeEvent{})
		emitter.AssertExpectations(t)
	})

	// Test that reorg race condition is handled.
	t.Run("reorg-safe", func(t *testing.T) {
		logger := testlog.Logger(t, log.LevelInfo)
		l1F := &testutils.MockL1Source{}
		defer l1F.AssertExpectations(t)
		l1F.ExpectL1BlockRefByNumber(refF.Number, refF, nil) // check signal
		l1F.ExpectL1BlockRefByNumber(refD.Number, refD, nil) // shows reorg to Finalize attempt
		l1F.ExpectL1BlockRefByNumber(refF.Number, refF, nil) // check signal
		l1F.ExpectL1BlockRefByNumber(refD.Number, refD, nil) // shows reorg to OnDerivationL1End attempt
		l1F.ExpectL1BlockRefByNumber(refF.Number, refF, nil) // check signal
		l1F.ExpectL1BlockRefByNumber(refE.Number, refE, nil) // post-reorg

		emitter := &testutils.MockEmitter{}
		fi := NewFinalizer(context.Background(), logger, &rollup.Config{}, l1F)
		fi.AttachEmitter(emitter)

		// now say B1 was included in C and became the new safe head
		fi.OnEvent(engine.SafeDerivedEvent{Safe: refB1, DerivedFrom: refC})
		fi.OnEvent(derive.DeriverIdleEvent{Origin: refC})
		emitter.AssertExpectations(t)

		// temporary fork of the L1, and derived safe L2 blocks from.
		refC0Alt := eth.L2BlockRef{
			Hash:           testutils.RandomHash(rng),
			Number:         refB1.Number + 1,
			ParentHash:     refB1.Hash,
			Time:           refB1.Time + cfg.BlockTime,
			L1Origin:       refC.ID(),
			SequenceNumber: 0,
		}
		refC1Alt := eth.L2BlockRef{
			Hash:           testutils.RandomHash(rng),
			Number:         refC0Alt.Number + 1,
			ParentHash:     refC0Alt.Hash,
			Time:           refC0Alt.Time + cfg.BlockTime,
			L1Origin:       refC.ID(),
			SequenceNumber: 1,
		}
		refDAlt := eth.L1BlockRef{
			Hash:       testutils.RandomHash(rng),
			Number:     refC.Number + 1,
			ParentHash: refC.Hash,
			Time:       refC.Time + l1Time,
		}
		fi.OnEvent(engine.SafeDerivedEvent{Safe: refC0Alt, DerivedFrom: refDAlt})
		fi.OnEvent(engine.SafeDerivedEvent{Safe: refC1Alt, DerivedFrom: refDAlt})

		// We get an early finality signal for F, of the chain that did not include refC0Alt and refC1Alt,
		// as L1 block F does not build on DAlt.
		// The finality signal was for a new chain, while derivation is on an old stale chain.
		// It should be detected that C0Alt and C1Alt cannot actually be finalized,
		// even though they are older than the latest finality signal.
		emitter.ExpectOnce(TryFinalizeEvent{})
		fi.OnEvent(FinalizeL1Event{FinalizedL1: refF})
		emitter.AssertExpectations(t)
		// cannot verify refC0Alt and refC1Alt, and refB1 is older and not checked
		emitter.ExpectOnceType("ResetEvent")
		fi.OnEvent(TryFinalizeEvent{})
		emitter.AssertExpectations(t) // no change in finality

		// And process DAlt, still stuck on old chain.

		emitter.ExpectOnce(TryFinalizeEvent{})
		fi.OnEvent(derive.DeriverIdleEvent{Origin: refDAlt})
		emitter.AssertExpectations(t)
		// no new finalized L2 blocks after early finality signal with stale chain
		emitter.ExpectOnceType("ResetEvent")
		fi.OnEvent(TryFinalizeEvent{})
		emitter.AssertExpectations(t)
		// Now reset, because of the reset error
		fi.OnEvent(rollup.ResetEvent{})
		require.Equal(t, refF, fi.FinalizedL1(), "remember the new finality signal for later however")

		// And process the canonical chain, with empty block D (no post-processing of canonical C0 blocks yet)
		emitter.ExpectOnce(TryFinalizeEvent{})
		fi.OnEvent(derive.DeriverIdleEvent{Origin: refD})
		emitter.AssertExpectations(t)
		fi.OnEvent(TryFinalizeEvent{})
		emitter.AssertExpectations(t) // no new finality

		// Include C0 in E
		fi.OnEvent(engine.SafeDerivedEvent{Safe: refC0, DerivedFrom: refE})
		fi.OnEvent(derive.DeriverIdleEvent{Origin: refE})
		// Due to the "finalityDelay" we don't repeat finality checks shortly after one another,
		// and don't expect a finality attempt.
		emitter.AssertExpectations(t)

		// if we reset the attempt, then we can finalize however.
		fi.triedFinalizeAt = 0
		emitter.ExpectOnce(TryFinalizeEvent{})
		fi.OnEvent(derive.DeriverIdleEvent{Origin: refE})
		emitter.AssertExpectations(t)
		emitter.ExpectOnce(engine.PromoteFinalizedEvent{Ref: refC0})
		fi.OnEvent(TryFinalizeEvent{})
		emitter.AssertExpectations(t)
	})

	// The Finalizer does not promote any blocks to finalized status after interop.
	// Blocks after interop are finalized with the interop deriver and interop backend.
	t.Run("disable-after-interop", func(t *testing.T) {
		logger := testlog.Logger(t, log.LevelInfo)
		l1F := &testutils.MockL1Source{}
		defer l1F.AssertExpectations(t)
		l1F.ExpectL1BlockRefByNumber(refD.Number, refD, nil)
		l1F.ExpectL1BlockRefByNumber(refD.Number, refD, nil)

		emitter := &testutils.MockEmitter{}
		fi := NewFinalizer(context.Background(), logger, &rollup.Config{
			InteropTime: &refC1.Time,
		}, l1F)
		fi.AttachEmitter(emitter)

		// now say C0 and C1 were included in D and became the new safe head
		fi.OnEvent(engine.SafeDerivedEvent{Safe: refC0, DerivedFrom: refD})
		fi.OnEvent(engine.SafeDerivedEvent{Safe: refC1, DerivedFrom: refD})
		fi.OnEvent(derive.DeriverIdleEvent{Origin: refD})
		emitter.AssertExpectations(t)

		emitter.ExpectOnce(TryFinalizeEvent{})
		fi.OnEvent(FinalizeL1Event{FinalizedL1: refD})
		emitter.AssertExpectations(t)

		// C1 was Interop, C0 was not yet interop and can be finalized
		emitter.ExpectOnce(engine.PromoteFinalizedEvent{Ref: refC0})
		fi.OnEvent(TryFinalizeEvent{})
		emitter.AssertExpectations(t)
	})
}
