package verification

import (
	"context"
	"testing"

	"github.com/pkg/errors"
	"github.com/prysmaticlabs/prysm/v5/consensus-types/blocks"
	"github.com/prysmaticlabs/prysm/v5/encoding/bytesutil"
	"github.com/prysmaticlabs/prysm/v5/testing/util"
	"github.com/stretchr/testify/require"
)

func TestBatchVerifier(t *testing.T) {
	ctx := context.Background()
	mockCV := func(err error) roblobCommitmentVerifier {
		return func(...blocks.ROBlob) error {
			return err
		}
	}
	var invCmtErr = errors.New("mock invalid commitment")
	type vbcbt func() (blocks.VerifiedROBlob, error)
	vbcb := func(bl blocks.ROBlob, err error) vbcbt {
		return func() (blocks.VerifiedROBlob, error) {
			return blocks.VerifiedROBlob{ROBlob: bl}, err
		}
	}
	cases := []struct {
		name   string
		nv     func() NewBlobVerifier
		cv     roblobCommitmentVerifier
		bandb  func(t *testing.T, n int) (blocks.ROBlock, []blocks.ROBlob)
		err    error
		nblobs int
		reqs   []Requirement
	}{
		{
			name: "no blobs",
			bandb: func(t *testing.T, nb int) (blocks.ROBlock, []blocks.ROBlob) {
				return util.GenerateTestDenebBlockWithSidecar(t, [32]byte{}, 0, nb)
			},
			nv: func() NewBlobVerifier {
				return func(bl blocks.ROBlob, reqs []Requirement) BlobVerifier {
					return &MockBlobVerifier{cbVerifiedROBlob: vbcb(bl, nil)}
				}
			},
			nblobs: 0,
		},
		{
			name: "happy path",
			nv: func() NewBlobVerifier {
				return func(bl blocks.ROBlob, reqs []Requirement) BlobVerifier {
					return &MockBlobVerifier{cbVerifiedROBlob: vbcb(bl, nil)}
				}
			},
			bandb: func(t *testing.T, nb int) (blocks.ROBlock, []blocks.ROBlob) {
				return util.GenerateTestDenebBlockWithSidecar(t, [32]byte{}, 0, nb)
			},
			nblobs: 3,
		},
		{
			name: "partial batch",
			nv: func() NewBlobVerifier {
				return func(bl blocks.ROBlob, reqs []Requirement) BlobVerifier {
					return &MockBlobVerifier{cbVerifiedROBlob: vbcb(bl, nil)}
				}
			},
			bandb: func(t *testing.T, nb int) (blocks.ROBlock, []blocks.ROBlob) {
				// Add extra blobs to the block that we won't return
				blk, blbs := util.GenerateTestDenebBlockWithSidecar(t, [32]byte{}, 0, nb+3)
				return blk, blbs[0:3]
			},
			nblobs: 3,
		},
		{
			name: "invalid commitment",
			nv: func() NewBlobVerifier {
				return func(bl blocks.ROBlob, reqs []Requirement) BlobVerifier {
					return &MockBlobVerifier{cbVerifiedROBlob: func() (blocks.VerifiedROBlob, error) {
						t.Fatal("Batch verifier should stop before this point")
						return blocks.VerifiedROBlob{}, nil
					}}
				}
			},
			cv: mockCV(invCmtErr),
			bandb: func(t *testing.T, nb int) (blocks.ROBlock, []blocks.ROBlob) {
				return util.GenerateTestDenebBlockWithSidecar(t, [32]byte{}, 0, nb)
			},
			err:    invCmtErr,
			nblobs: 1,
		},
		{
			name: "signature mismatch",
			nv: func() NewBlobVerifier {
				return func(bl blocks.ROBlob, reqs []Requirement) BlobVerifier {
					return &MockBlobVerifier{cbVerifiedROBlob: func() (blocks.VerifiedROBlob, error) {
						t.Fatal("Batch verifier should stop before this point")
						return blocks.VerifiedROBlob{}, nil
					}}
				}
			},
			bandb: func(t *testing.T, nb int) (blocks.ROBlock, []blocks.ROBlob) {
				blk, blbs := util.GenerateTestDenebBlockWithSidecar(t, [32]byte{}, 0, nb)
				blbs[0].SignedBlockHeader.Signature = []byte("wrong")
				return blk, blbs
			},
			err:    ErrBatchSignatureMismatch,
			nblobs: 2,
		},
		{
			name: "root mismatch",
			nv: func() NewBlobVerifier {
				return func(bl blocks.ROBlob, reqs []Requirement) BlobVerifier {
					return &MockBlobVerifier{cbVerifiedROBlob: func() (blocks.VerifiedROBlob, error) {
						t.Fatal("Batch verifier should stop before this point")
						return blocks.VerifiedROBlob{}, nil
					}}
				}
			},
			bandb: func(t *testing.T, nb int) (blocks.ROBlock, []blocks.ROBlob) {
				blk, blbs := util.GenerateTestDenebBlockWithSidecar(t, [32]byte{}, 0, nb)
				wr, err := blocks.NewROBlobWithRoot(blbs[0].BlobSidecar, bytesutil.ToBytes32([]byte("wrong")))
				require.NoError(t, err)
				blbs[0] = wr
				return blk, blbs
			},
			err:    ErrBatchBlockRootMismatch,
			nblobs: 1,
		},
		{
			name: "idx oob",
			nv: func() NewBlobVerifier {
				return func(bl blocks.ROBlob, reqs []Requirement) BlobVerifier {
					return &MockBlobVerifier{
						ErrBlobIndexInBounds: ErrBlobIndexInvalid,
						cbVerifiedROBlob: func() (blocks.VerifiedROBlob, error) {
							t.Fatal("Batch verifier should stop before this point")
							return blocks.VerifiedROBlob{}, nil
						}}
				}
			},
			bandb: func(t *testing.T, nb int) (blocks.ROBlock, []blocks.ROBlob) {
				return util.GenerateTestDenebBlockWithSidecar(t, [32]byte{}, 0, nb)
			},
			nblobs: 1,
			err:    ErrBlobIndexInvalid,
		},
		{
			name: "inclusion proof invalid",
			nv: func() NewBlobVerifier {
				return func(bl blocks.ROBlob, reqs []Requirement) BlobVerifier {
					return &MockBlobVerifier{
						ErrSidecarInclusionProven: ErrSidecarInclusionProofInvalid,
						cbVerifiedROBlob: func() (blocks.VerifiedROBlob, error) {
							t.Fatal("Batch verifier should stop before this point")
							return blocks.VerifiedROBlob{}, nil
						}}
				}
			},
			bandb: func(t *testing.T, nb int) (blocks.ROBlock, []blocks.ROBlob) {
				return util.GenerateTestDenebBlockWithSidecar(t, [32]byte{}, 0, nb)
			},
			nblobs: 1,
			err:    ErrSidecarInclusionProofInvalid,
		},
	}
	for _, c := range cases {
		t.Run(c.name, func(t *testing.T) {
			blk, blbs := c.bandb(t, c.nblobs)
			reqs := c.reqs
			if reqs == nil {
				reqs = InitsyncSidecarRequirements
			}
			bbv := NewBlobBatchVerifier(c.nv(), reqs)
			if c.cv == nil {
				bbv.verifyKzg = mockCV(nil)
			} else {
				bbv.verifyKzg = c.cv
			}
			vb, err := bbv.VerifiedROBlobs(ctx, blk, blbs)
			if c.err != nil {
				require.ErrorIs(t, err, c.err)
				return
			}
			require.NoError(t, err)
			require.Equal(t, c.nblobs, len(vb))
		})
	}
}
