package server

import (
	"encoding/json"
	"reflect"
	"testing"
	"time"

	"github.com/brianvoe/gofakeit/v6"
	rmpb "github.com/pingcap/kvproto/pkg/resource_manager"
	"github.com/prometheus/client_golang/prometheus/testutil"
	"github.com/stretchr/testify/require"
)

func TestPatchResourceGroup(t *testing.T) {
	re := require.New(t)
	rg := &ResourceGroup{Name: "test", Mode: rmpb.GroupMode_RUMode, RUSettings: NewRequestUnitSettings("test", nil)}
	testCaseRU := []struct {
		patchJSONString  string
		expectJSONString string
	}{
		{`{"name":"test", "mode":1, "r_u_settings": {"r_u":{"settings":{"fill_rate": 200000}}}}`,
			`{"name":"test","mode":1,"r_u_settings":{"r_u":{"settings":{"fill_rate":200000},"state":{"initialized":false}}},"priority":0}`},
		{`{"name":"test", "mode":1, "r_u_settings": {"r_u":{"settings":{"fill_rate": 200000, "burst_limit": -1}}}}`,
			`{"name":"test","mode":1,"r_u_settings":{"r_u":{"settings":{"fill_rate":200000,"burst_limit":-1},"state":{"initialized":false}}},"priority":0}`},
		{`{"name":"test", "mode":1, "r_u_settings": {"r_u":{"settings":{"fill_rate": 200000, "burst_limit": -1}}}, "priority": 8, "runaway_settings": {"rule":{"exec_elapsed_time_ms":10000}, "action":1} }`,
			`{"name":"test","mode":1,"r_u_settings":{"r_u":{"settings":{"fill_rate":200000,"burst_limit":-1},"state":{"initialized":false}}},"priority":8,"runaway_settings":{"rule":{"exec_elapsed_time_ms":10000},"action":1}}`},
	}

	for _, ca := range testCaseRU {
		patch := &rmpb.ResourceGroup{}
		err := json.Unmarshal([]byte(ca.patchJSONString), patch)
		re.NoError(err)
		err = rg.PatchSettings(patch)
		re.NoError(err)
		res, err := json.Marshal(rg.Clone(false))
		re.NoError(err)
		re.Equal(ca.expectJSONString, string(res))
	}
}

func TestResourceGroupRequestRUObservesAllocationMetrics(t *testing.T) {
	re := require.New(t)
	groupName := "request-ru-observe-group"
	t.Cleanup(func() {
		deleteServerAllocationMetricLabelValues(groupName)
	})
	rg := FromProtoResourceGroup(&rmpb.ResourceGroup{
		Name: groupName,
		Mode: rmpb.GroupMode_RUMode,
		RUSettings: &rmpb.GroupRequestUnitSettings{
			RU: &rmpb.TokenBucket{
				Tokens: 0,
				Settings: &rmpb.TokenLimitSettings{
					FillRate:   10,
					BurstLimit: 100,
				},
			},
		},
	})

	resp := rg.RequestRU(time.Now(), 150000, 10000, 1, newGroupRUTracker(), newRequestMetrics(groupName))

	re.NotNil(resp)
	granted := readHistogramMetric(re, grantedTokensHistogram.WithLabelValues(groupName))
	re.Equal(uint64(1), granted.GetSampleCount())
	re.Equal(resp.GetGrantedTokens().GetTokens(), granted.GetSampleSum())
	trickle := readHistogramMetric(re, trickleDurationHistogram.WithLabelValues(groupName))
	re.Equal(uint64(1), trickle.GetSampleCount())
	re.Equal(float64(resp.GetTrickleTimeMs()), trickle.GetSampleSum())
	re.Equal(1.0, testutil.ToFloat64(requestCauseCounter.WithLabelValues(groupName, throttleKindLabel, groupCauseLabel)))
	re.Equal(1.0, testutil.ToFloat64(requestCauseCounter.WithLabelValues(groupName, trickleKindLabel, groupCauseLabel)))
}

func resetSizeCache(obj any) {
	resetSizeCacheRecursive(reflect.ValueOf(obj))
}

func resetSizeCacheRecursive(value reflect.Value) {
	if value.Kind() == reflect.Ptr {
		value = value.Elem()
	}

	if value.Kind() != reflect.Struct {
		return
	}

	for i := range value.NumField() {
		fieldValue := value.Field(i)
		fieldType := value.Type().Field(i)

		if fieldType.Name == "XXX_sizecache" && fieldType.Type.Kind() == reflect.Int32 {
			fieldValue.SetInt(0)
		} else {
			resetSizeCacheRecursive(fieldValue)
		}
	}
}

func TestClone(t *testing.T) {
	for range 11 {
		var rg ResourceGroup
		gofakeit.Struct(&rg)
		// hack to reset XXX_sizecache, gofakeit will random set this field but proto clone will not copy this field.
		resetSizeCache(&rg)
		rgClone := rg.Clone(true)
		require.EqualValues(t, &rg, rgClone)
	}
}
