package sync

import (
	"testing"

	"github.com/OffchainLabs/prysm/v7/beacon-chain/p2p"
	"github.com/OffchainLabs/prysm/v7/beacon-chain/startup"
	"github.com/OffchainLabs/prysm/v7/config/params"
	"github.com/OffchainLabs/prysm/v7/consensus-types/blocks"
	types "github.com/OffchainLabs/prysm/v7/consensus-types/primitives"
	"github.com/OffchainLabs/prysm/v7/genesis"
	ethpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1"
	"github.com/OffchainLabs/prysm/v7/testing/require"
	"github.com/OffchainLabs/prysm/v7/testing/util"
)

func (c *blobsTestCase) defaultOldestSlotByRange(t *testing.T) types.Slot {
	currentEpoch := c.clock.CurrentEpoch()
	oldestEpoch := max(currentEpoch-params.BeaconConfig().MinEpochsForBlobsSidecarsRequest, params.BeaconConfig().DenebForkEpoch)
	oldestSlot := util.SlotAtEpoch(t, oldestEpoch)
	return oldestSlot
}

func blobRangeRequestFromSidecars(scs []blocks.ROBlob) any {
	maxBlobs := params.BeaconConfig().MaxBlobsPerBlock(scs[0].Slot())
	count := uint64(len(scs) / maxBlobs)
	return &ethpb.BlobSidecarsByRangeRequest{
		StartSlot: scs[0].Slot(),
		Count:     count,
	}
}

func (c *blobsTestCase) filterExpectedByRange(t *testing.T, scs []blocks.ROBlob, req any) []*expectedBlobChunk {
	var expect []*expectedBlobChunk
	blockOffset := 0
	lastRoot := scs[0].BlockRoot()
	rreq, ok := req.(*ethpb.BlobSidecarsByRangeRequest)
	require.Equal(t, true, ok)
	var writes uint64
	for i := range scs {
		sc := scs[i]
		root := sc.BlockRoot()
		if root != lastRoot {
			blockOffset += 1
		}
		lastRoot = root

		if sc.Slot() < c.oldestSlot(t) {
			continue
		}
		if sc.Slot() < rreq.StartSlot || sc.Slot() > rreq.StartSlot+types.Slot(rreq.Count)-1 {
			continue
		}
		if writes == params.BeaconConfig().MaxRequestBlobSidecars {
			continue
		}
		expect = append(expect, &expectedBlobChunk{
			sidecar: &sc,
			code:    responseCodeSuccess,
			message: "",
		})
		writes += 1
	}
	return expect
}

func (c *blobsTestCase) runTestBlobSidecarsByRange(t *testing.T) {
	if c.serverHandle == nil {
		c.serverHandle = func(s *Service) rpcHandler { return s.blobSidecarsByRangeRPCHandler }
	}
	if c.defineExpected == nil {
		c.defineExpected = c.filterExpectedByRange
	}
	if c.requestFromSidecars == nil {
		c.requestFromSidecars = blobRangeRequestFromSidecars
	}
	if c.topic == "" {
		c.topic = p2p.RPCBlobSidecarsByRangeTopicV1
	}
	if c.oldestSlot == nil {
		c.oldestSlot = c.defaultOldestSlotByRange
	}
	if c.streamReader == nil {
		c.streamReader = defaultExpectedRequirer
	}
	c.run(t)
}

