package sync

import (
	"context"
	"fmt"
	"reflect"
	"testing"
	"time"

	mockChain "github.com/OffchainLabs/prysm/v7/beacon-chain/blockchain/testing"
	"github.com/OffchainLabs/prysm/v7/beacon-chain/core/signing"
	testingdb "github.com/OffchainLabs/prysm/v7/beacon-chain/db/testing"
	doublylinkedtree "github.com/OffchainLabs/prysm/v7/beacon-chain/forkchoice/doubly-linked-tree"
	"github.com/OffchainLabs/prysm/v7/beacon-chain/p2p"
	"github.com/OffchainLabs/prysm/v7/beacon-chain/p2p/encoder"
	mockp2p "github.com/OffchainLabs/prysm/v7/beacon-chain/p2p/testing"
	p2ptypes "github.com/OffchainLabs/prysm/v7/beacon-chain/p2p/types"
	"github.com/OffchainLabs/prysm/v7/beacon-chain/startup"
	"github.com/OffchainLabs/prysm/v7/beacon-chain/state/stategen"
	mockSync "github.com/OffchainLabs/prysm/v7/beacon-chain/sync/initial-sync/testing"
	"github.com/OffchainLabs/prysm/v7/beacon-chain/verification"
	"github.com/OffchainLabs/prysm/v7/config/params"
	"github.com/OffchainLabs/prysm/v7/consensus-types/primitives"
	"github.com/OffchainLabs/prysm/v7/encoding/bytesutil"
	ethpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1"
	"github.com/OffchainLabs/prysm/v7/testing/assert"
	"github.com/OffchainLabs/prysm/v7/testing/require"
	"github.com/OffchainLabs/prysm/v7/time/slots"
	"github.com/golang/snappy"
	pubsub "github.com/libp2p/go-libp2p-pubsub"
	pubsubpb "github.com/libp2p/go-libp2p-pubsub/pb"
	"github.com/libp2p/go-libp2p/core/peer"
)

