package object_test

import (
	"testing"

	object_pb "bonanza.build/pkg/proto/storage/object"
	"bonanza.build/pkg/storage/object"

	"github.com/buildbarn/bb-storage/pkg/testutil"
	"github.com/buildbarn/bb-storage/pkg/util"
	"github.com/stretchr/testify/require"

	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
)

func TestLocalReference(t *testing.T) {
	t.Run("SHA256_V1", func(t *testing.T) {
		referenceFormat := util.Must(object.NewReferenceFormat(object_pb.ReferenceFormat_SHA256_V1))
		t.Run("InvalidLength", func(t *testing.T) {
			_, err := referenceFormat.NewLocalReference([]byte{1, 2, 3})
			testutil.RequireEqualStatus(t, status.Error(codes.InvalidArgument, "Reference is 3 bytes in size, while SHA256_V1 references are 40 bytes in size"), err)
		})

		t.Run("SizeTooSmall", func(t *testing.T) {
			_, err := referenceFormat.NewLocalReference([]byte{
				// SHA-256 hash.
				0xe3, 0xb0, 0xc4, 0x42, 0x98, 0xfc, 0x1c, 0x14,
				0x9a, 0xfb, 0xf4, 0xc8, 0x99, 0x6f, 0xb9, 0x24,
				0x27, 0xae, 0x41, 0xe4, 0x64, 0x9b, 0x93, 0x4c,
				0xa4, 0x95, 0x99, 0x1b, 0x78, 0x52, 0xb8, 0x55,
				// Size in bytes.
				0x00, 0x00, 0x00,
				// Height.
				0x00,
				// Degree.
				0x00, 0x00,
				// Maximum parents total size in bytes.
				0x00, 0x00,
			})
			testutil.RequireEqualStatus(t, status.Error(codes.InvalidArgument, "Size is 0 bytes, which lies outside the permitted range of [1, 2097152] bytes"), err)
		})

		t.Run("SizeTooLarge", func(t *testing.T) {
			_, err := referenceFormat.NewLocalReference([]byte{
				// SHA-256 hash.
				0x33, 0x03, 0xf7, 0x77, 0x21, 0x1b, 0xdd, 0x46,
				0xb3, 0x8d, 0x92, 0x00, 0xea, 0xa5, 0xc5, 0xd8,
				0x10, 0x3c, 0x66, 0x03, 0x60, 0x5a, 0x22, 0xaf,
				0x94, 0x2a, 0x12, 0xbd, 0x36, 0xf7, 0x06, 0xe6,
				// Size in bytes.
				0x52, 0xe3, 0xd8,
				// Height.
				0xee,
				// Degree.
				0x7d, 0xc5,
				// Maximum parents total size in bytes.
				0x56, 0x51,
			})
			testutil.RequireEqualStatus(t, status.Error(codes.InvalidArgument, "Size is 14213970 bytes, which lies outside the permitted range of [1, 2097152] bytes"), err)
		})

		t.Run("DegreeTooLarge", func(t *testing.T) {
			_, err := referenceFormat.NewLocalReference([]byte{
				// SHA-256 hash.
				0x1f, 0x3d, 0xf4, 0x7b, 0x71, 0xc1, 0x0e, 0x91,
				0x71, 0xe7, 0x23, 0x34, 0xf5, 0x93, 0x4f, 0xc5,
				0x50, 0x65, 0xb7, 0xc1, 0x2e, 0xb9, 0x58, 0xc4,
				0xd9, 0x32, 0xfa, 0xb7, 0xa0, 0xaa, 0x93, 0xd5,
				// Size in bytes.
				0xe0, 0x32, 0x00,
				// Height.
				0x12,
				// Degree.
				0x46, 0x01,
				// Maximum parents total size in bytes.
				0x56, 0x51,
			})
			testutil.RequireEqualStatus(t, status.Error(codes.InvalidArgument, "Degree is 326, while an object of 13024 bytes in size can only have a maximum degree of 325"), err)
		})

		t.Run("ZeroHeightButNonZeroDegree", func(t *testing.T) {
			_, err := referenceFormat.NewLocalReference([]byte{
				// SHA-256 hash.
				0xad, 0x1b, 0x1a, 0xa1, 0xa5, 0x36, 0xec, 0xe4,
				0x81, 0x5c, 0x3d, 0xe7, 0xa3, 0x62, 0xad, 0xb7,
				0xb3, 0x64, 0xe6, 0x8a, 0x23, 0x08, 0xf9, 0xfa,
				0x3c, 0xa9, 0x5f, 0x0c, 0x21, 0xbe, 0x41, 0x15,
				// Size in bytes.
				0x22, 0x51, 0x00,
				// Height.
				0x00,
				// Degree.
				0x46, 0x00,
				// Maximum parents total size in bytes.
				0x00, 0x00,
			})
			testutil.RequireEqualStatus(t, status.Error(codes.InvalidArgument, "Degree is 70 and height is 0, while both either have to be zero or non-zero"), err)
		})

		t.Run("MaximumTotalParentsSizeBytesTooSmall", func(t *testing.T) {
			_, err := referenceFormat.NewLocalReference([]byte{
				// SHA-256 hash.
				0xe4, 0x4a, 0x15, 0x10, 0x84, 0x16, 0x45, 0xb3,
				0xb4, 0x1a, 0x9a, 0xc5, 0x1e, 0x27, 0xf1, 0x93,
				0x0f, 0x81, 0xff, 0x45, 0xc3, 0xb4, 0x0c, 0x96,
				0xe8, 0x1f, 0x89, 0x8f, 0xf4, 0xcb, 0x19, 0x1f,
				// Size in bytes.
				0xe2, 0x3c, 0x00,
				// Height.
				0x24,
				// Degree.
				0x07, 0x00,
				// Maximum parents total size in bytes.
				0x77, 0x05,
			})
			testutil.RequireEqualStatus(t, status.Error(codes.InvalidArgument, "Maximum total parents size is 1399 bytes, which at height 36 lies outside the permitted range of [1400, 73400320] bytes"), err)
		})

		t.Run("MaximumTotalParentsSizeBytesTooLarge", func(t *testing.T) {
			_, err := referenceFormat.NewLocalReference([]byte{
				// SHA-256 hash.
				0xfb, 0xd5, 0x55, 0x7c, 0xfa, 0xdc, 0xce, 0x57,
				0x53, 0x59, 0x59, 0x36, 0xf3, 0xf0, 0x2a, 0x7d,
				0x00, 0x35, 0x6a, 0x36, 0x81, 0xcf, 0x3c, 0x2d,
				0x18, 0xb0, 0xbe, 0x64, 0xe1, 0x0d, 0xe3, 0x6a,
				// Size in bytes.
				0xb1, 0xfd, 0x00,
				// Height.
				0x69,
				// Degree.
				0x08, 0x00,
				// Maximum parents total size in bytes.
				0x01, 0x8d,
			})
			testutil.RequireEqualStatus(t, status.Error(codes.InvalidArgument, "Maximum total parents size is 218169344 bytes, which at height 105 lies outside the permitted range of [4160, 218103808] bytes"), err)
		})

		t.Run("SmallestPossibleObject", func(t *testing.T) {
			// A reference with all of its fields set to
			// their minimum permitted values.
			reference, err := referenceFormat.NewLocalReference([]byte{
				// SHA-256 hash.
				0xca, 0x97, 0x81, 0x12, 0xca, 0x1b, 0xbd, 0xca,
				0xfa, 0xc2, 0x31, 0xb3, 0x9a, 0x23, 0xdc, 0x4d,
				0xa7, 0x86, 0xef, 0xf8, 0x14, 0x7c, 0x4e, 0x72,
				0xb9, 0x80, 0x77, 0x85, 0xaf, 0xee, 0x48, 0xbb,
				// Size in bytes.
				0x01, 0x00, 0x00,
				// Height.
				0x00,
				// Degree.
				0x00, 0x00,
				// Maximum parents total size in bytes.
				0x00, 0x00,
			})
			require.NoError(t, err)

			require.Equal(t, []byte{
				0xca, 0x97, 0x81, 0x12, 0xca, 0x1b, 0xbd, 0xca,
				0xfa, 0xc2, 0x31, 0xb3, 0x9a, 0x23, 0xdc, 0x4d,
				0xa7, 0x86, 0xef, 0xf8, 0x14, 0x7c, 0x4e, 0x72,
				0xb9, 0x80, 0x77, 0x85, 0xaf, 0xee, 0x48, 0xbb,
			}, reference.GetHash())
			require.Equal(t, 1, reference.GetSizeBytes())
			require.Equal(t, 0, reference.GetHeight())
			require.Equal(t, 0, reference.GetDegree())
			require.Equal(t, 0, reference.GetMaximumTotalParentsSizeBytes(false))
			require.Equal(t, 0, reference.GetMaximumTotalParentsSizeBytes(true))

			// Flattening an object with degree 0 should
			// have no effect.
			require.Equal(t, reference, reference.Flatten().GetLocalReference())
		})

		t.Run("LargestPossibleObject", func(t *testing.T) {
			// A reference with all of its fields set to
			// their maximum permitted values.
			reference, err := referenceFormat.NewLocalReference([]byte{
				// SHA-256 hash.
				0x37, 0xdd, 0xcf, 0x55, 0xde, 0xd3, 0x1b, 0x26,
				0x33, 0x31, 0xb3, 0x1f, 0xfe, 0x68, 0xd6, 0xf4,
				0x10, 0xe8, 0x18, 0x6c, 0x61, 0xab, 0x66, 0xed,
				0x8d, 0xb0, 0x4e, 0x2b, 0x43, 0xdc, 0x48, 0x18,
				// Size in bytes.
				0x00, 0x00, 0x20,
				// Height.
				0xff,
				// Degree.
				0xcc, 0xcc,
				// Maximum parents total size in bytes.
				0xe0, 0x97,
			})
			require.NoError(t, err)

			require.Equal(t, []byte{
				0x37, 0xdd, 0xcf, 0x55, 0xde, 0xd3, 0x1b, 0x26,
				0x33, 0x31, 0xb3, 0x1f, 0xfe, 0x68, 0xd6, 0xf4,
				0x10, 0xe8, 0x18, 0x6c, 0x61, 0xab, 0x66, 0xed,
				0x8d, 0xb0, 0x4e, 0x2b, 0x43, 0xdc, 0x48, 0x18,
			}, reference.GetHash())
			require.Equal(t, 2097152, reference.GetSizeBytes())
			require.Equal(t, 255, reference.GetHeight())
			require.Equal(t, 52428, reference.GetDegree())
			require.Equal(t, 532676608, reference.GetMaximumTotalParentsSizeBytes(false))
			require.Equal(t, 534773760, reference.GetMaximumTotalParentsSizeBytes(true))

			require.Equal(t, []byte{
				// SHA-256 hash.
				0x37, 0xdd, 0xcf, 0x55, 0xde, 0xd3, 0x1b, 0x26,
				0x33, 0x31, 0xb3, 0x1f, 0xfe, 0x68, 0xd6, 0xf4,
				0x10, 0xe8, 0x18, 0x6c, 0x61, 0xab, 0x66, 0xed,
				0x8d, 0xb0, 0x4e, 0x2b, 0x43, 0xdc, 0x48, 0x18,
				// Size in bytes.
				0x00, 0x00, 0x20,
				// Height.
				0x00,
				// Degree.
				0x00, 0x00,
				// Maximum parents total size in bytes.
				0x00, 0x00,
			}, reference.Flatten().GetLocalReference().GetRawReference())
		})
	})
}