func TestBlobByRangeOK(t *testing.T) {
	params.SetupTestConfigCleanup(t)
	ds := util.SlotAtEpoch(t, params.BeaconConfig().DenebForkEpoch)
	params.BeaconConfig().InitializeForkSchedule()
	retainSlots := util.SlotAtEpoch(t, params.BeaconConfig().MinEpochsForBlobsSidecarsRequest)
	current := ds + retainSlots
	cases := []*blobsTestCase{
		{
			name:    "beginning of window + 10",
			nblocks: 10,
		},
		{
			name:    "10 slots before window, 10 slots after, count = 20",
			nblocks: 10,
			requestFromSidecars: func(scs []blocks.ROBlob) any {
				return &ethpb.BlobSidecarsByRangeRequest{
					StartSlot: scs[0].Slot() - 10,
					Count:     20,
				}
			},
		},
		{
			name:    "request before window, empty response",
			nblocks: 10,
			requestFromSidecars: func(scs []blocks.ROBlob) any {
				return &ethpb.BlobSidecarsByRangeRequest{
					StartSlot: scs[0].Slot() - 10,
					Count:     10,
				}
			},
			total: func() *int { x := 0; return &x }(),
		},
		{
			name:    "10 blocks * 4 blobs = 40",
			nblocks: 10,
			requestFromSidecars: func(scs []blocks.ROBlob) any {
				return &ethpb.BlobSidecarsByRangeRequest{
					StartSlot: scs[0].Slot() - 10,
					Count:     20,
				}
			},
			total: func() *int { x := params.BeaconConfig().MaxBlobsPerBlock(ds) * 10; return &x }(), // 10 blocks * 4 blobs = 40
		},
		{
			name:    "when request count > MAX_REQUEST_BLOCKS_DENEB, MAX_REQUEST_BLOBS_SIDECARS sidecars in response",
			nblocks: int(params.BeaconConfig().MaxRequestBlocksDeneb) + 1,
			requestFromSidecars: func(scs []blocks.ROBlob) any {
				return &ethpb.BlobSidecarsByRangeRequest{
					StartSlot: scs[0].Slot(),
					Count:     params.BeaconConfig().MaxRequestBlocksDeneb + 1,
				}
			},
			total: func() *int { x := int(params.BeaconConfig().MaxRequestBlobSidecars); return &x }(),
		},
	}
	clock := startup.NewClock(genesis.Time(), genesis.ValidatorsRoot(), startup.WithSlotAsNow(current))
	for _, c := range cases {
		c.clock = clock
		t.Run(c.name, func(t *testing.T) {
			c.runTestBlobSidecarsByRange(t)
		})
	}
}

func TestBlobsByRangeValidation(t *testing.T) {
	params.SetupTestConfigCleanup(t)
	repositionFutureEpochs(params.BeaconConfig())
	denebSlot := util.SlotAtEpoch(t, params.BeaconConfig().DenebForkEpoch)

	minReqEpochs := params.BeaconConfig().MinEpochsForBlobsSidecarsRequest
	minReqSlots := util.SlotAtEpoch(t, minReqEpochs)
	// spec criteria for mix,max bound checking
	/*
		Clients MUST keep a record of signed blobs sidecars seen on the epoch range
		[max(current_epoch - MIN_EPOCHS_FOR_BLOB_SIDECARS_REQUESTS, DENEB_FORK_EPOCH), current_epoch]
		where current_epoch is defined by the current wall-clock time,
		and clients MUST support serving requests of blobs on this range.
	*/
	defaultCurrent := denebSlot + 100 + minReqSlots
	defaultMinStart, err := BlobRPCMinValidSlot(defaultCurrent)
	require.NoError(t, err)
	cases := []struct {
		name    string
		current types.Slot
		req     *ethpb.BlobSidecarsByRangeRequest

		start types.Slot
		end   types.Slot
		batch uint64
		err   error
	}{
		{
			name:    "start at current",
			current: denebSlot + 100,
			req: &ethpb.BlobSidecarsByRangeRequest{
				StartSlot: denebSlot + 100,
				Count:     10,
			},
			start: denebSlot + 100,
			end:   denebSlot + 100,
			batch: 10,
		},
		{
			name:    "start after current",
			current: denebSlot,
			req: &ethpb.BlobSidecarsByRangeRequest{
				StartSlot: denebSlot + 100,
				Count:     10,
			},
			start: denebSlot,
			end:   denebSlot,
			batch: 0,
		},
		{
			name:    "start before current_epoch - MIN_EPOCHS_FOR_BLOB_SIDECARS_REQUESTS",
			current: defaultCurrent,
			req: &ethpb.BlobSidecarsByRangeRequest{
				StartSlot: defaultMinStart - 100,
				Count:     10,
			},
			start: defaultMinStart,
			end:   defaultMinStart,
			batch: 10,
		},
		{
			name:    "start before current_epoch - MIN_EPOCHS_FOR_BLOB_SIDECARS_REQUESTS - end still valid",
			current: defaultCurrent,
			req: &ethpb.BlobSidecarsByRangeRequest{
				StartSlot: defaultMinStart - 10,
				Count:     20,
			},
			start: defaultMinStart,
			end:   defaultMinStart + 9,
			batch: blobBatchLimit(defaultCurrent),
		},
		{
			name:    "count > MAX_REQUEST_BLOB_SIDECARS",
			current: defaultCurrent,
			req: &ethpb.BlobSidecarsByRangeRequest{
				StartSlot: defaultMinStart - 10,
				Count:     1000,
			},
			start: defaultMinStart,
			end:   defaultMinStart - 10 + 999,
			// a large count is ok, we just limit the amount of actual responses
			batch: blobBatchLimit(defaultCurrent),
		},
		{
			name:    "start + count > current",
			current: defaultCurrent,
			req: &ethpb.BlobSidecarsByRangeRequest{
				StartSlot: defaultCurrent + 100,
				Count:     100,
			},
			start: defaultCurrent,
			end:   defaultCurrent,
			batch: 0,
		},
		{
			name:    "start before deneb",
			current: defaultCurrent - minReqSlots + 100,
			req: &ethpb.BlobSidecarsByRangeRequest{
				StartSlot: denebSlot - 10,
				Count:     100,
			},
			start: denebSlot,
			end:   denebSlot + 89,
			batch: blobBatchLimit(defaultCurrent - minReqSlots + 100),
		},
	}
	for _, c := range cases {
		t.Run(c.name, func(t *testing.T) {
			rp, err := validateBlobsByRange(c.req, c.current)
			if c.err != nil {
				require.ErrorIs(t, err, c.err)
				return
			} else {
				require.NoError(t, err)
			}
			require.Equal(t, c.start, rp.start)
			require.Equal(t, c.end, rp.end)
			require.Equal(t, c.batch, rp.size)
		})
	}
}