func TestService_ValidateSyncCommitteeMessage(t *testing.T) {
	beaconDB := testingdb.SetupDB(t)
	headRoot, keys := fillUpBlocksAndState(t.Context(), t, beaconDB)
	defaultTopic := p2p.SyncCommitteeSubnetTopicFormat
	fakeDigest := []byte{0xAB, 0x00, 0xCC, 0x9E}
	defaultTopic = defaultTopic + "/" + encoder.ProtocolSuffixSSZSnappy
	chainService := &mockChain.ChainService{
		Genesis:        time.Now(),
		ValidatorsRoot: [32]byte{'A'},
	}
	var emptySig [96]byte
	type args struct {
		pid   peer.ID
		msg   *ethpb.SyncCommitteeMessage
		topic string
	}
	tests := []struct {
		name     string
		svcopts  []Option
		setupSvc func(s *Service, msg *ethpb.SyncCommitteeMessage, topic string) (*Service, string, *startup.Clock)
		args     args
		want     pubsub.ValidationResult
	}{
		{
			name: "Is syncing",
			svcopts: []Option{
				WithP2P(mockp2p.NewTestP2P(t)),
				WithInitialSync(&mockSync.Sync{IsSyncing: true}),
				WithChainService(chainService),
				WithOperationNotifier(chainService.OperationNotifier()),
			},
			setupSvc: func(s *Service, msg *ethpb.SyncCommitteeMessage, topic string) (*Service, string, *startup.Clock) {
				s.cfg.stateGen = stategen.New(beaconDB, doublylinkedtree.New())
				msg.BlockRoot = headRoot[:]
				s.cfg.beaconDB = beaconDB
				s.initCaches()
				return s, topic, startup.NewClock(time.Now(), [32]byte{})
			},
			args: args{
				pid:   "random",
				topic: "junk",
				msg: &ethpb.SyncCommitteeMessage{
					Slot:           1,
					ValidatorIndex: 1,
					BlockRoot:      params.BeaconConfig().ZeroHash[:],
					Signature:      emptySig[:],
				}},
			want: pubsub.ValidationIgnore,
		},
		{
			name: "Bad Topic",
			svcopts: []Option{
				WithP2P(mockp2p.NewTestP2P(t)),
				WithInitialSync(&mockSync.Sync{IsSyncing: false}),
				WithChainService(chainService),
				WithOperationNotifier(chainService.OperationNotifier()),
			},
			setupSvc: func(s *Service, msg *ethpb.SyncCommitteeMessage, topic string) (*Service, string, *startup.Clock) {
				s.cfg.stateGen = stategen.New(beaconDB, doublylinkedtree.New())
				msg.BlockRoot = headRoot[:]
				s.cfg.beaconDB = beaconDB
				s.initCaches()
				return s, topic, startup.NewClock(time.Now(), [32]byte{})
			},
			args: args{
				pid:   "random",
				topic: "junk",
				msg: &ethpb.SyncCommitteeMessage{
					Slot:           1,
					ValidatorIndex: 1,
					BlockRoot:      params.BeaconConfig().ZeroHash[:],
					Signature:      emptySig[:],
				}},
			want: pubsub.ValidationReject,
		},
		{
			name: "Future Slot Message",
			svcopts: []Option{
				WithP2P(mockp2p.NewTestP2P(t)),
				WithInitialSync(&mockSync.Sync{IsSyncing: false}),
				WithChainService(chainService),
				WithOperationNotifier(chainService.OperationNotifier()),
			},
			setupSvc: func(s *Service, msg *ethpb.SyncCommitteeMessage, topic string) (*Service, string, *startup.Clock) {
				s.cfg.stateGen = stategen.New(beaconDB, doublylinkedtree.New())
				s.cfg.beaconDB = beaconDB
				s.initCaches()
				return s, topic, startup.NewClock(time.Now(), [32]byte{})
			},
			args: args{
				pid:   "random",
				topic: fmt.Sprintf(defaultTopic, fakeDigest, 0),
				msg: &ethpb.SyncCommitteeMessage{
					Slot:           10,
					ValidatorIndex: 1,
					BlockRoot:      params.BeaconConfig().ZeroHash[:],
					Signature:      emptySig[:],
				}},
			want: pubsub.ValidationIgnore,
		},
		{
			name: "Already Seen Message",
			svcopts: []Option{
				WithP2P(mockp2p.NewTestP2P(t)),
				WithInitialSync(&mockSync.Sync{IsSyncing: false}),
				WithChainService(chainService),
				WithOperationNotifier(chainService.OperationNotifier()),
			},
			setupSvc: func(s *Service, msg *ethpb.SyncCommitteeMessage, topic string) (*Service, string, *startup.Clock) {
				s.cfg.stateGen = stategen.New(beaconDB, doublylinkedtree.New())
				s.cfg.beaconDB = beaconDB
				s.initCaches()
				m := &ethpb.SyncCommitteeMessage{
					Slot:           1,
					ValidatorIndex: 1,
					BlockRoot:      params.BeaconConfig().ZeroHash[:],
				}
				s.setSeenSyncMessageIndexSlot(m, 0)
				return s, topic, startup.NewClock(time.Now(), [32]byte{})
			},
			args: args{
				pid:   "random",
				topic: fmt.Sprintf(defaultTopic, fakeDigest, 0),
				msg: &ethpb.SyncCommitteeMessage{
					Slot:           1,
					ValidatorIndex: 1,
					BlockRoot:      params.BeaconConfig().ZeroHash[:],
					Signature:      emptySig[:],
				}},
			want: pubsub.ValidationIgnore,
		},
		{
			name: "Non-existent block root",
			svcopts: []Option{
				WithP2P(mockp2p.NewTestP2P(t)),
				WithInitialSync(&mockSync.Sync{IsSyncing: false}),
				WithChainService(chainService),
				WithOperationNotifier(chainService.OperationNotifier()),
			},
			setupSvc: func(s *Service, msg *ethpb.SyncCommitteeMessage, topic string) (*Service, string, *startup.Clock) {
				s.cfg.stateGen = stategen.New(beaconDB, doublylinkedtree.New())
				s.cfg.beaconDB = beaconDB
				s.initCaches()
				s.cfg.chain = &mockChain.ChainService{Genesis: time.Now()}
				incorrectRoot := [32]byte{0xBB}
				msg.BlockRoot = incorrectRoot[:]

				gt := time.Now().Add(-time.Second * time.Duration(params.BeaconConfig().SecondsPerSlot) * time.Duration(10))
				return s, topic, startup.NewClock(gt, [32]byte{'A'})
			},
			args: args{
				pid:   "random",
				topic: fmt.Sprintf(defaultTopic, fakeDigest, 0),
				msg: &ethpb.SyncCommitteeMessage{
					Slot:           1,
					ValidatorIndex: 1,
					BlockRoot:      params.BeaconConfig().ZeroHash[:],
					Signature:      emptySig[:],
				}},
			want: pubsub.ValidationIgnore,
		},
		{
			name: "Subnet is non-existent",
			svcopts: []Option{
				WithP2P(mockp2p.NewTestP2P(t)),
				WithInitialSync(&mockSync.Sync{IsSyncing: false}),
				WithChainService(chainService),
				WithOperationNotifier(chainService.OperationNotifier()),
			},
			setupSvc: func(s *Service, msg *ethpb.SyncCommitteeMessage, topic string) (*Service, string, *startup.Clock) {
				s.cfg.stateGen = stategen.New(beaconDB, doublylinkedtree.New())
				s.cfg.beaconDB = beaconDB
				s.initCaches()
				msg.BlockRoot = headRoot[:]
				hState, err := beaconDB.State(t.Context(), headRoot)
				assert.NoError(t, err)
				s.cfg.chain = &mockChain.ChainService{
					SyncCommitteeIndices: []primitives.CommitteeIndex{0},
					Genesis:              time.Now(),
				}
				numOfVals := hState.NumValidators()

				chosenVal := numOfVals - 10
				msg.Signature = emptySig[:]
				msg.BlockRoot = headRoot[:]
				msg.ValidatorIndex = primitives.ValidatorIndex(chosenVal)
				msg.Slot = slots.PrevSlot(hState.Slot())

				gt := time.Now().Add(-time.Second * time.Duration(params.BeaconConfig().SecondsPerSlot) * time.Duration(slots.PrevSlot(hState.Slot())))
				vr := [32]byte{'A'}
				clock := startup.NewClock(gt, vr)
				digest := params.ForkDigest(slots.ToEpoch(clock.CurrentSlot()))
				actualTopic := fmt.Sprintf(defaultTopic, digest, 5)

				return s, actualTopic, clock
			},
			args: args{
				pid:   "random",
				topic: defaultTopic,
				msg: &ethpb.SyncCommitteeMessage{
					Slot:           1,
					ValidatorIndex: 1,
					BlockRoot:      params.BeaconConfig().ZeroHash[:],
					Signature:      emptySig[:],
				}},
			want: pubsub.ValidationReject,
		},
		{
			name: "Validator is non-existent",
			svcopts: []Option{
				WithP2P(mockp2p.NewTestP2P(t)),
				WithInitialSync(&mockSync.Sync{IsSyncing: false}),
				WithChainService(chainService),
				WithOperationNotifier(chainService.OperationNotifier()),
			},
			setupSvc: func(s *Service, msg *ethpb.SyncCommitteeMessage, topic string) (*Service, string, *startup.Clock) {
				s.cfg.stateGen = stategen.New(beaconDB, doublylinkedtree.New())
				s.cfg.beaconDB = beaconDB
				s.initCaches()
				msg.BlockRoot = headRoot[:]
				hState, err := beaconDB.State(t.Context(), headRoot)
				assert.NoError(t, err)
				s.cfg.chain = &mockChain.ChainService{
					Genesis: time.Now(),
				}

				numOfVals := hState.NumValidators()

				chosenVal := numOfVals + 10
				msg.Signature = emptySig[:]
				msg.BlockRoot = headRoot[:]
				msg.ValidatorIndex = primitives.ValidatorIndex(chosenVal)
				msg.Slot = slots.PrevSlot(hState.Slot())

				gt := time.Now().Add(-time.Second * time.Duration(params.BeaconConfig().SecondsPerSlot) * time.Duration(slots.PrevSlot(hState.Slot())))
				vr := [32]byte{'A'}
				clock := startup.NewClock(gt, vr)
				digest := params.ForkDigest(clock.CurrentEpoch())
				actualTopic := fmt.Sprintf(defaultTopic, digest, 5)

				return s, actualTopic, startup.NewClock(gt, vr)
			},
			args: args{
				pid:   "random",
				topic: defaultTopic,
				msg: &ethpb.SyncCommitteeMessage{
					Slot:           1,
					ValidatorIndex: 1,
					BlockRoot:      params.BeaconConfig().ZeroHash[:],
					Signature:      emptySig[:],
				}},
			want: pubsub.ValidationIgnore,
		},
		{
			name: "Invalid Sync Committee Signature",
			svcopts: []Option{
				WithP2P(mockp2p.NewTestP2P(t)),
				WithInitialSync(&mockSync.Sync{IsSyncing: false}),
				WithChainService(chainService),
				WithOperationNotifier(chainService.OperationNotifier()),
			},
			setupSvc: func(s *Service, msg *ethpb.SyncCommitteeMessage, topic string) (*Service, string, *startup.Clock) {
				s.cfg.stateGen = stategen.New(beaconDB, doublylinkedtree.New())
				s.cfg.beaconDB = beaconDB
				s.initCaches()
				msg.BlockRoot = headRoot[:]
				hState, err := beaconDB.State(t.Context(), headRoot)
				assert.NoError(t, err)

				numOfVals := hState.NumValidators()

				chosenVal := numOfVals - 10
				msg.Signature = emptySig[:]
				msg.BlockRoot = headRoot[:]
				msg.ValidatorIndex = primitives.ValidatorIndex(chosenVal)
				msg.Slot = slots.PrevSlot(hState.Slot())

				d, err := signing.Domain(hState.Fork(), slots.ToEpoch(hState.Slot()), params.BeaconConfig().DomainSyncCommittee, hState.GenesisValidatorsRoot())
				assert.NoError(t, err)
				subCommitteeSize := params.BeaconConfig().SyncCommitteeSize / params.BeaconConfig().SyncCommitteeSubnetCount
				s.cfg.chain = &mockChain.ChainService{
					SyncCommitteeIndices: []primitives.CommitteeIndex{primitives.CommitteeIndex(subCommitteeSize)},
					SyncCommitteeDomain:  d,
					PublicKey:            bytesutil.ToBytes48(keys[chosenVal].PublicKey().Marshal()),
					Genesis:              time.Now(),
				}

				// Set Topic and Subnet
				gt := time.Now().Add(-time.Second * time.Duration(params.BeaconConfig().SecondsPerSlot) * time.Duration(slots.PrevSlot(hState.Slot())))
				vr := [32]byte{'A'}
				clock := startup.NewClock(gt, vr)
				digest := params.ForkDigest(slots.ToEpoch(clock.CurrentSlot()))
				actualTopic := fmt.Sprintf(defaultTopic, digest, 5)

				return s, actualTopic, startup.NewClock(gt, vr)
			},
			args: args{
				pid:   "random",
				topic: defaultTopic,
				msg: &ethpb.SyncCommitteeMessage{
					Slot:           1,
					ValidatorIndex: 1,
					BlockRoot:      params.BeaconConfig().ZeroHash[:],
					Signature:      emptySig[:],
				}},
			want: pubsub.ValidationReject,
		},
		{
			name: "Valid Sync Committee Signature",
			svcopts: []Option{
				WithP2P(mockp2p.NewTestP2P(t)),
				WithInitialSync(&mockSync.Sync{IsSyncing: false}),
				WithChainService(chainService),
				WithOperationNotifier(chainService.OperationNotifier()),
			},
			setupSvc: func(s *Service, msg *ethpb.SyncCommitteeMessage, topic string) (*Service, string, *startup.Clock) {
				s.cfg.stateGen = stategen.New(beaconDB, doublylinkedtree.New())
				s.cfg.beaconDB = beaconDB
				s.initCaches()
				msg.BlockRoot = headRoot[:]
				hState, err := beaconDB.State(t.Context(), headRoot)
				assert.NoError(t, err)
				subCommitteeSize := params.BeaconConfig().SyncCommitteeSize / params.BeaconConfig().SyncCommitteeSubnetCount

				numOfVals := hState.NumValidators()

				chosenVal := numOfVals - 10
				d, err := signing.Domain(hState.Fork(), slots.ToEpoch(hState.Slot()), params.BeaconConfig().DomainSyncCommittee, hState.GenesisValidatorsRoot())
				assert.NoError(t, err)
				rawBytes := p2ptypes.SSZBytes(headRoot[:])
				sigRoot, err := signing.ComputeSigningRoot(&rawBytes, d)
				assert.NoError(t, err)

				s.cfg.chain = &mockChain.ChainService{
					SyncCommitteeIndices: []primitives.CommitteeIndex{primitives.CommitteeIndex(subCommitteeSize)},
					SyncCommitteeDomain:  d,
					PublicKey:            bytesutil.ToBytes48(keys[chosenVal].PublicKey().Marshal()),
					Genesis:              time.Now(),
				}

				msg.Signature = keys[chosenVal].Sign(sigRoot[:]).Marshal()
				msg.BlockRoot = headRoot[:]
				msg.ValidatorIndex = primitives.ValidatorIndex(chosenVal)
				msg.Slot = slots.PrevSlot(hState.Slot())

				// Set Topic and Subnet
				gt := time.Now().Add(-time.Second * time.Duration(params.BeaconConfig().SecondsPerSlot) * time.Duration(slots.PrevSlot(hState.Slot())))
				vr := [32]byte{'A'}
				clock := startup.NewClock(gt, vr)
				digest := params.ForkDigest(slots.ToEpoch(clock.CurrentSlot()))
				actualTopic := fmt.Sprintf(defaultTopic, digest, 1)

				return s, actualTopic, startup.NewClock(gt, vr)
			},
			args: args{
				pid:   "random",
				topic: defaultTopic,
				msg: &ethpb.SyncCommitteeMessage{
					Slot:           1,
					ValidatorIndex: 1,
					BlockRoot:      params.BeaconConfig().ZeroHash[:],
					Signature:      emptySig[:],
				}},
			want: pubsub.ValidationAccept,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ctx := t.Context()
			ctx, cancel := context.WithCancel(ctx)
			defer cancel()

			cw := startup.NewClockSynchronizer()
			opts := []Option{WithClockWaiter(cw), WithStateNotifier(chainService.StateNotifier())}
			svc := NewService(ctx, append(opts, tt.svcopts...)...)
			var clock *startup.Clock
			svc, tt.args.topic, clock = tt.setupSvc(svc, tt.args.msg, tt.args.topic)
			markInitSyncComplete(t, svc)
			require.NoError(t, cw.SetClock(clock))
			svc.verifierWaiter = verification.NewInitializerWaiter(cw, chainService.ForkChoiceStore, svc.cfg.stateGen, chainService)
			go svc.Start()

			marshalledObj, err := tt.args.msg.MarshalSSZ()
			assert.NoError(t, err)
			marshalledObj = snappy.Encode(nil, marshalledObj)
			msg := &pubsub.Message{
				Message: &pubsubpb.Message{
					Data:  marshalledObj,
					Topic: &tt.args.topic,
				},
				ReceivedFrom:  "",
				ValidatorData: nil,
			}
			for range 10 {
				if !svc.chainIsStarted() {
					time.Sleep(100 * time.Millisecond)
				}
			}
			require.Equal(t, true, svc.chainIsStarted())
			if got, err := svc.validateSyncCommitteeMessage(ctx, tt.args.pid, msg); got != tt.want {
				_ = err
				t.Errorf("validateSyncCommitteeMessage() = %v, want %v", got, tt.want)
			}
		})
	}
}

