package types_test

import (
	"bytes"
	"encoding/binary"
	"testing"

	sdkerrors "cosmossdk.io/errors"
	"github.com/celestiaorg/celestia-app/v6/pkg/appconsts"
	"github.com/celestiaorg/celestia-app/v6/test/util/random"
	"github.com/celestiaorg/celestia-app/v6/test/util/testfactory"
	"github.com/celestiaorg/celestia-app/v6/test/util/testnode"
	"github.com/celestiaorg/celestia-app/v6/x/blob/types"
	"github.com/celestiaorg/go-square/v3/inclusion"
	"github.com/celestiaorg/go-square/v3/share"
	"github.com/cometbft/cometbft/crypto/merkle"
	"github.com/cosmos/cosmos-sdk/codec"
	sdk "github.com/cosmos/cosmos-sdk/types"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestMsgTypeURLParity(t *testing.T) {
	require.Equal(t, sdk.MsgTypeURL(&types.MsgPayForBlobs{}), types.URLMsgPayForBlobs)
}

func TestLegacyAminoCodec(t *testing.T) {
	// Create a legacy amino codec
	cdc := codec.NewLegacyAmino()
	types.RegisterLegacyAminoCodec(cdc)
	sdk.RegisterLegacyAminoCodec(cdc)

	// Create a valid MsgPayForBlobs
	validMsg := validMsgPayForBlobs(t)

	// Test amino encoding/decoding
	bz, err := cdc.Marshal(validMsg)
	require.NoError(t, err)

	var decoded types.MsgPayForBlobs
	err = cdc.Unmarshal(bz, &decoded)
	require.NoError(t, err)

	// Verify the decoded message matches the original
	require.Equal(t, validMsg.Signer, decoded.Signer)
	require.Equal(t, validMsg.Namespaces, decoded.Namespaces)
	require.Equal(t, validMsg.BlobSizes, decoded.BlobSizes)
	require.Equal(t, validMsg.ShareCommitments, decoded.ShareCommitments)
	require.Equal(t, validMsg.ShareVersions, decoded.ShareVersions)

	// Test amino JSON encoding/decoding (needed for ledger signing)
	jsonBz, err := cdc.MarshalJSON(validMsg)
	require.NoError(t, err)

	var decodedFromJSON types.MsgPayForBlobs
	err = cdc.UnmarshalJSON(jsonBz, &decodedFromJSON)
	require.NoError(t, err)

	// Verify the JSON decoded message matches the original
	require.Equal(t, validMsg.Signer, decodedFromJSON.Signer)
	require.Equal(t, validMsg.Namespaces, decodedFromJSON.Namespaces)
	require.Equal(t, validMsg.BlobSizes, decodedFromJSON.BlobSizes)
	require.Equal(t, validMsg.ShareCommitments, decodedFromJSON.ShareCommitments)
	require.Equal(t, validMsg.ShareVersions, decodedFromJSON.ShareVersions)
}

func TestValidateBasic(t *testing.T) {
	type test struct {
		name    string
		msg     *types.MsgPayForBlobs
		wantErr *sdkerrors.Error
	}

	validMsg := validMsgPayForBlobs(t)

	// MsgPayForBlobs that uses parity shares namespace
	paritySharesMsg := validMsgPayForBlobs(t)
	paritySharesMsg.Namespaces[0] = share.ParitySharesNamespace.Bytes()

	// MsgPayForBlobs that uses tail padding namespace
	tailPaddingMsg := validMsgPayForBlobs(t)
	tailPaddingMsg.Namespaces[0] = share.TailPaddingNamespace.Bytes()

	// MsgPayForBlobs that uses transaction namespace
	txNamespaceMsg := validMsgPayForBlobs(t)
	txNamespaceMsg.Namespaces[0] = share.TxNamespace.Bytes()

	// MsgPayForBlobs that uses intermediateStateRoots namespace
	intermediateStateRootsNamespaceMsg := validMsgPayForBlobs(t)
	intermediateStateRootsNamespaceMsg.Namespaces[0] = share.IntermediateStateRootsNamespace.Bytes()

	// MsgPayForBlobs that uses the max primary reserved namespace
	maxReservedNamespaceMsg := validMsgPayForBlobs(t)
	maxReservedNamespaceMsg.Namespaces[0] = share.MaxPrimaryReservedNamespace.Bytes()

	// MsgPayForBlobs that has an empty share commitment
	emptyShareCommitment := validMsgPayForBlobs(t)
	emptyShareCommitment.ShareCommitments[0] = []byte{}

	// MsgPayForBlobs that has an invalid share commitment size
	invalidShareCommitmentSize := validMsgPayForBlobs(t)
	invalidShareCommitmentSize.ShareCommitments[0] = bytes.Repeat([]byte{0x1}, 31)

	// MsgPayForBlobs that has no namespaces
	noNamespaces := validMsgPayForBlobs(t)
	noNamespaces.Namespaces = [][]byte{}

	// MsgPayForBlobs that has no share versions
	noShareVersions := validMsgPayForBlobs(t)
	noShareVersions.ShareVersions = []uint32{}

	// MsgPayForBlobs that has no blob sizes
	noBlobSizes := validMsgPayForBlobs(t)
	noBlobSizes.BlobSizes = []uint32{}

	// MsgPayForBlobs that has no share commitments
	noShareCommitments := validMsgPayForBlobs(t)
	noShareCommitments.ShareCommitments = [][]byte{}

	tests := []test{
		{
			name:    "valid msg",
			msg:     validMsg,
			wantErr: nil,
		},
		{
			name:    "parity shares namespace",
			msg:     paritySharesMsg,
			wantErr: types.ErrReservedNamespace,
		},
		{
			name:    "tail padding namespace",
			msg:     tailPaddingMsg,
			wantErr: types.ErrReservedNamespace,
		},
		{
			name:    "tx namespace",
			msg:     txNamespaceMsg,
			wantErr: types.ErrReservedNamespace,
		},
		{
			name:    "intermediate state root namespace",
			msg:     intermediateStateRootsNamespaceMsg,
			wantErr: types.ErrReservedNamespace,
		},
		{
			name:    "max reserved namespace",
			msg:     maxReservedNamespaceMsg,
			wantErr: types.ErrReservedNamespace,
		},
		{
			name:    "empty share commitment",
			msg:     emptyShareCommitment,
			wantErr: types.ErrInvalidShareCommitment,
		},
		{
			name:    "incorrect hash size share commitment",
			msg:     invalidShareCommitmentSize,
			wantErr: types.ErrInvalidShareCommitment,
		},
		{
			name:    "no namespace ids",
			msg:     noNamespaces,
			wantErr: types.ErrNoNamespaces,
		},
		{
			name:    "no share versions",
			msg:     noShareVersions,
			wantErr: types.ErrNoShareVersions,
		},
		{
			name:    "no blob sizes",
			msg:     noBlobSizes,
			wantErr: types.ErrNoBlobSizes,
		},
		{
			name:    "no share commitments",
			msg:     noShareCommitments,
			wantErr: types.ErrNoShareCommitments,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			err := tt.msg.ValidateBasic()
			if tt.wantErr != nil {
				require.Error(t, err)
				assert.Contains(t, err.Error(), tt.wantErr.Error())
				space, code, log := sdkerrors.ABCIInfo(err, false)
				assert.Equal(t, tt.wantErr.Codespace(), space)
				assert.Equal(t, tt.wantErr.ABCICode(), code)
				t.Log(log)
			}
		})
	}
}

