package proto_test

import (
	"math"
	"math/rand"
	"testing"

	"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/util/proto/test"
	generated "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/util/proto/test/custom"
	"github.com/stretchr/testify/require"
	goproto "google.golang.org/protobuf/proto"
)

type protoInt interface {
	~int32 | ~uint32 | ~int64 | ~uint64
}

func nonZero[T protoInt]() T {
	var r T
	for r == 0 {
		r = T(rand.Uint64())
	}
	return r
}

func TestStableMarshalSingle(t *testing.T) {
	t.Run("empty", func(t *testing.T) {
		input := &generated.Primitives{}
		require.Zero(t, input.StableSize())

		r := input.MarshalProtobuf(nil)
		require.Empty(t, r)
	})

	marshalCases := []struct {
		name  string
		input *generated.Primitives
	}{
		{name: "bytes", input: &generated.Primitives{FieldA: []byte{1, 2, 3}}},
		{name: "string", input: &generated.Primitives{FieldB: "123"}},
		{name: "bool", input: &generated.Primitives{FieldC: true}},
		{name: "int32", input: &generated.Primitives{FieldD: -10}},
		{name: "uint32", input: &generated.Primitives{FieldE: nonZero[uint32]()}},
		{name: "int64", input: &generated.Primitives{FieldF: nonZero[int64]()}},
		{name: "uint64", input: &generated.Primitives{FieldG: nonZero[uint64]()}},
		{name: "uint64", input: &generated.Primitives{FieldI: nonZero[uint64]()}},
		{name: "float64", input: &generated.Primitives{FieldJ: math.Float64frombits(12345677890)}},
		{name: "fixed32", input: &generated.Primitives{FieldK: nonZero[uint32]()}},
		{name: "enum, positive", input: &generated.Primitives{FieldH: generated.Primitives_POSITIVE}},
		{name: "enum, negative", input: &generated.Primitives{FieldH: generated.Primitives_NEGATIVE}},
	}
	for _, tc := range marshalCases {
		t.Run(tc.name, func(t *testing.T) {
			r := tc.input.MarshalProtobuf(nil)
			require.Equal(t, len(r), tc.input.StableSize())
			require.NotEmpty(t, r)

			var actual test.Primitives
			require.NoError(t, goproto.Unmarshal(r, &actual))

			var actualFrostfs generated.Primitives
			require.NoError(t, actualFrostfs.UnmarshalProtobuf(r))
			require.Equal(t, tc.input, &actualFrostfs)

			// Compare each field directly, because proto-generated code has private fields.
			require.Equal(t, tc.input.FieldA, actual.FieldA)
			require.Equal(t, tc.input.FieldB, actual.FieldB)
			require.Equal(t, tc.input.FieldC, actual.FieldC)
			require.Equal(t, tc.input.FieldD, actual.FieldD)
			require.Equal(t, tc.input.FieldE, actual.FieldE)
			require.Equal(t, tc.input.FieldF, actual.FieldF)
			require.Equal(t, tc.input.FieldG, actual.FieldG)
			require.Equal(t, tc.input.FieldI, actual.FieldI)
			require.Equal(t, tc.input.FieldJ, actual.FieldJ)
			require.Equal(t, tc.input.FieldK, actual.FieldK)
			require.EqualValues(t, tc.input.FieldH, actual.FieldH)
		})
	}
}

func randIntSlice[T protoInt](n int, includeZero bool) []T {
	r := make([]T, n)
	if n == 0 {
		return r
	}
	for i := range r {
		r[i] = T(rand.Uint64())
	}
	if includeZero {
		r[0] = 0
	}
	return r
}

func TestStableMarshalRep(t *testing.T) {
	t.Run("empty", func(t *testing.T) {
		marshalCases := []struct {
			name  string
			input *generated.RepPrimitives
		}{
			{name: "default", input: &generated.RepPrimitives{}},
			{name: "bytes", input: &generated.RepPrimitives{FieldA: [][]byte{}}},
			{name: "string", input: &generated.RepPrimitives{FieldB: []string{}}},
			{name: "int32", input: &generated.RepPrimitives{FieldC: []int32{}}},
			{name: "uint32", input: &generated.RepPrimitives{FieldD: []uint32{}}},
			{name: "int64", input: &generated.RepPrimitives{FieldE: []int64{}}},
			{name: "uint64", input: &generated.RepPrimitives{FieldF: []uint64{}}},
			{name: "uint64", input: &generated.RepPrimitives{FieldFu: []uint64{}}},
		}

		for _, tc := range marshalCases {
			t.Run(tc.name, func(t *testing.T) {
				require.Zero(t, tc.input.StableSize())

				r := tc.input.MarshalProtobuf(nil)
				require.Empty(t, r)
			})
		}
	})

	marshalCases := []struct {
		name  string
		input *generated.RepPrimitives
	}{
		{name: "bytes", input: &generated.RepPrimitives{FieldA: [][]byte{{1, 2, 3}}}},
		{name: "string", input: &generated.RepPrimitives{FieldB: []string{"123"}}},
		{name: "int32", input: &generated.RepPrimitives{FieldC: randIntSlice[int32](1, true)}},
		{name: "int32", input: &generated.RepPrimitives{FieldC: randIntSlice[int32](2, true)}},
		{name: "int32", input: &generated.RepPrimitives{FieldC: randIntSlice[int32](2, false)}},
		{name: "uint32", input: &generated.RepPrimitives{FieldD: randIntSlice[uint32](1, true)}},
		{name: "uint32", input: &generated.RepPrimitives{FieldD: randIntSlice[uint32](2, true)}},
		{name: "uint32", input: &generated.RepPrimitives{FieldD: randIntSlice[uint32](2, false)}},
		{name: "int64", input: &generated.RepPrimitives{FieldE: randIntSlice[int64](1, true)}},
		{name: "int64", input: &generated.RepPrimitives{FieldE: randIntSlice[int64](2, true)}},
		{name: "int64", input: &generated.RepPrimitives{FieldE: randIntSlice[int64](2, false)}},
		{name: "uint64", input: &generated.RepPrimitives{FieldF: randIntSlice[uint64](1, true)}},
		{name: "uint64", input: &generated.RepPrimitives{FieldF: randIntSlice[uint64](2, true)}},
		{name: "uint64", input: &generated.RepPrimitives{FieldF: randIntSlice[uint64](2, false)}},
		{name: "uint64", input: &generated.RepPrimitives{FieldFu: randIntSlice[uint64](1, true)}},
		{name: "uint64", input: &generated.RepPrimitives{FieldFu: randIntSlice[uint64](2, true)}},
		{name: "uint64", input: &generated.RepPrimitives{FieldFu: randIntSlice[uint64](2, false)}},
	}
	for _, tc := range marshalCases {
		t.Run(tc.name, func(t *testing.T) {
			r := tc.input.MarshalProtobuf(nil)
			require.Equal(t, len(r), tc.input.StableSize())
			require.NotEmpty(t, r)

			var actual test.RepPrimitives
			require.NoError(t, goproto.Unmarshal(r, &actual))

			// Compare each field directly, because proto-generated code has private fields.
			require.Equal(t, tc.input.FieldA, actual.FieldA)
			require.Equal(t, tc.input.FieldB, actual.FieldB)
			require.Equal(t, tc.input.FieldC, actual.FieldC)
			require.Equal(t, tc.input.FieldD, actual.FieldD)
			require.Equal(t, tc.input.FieldE, actual.FieldE)
			require.Equal(t, tc.input.FieldF, actual.FieldF)
			require.Equal(t, tc.input.FieldFu, actual.FieldFu)
		})
	}
}