func TestService_ignoreHasSeenSyncMsg(t *testing.T) {
	tests := []struct {
		name      string
		setupSvc  func(s *Service, msg *ethpb.SyncCommitteeMessage, topic string) (*Service, string)
		msg       *ethpb.SyncCommitteeMessage
		committee []primitives.CommitteeIndex
		want      pubsub.ValidationResult
	}{
		{
			name: "has seen",
			setupSvc: func(s *Service, msg *ethpb.SyncCommitteeMessage, topic string) (*Service, string) {
				s.initCaches()
				m := &ethpb.SyncCommitteeMessage{
					Slot:      1,
					BlockRoot: params.BeaconConfig().ZeroHash[:],
				}
				s.setSeenSyncMessageIndexSlot(m, 0)
				return s, ""
			},
			msg: &ethpb.SyncCommitteeMessage{ValidatorIndex: 0, Slot: 1,
				BlockRoot: params.BeaconConfig().ZeroHash[:]},
			committee: []primitives.CommitteeIndex{1, 2, 3},
			want:      pubsub.ValidationIgnore,
		},
		{
			name: "has not seen",
			setupSvc: func(s *Service, msg *ethpb.SyncCommitteeMessage, topic string) (*Service, string) {
				s.initCaches()
				m := &ethpb.SyncCommitteeMessage{
					Slot:      1,
					BlockRoot: params.BeaconConfig().ZeroHash[:],
				}
				s.setSeenSyncMessageIndexSlot(m, 0)
				return s, ""
			},
			msg: &ethpb.SyncCommitteeMessage{ValidatorIndex: 1, Slot: 1,
				BlockRoot: bytesutil.PadTo([]byte{'A'}, 32)},
			committee: []primitives.CommitteeIndex{1, 2, 3},
			want:      pubsub.ValidationAccept,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			s := &Service{
				cfg: &config{chain: &mockChain.ChainService{}},
			}
			s, _ = tt.setupSvc(s, tt.msg, "")
			f := s.ignoreHasSeenSyncMsg(t.Context(), tt.msg, tt.committee)
			result, err := f(t.Context())
			_ = err
			require.Equal(t, tt.want, result)
		})
	}
}