// totalBlobSize subtracts the delimiter size from the desired total size. this
// is useful for testing for blobs that occupy exactly so many shares.
func totalBlobSize(size int) int {
	return size - delimLen(uint64(size))
}

func delimLen(size uint64) int {
	lenBuf := make([]byte, binary.MaxVarintLen64)
	return binary.PutUvarint(lenBuf, size)
}

func validMsgPayForBlobs(t *testing.T) *types.MsgPayForBlobs {
	signer, err := testnode.NewOfflineSigner()
	require.NoError(t, err)
	ns1 := share.NamespaceVersionZeroPrefix
	ns1 = append(ns1, bytes.Repeat([]byte{0x01}, share.NamespaceVersionZeroIDSize)...)
	ns := share.MustNewNamespace(share.NamespaceVersionZero, ns1)
	data := bytes.Repeat([]byte{2}, totalBlobSize(share.ContinuationSparseShareContentSize*12))

	blob, err := share.NewV0Blob(ns, data)
	require.NoError(t, err)

	addr := signer.Account(testfactory.TestAccName).Address()
	pfb, err := types.NewMsgPayForBlobs(addr.String(), appconsts.Version, blob)
	assert.NoError(t, err)

	return pfb
}

func TestNewMsgPayForBlobs(t *testing.T) {
	type testCase struct {
		name        string
		signer      string
		blobs       []*share.Blob
		expectedErr bool
	}
	ns1 := share.MustNewV0Namespace(bytes.Repeat([]byte{1}, share.NamespaceVersionZeroIDSize))
	ns2 := share.MustNewV0Namespace(bytes.Repeat([]byte{2}, share.NamespaceVersionZeroIDSize))
	address := sdk.MustAccAddressFromBech32(testfactory.TestAccAddr)

	testCases := []testCase{
		{
			name:   "valid msg PFB with small blob",
			signer: testfactory.TestAccAddr,
			blobs:  []*share.Blob{mustNewBlob(t, ns1, []byte{1}, share.ShareVersionZero, nil)},
		},
		{
			name:   "valid msg PFB with large blob",
			signer: testfactory.TestAccAddr,
			blobs:  []*share.Blob{mustNewBlob(t, ns1, random.Bytes(1000000), share.ShareVersionZero, nil)},
		},
		{
			name:   "valid msg PFB with two blobs",
			signer: testfactory.TestAccAddr,
			blobs: []*share.Blob{
				mustNewBlob(t, ns1, []byte{1}, share.ShareVersionZero, nil),
				mustNewBlob(t, ns2, []byte{2}, share.ShareVersionZero, nil),
			},
			expectedErr: false,
		},
		{
			name:   "valid msg PFB with share version 1",
			signer: testfactory.TestAccAddr,
			blobs: []*share.Blob{
				mustNewBlob(t, ns1, random.Bytes(10000), share.ShareVersionOne, address),
			},
			expectedErr: false,
		},
		{
			name:   "msg PFB with tx namespace returns an error",
			signer: testfactory.TestAccAddr,
			blobs: []*share.Blob{
				mustNewBlob(t, share.TxNamespace, random.Bytes(1000000), share.ShareVersionZero, nil),
			},
			expectedErr: true,
		},
		{
			name:   "msg PFB with invalid signer returns an error",
			signer: testfactory.TestAccAddr[:10],
			blobs: []*share.Blob{
				mustNewBlob(t, ns1, []byte{1}, share.ShareVersionZero, nil),
			},
			expectedErr: true,
		},
	}
	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			msgPFB, err := types.NewMsgPayForBlobs(tc.signer, appconsts.Version, tc.blobs...)
			if tc.expectedErr {
				assert.Error(t, err)
				return
			}

			for i, blob := range tc.blobs {
				assert.Equal(t, uint32(len(blob.Data())), msgPFB.BlobSizes[i])
				ns, err := share.NewNamespaceFromBytes(msgPFB.Namespaces[i])
				assert.NoError(t, err)
				assert.Equal(t, ns, blob.Namespace())

				expectedCommitment, err := inclusion.CreateCommitment(blob, merkle.HashFromByteSlices, appconsts.SubtreeRootThreshold)
				require.NoError(t, err)
				assert.Equal(t, expectedCommitment, msgPFB.ShareCommitments[i])
			}
		})
	}
}

func mustNewBlob(t *testing.T, ns share.Namespace, data []byte, shareVersion uint8, signer []byte) *share.Blob {
	blob, err := share.NewBlob(ns, data, shareVersion, signer)
	require.NoError(t, err)
	return blob
}

func TestValidateBlobs(t *testing.T) {
	type test struct {
		name        string
		blob        *share.Blob
		expectError bool
	}

	tests := []test{
		{
			name:        "valid blob",
			blob:        mustNewBlob(t, share.RandomBlobNamespace(), []byte{1}, appconsts.DefaultShareVersion, nil),
			expectError: false,
		},
		{
			name:        "invalid namespace",
			blob:        mustNewBlob(t, share.TxNamespace, []byte{1}, appconsts.DefaultShareVersion, nil),
			expectError: true,
		},
		{
			name:        "empty blob",
			blob:        &share.Blob{},
			expectError: true,
		},
	}

	for _, tt := range tests {
		err := types.ValidateBlobs(tt.blob)
		if tt.expectError {
			assert.Error(t, err)
		} else {
			assert.NoError(t, err)
		}
	}
}