func TestBlobRPCMinValidSlot(t *testing.T) {
	denebSlot := util.SlotAtEpoch(t, params.BeaconConfig().DenebForkEpoch)
	cases := []struct {
		name     string
		current  func(t *testing.T) types.Slot
		expected types.Slot
		err      error
	}{
		{
			name: "before deneb",
			current: func(t *testing.T) types.Slot {
				st := util.SlotAtEpoch(t, params.BeaconConfig().DenebForkEpoch-1)
				// note: we no longer need to deal with deneb fork epoch being far future
				return st
			},
			expected: denebSlot,
		},
		{
			name: "equal to deneb",
			current: func(t *testing.T) types.Slot {
				st := util.SlotAtEpoch(t, params.BeaconConfig().DenebForkEpoch)
				// note: we no longer need to deal with deneb fork epoch being far future
				return st
			},
			expected: denebSlot,
		},
		{
			name: "after deneb, before expiry starts",
			current: func(t *testing.T) types.Slot {
				st := util.SlotAtEpoch(t, params.BeaconConfig().DenebForkEpoch+params.BeaconConfig().MinEpochsForBlobsSidecarsRequest)
				// note: we no longer need to deal with deneb fork epoch being far future
				return st
			},
			expected: denebSlot,
		},
		{
			name: "expiry starts one epoch after deneb + MIN_EPOCHS_FOR_BLOB_SIDECARS_REQUESTS",
			current: func(t *testing.T) types.Slot {
				st := util.SlotAtEpoch(t, params.BeaconConfig().DenebForkEpoch+params.BeaconConfig().MinEpochsForBlobsSidecarsRequest+1)
				// note: we no longer need to deal with deneb fork epoch being far future
				return st
			},
			expected: denebSlot + params.BeaconConfig().SlotsPerEpoch,
		},
	}
	for _, c := range cases {
		t.Run(c.name, func(t *testing.T) {
			current := c.current(t)
			got, err := BlobRPCMinValidSlot(current)
			if c.err != nil {
				require.ErrorIs(t, err, c.err)
				return
			}
			require.NoError(t, err)
			require.Equal(t, c.expected, got)
		})
	}
}