func TestService_rejectIncorrectSyncCommittee(t *testing.T) {
	tests := []struct {
		name             string
		cfg              *config
		setupTopic       func(s *Service) string
		committeeIndices []primitives.CommitteeIndex
		want             pubsub.ValidationResult
	}{
		{
			name: "invalid",
			cfg: &config{
				chain: &mockChain.ChainService{},
				clock: startup.NewClock(time.Now(), [32]byte{1}),
			},
			committeeIndices: []primitives.CommitteeIndex{0},
			setupTopic: func(_ *Service) string {
				return "foobar"
			},
			want: pubsub.ValidationReject,
		},
		{
			name: "valid",
			cfg: &config{
				chain: &mockChain.ChainService{},
				clock: startup.NewClock(time.Now(), [32]byte{1}),
			},
			committeeIndices: []primitives.CommitteeIndex{0},
			setupTopic: func(s *Service) string {
				format := p2p.GossipTypeMapping[reflect.TypeFor[*ethpb.SyncCommitteeMessage]()]

				digest, err := s.currentForkDigest()
				require.NoError(t, err)
				prefix := fmt.Sprintf(format, digest, 0 /* validator index 0 */)
				topic := prefix + "foobar"
				return topic
			},
			want: pubsub.ValidationAccept,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			s := &Service{
				cfg: tt.cfg,
			}
			topic := tt.setupTopic(s)
			f := s.rejectIncorrectSyncCommittee(tt.committeeIndices, topic)
			result, err := f(t.Context())
			_ = err
			require.Equal(t, tt.want, result)
		})
	}
}

func Test_ignoreEmptyCommittee(t *testing.T) {
	tests := []struct {
		name      string
		committee []primitives.CommitteeIndex
		want      pubsub.ValidationResult
	}{
		{
			name:      "nil",
			committee: nil,
			want:      pubsub.ValidationIgnore,
		},
		{
			name:      "empty",
			committee: []primitives.CommitteeIndex{},
			want:      pubsub.ValidationIgnore,
		},
		{
			name:      "non-empty",
			committee: []primitives.CommitteeIndex{1},
			want:      pubsub.ValidationAccept,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			f := ignoreEmptyCommittee(tt.committee)
			result, err := f(t.Context())
			_ = err
			require.Equal(t, tt.want, result)
		})
	}
}
