package metricsprovider

import (
	"context"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"github.com/tidbcloud/cluster-service-ng/pkg/k8s"
	autoscalingv2 "k8s.io/api/autoscaling/v2"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/labels"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/apimachinery/pkg/types"
	scalefake "k8s.io/client-go/scale/fake"
	core "k8s.io/client-go/testing"
	metricsfake "k8s.io/metrics/pkg/client/clientset/versioned/fake"
	"sigs.k8s.io/controller-runtime/pkg/client/fake"
	"sigs.k8s.io/custom-metrics-apiserver/pkg/provider"
)

func setupTiDBCPUScaleInGateProvider(t *testing.T, tc *extCPUTestCase, threshold string) provider.MetricsProvider {
	t.Helper()

	s := tc.buildTestScheme(t)
	hpa := tc.buildHPA()
	hpa.Annotations = map[string]string{
		AnnotationTiDBCPUScaleInThreshold: threshold,
	}

	objects := tc.buildPods()
	objects = append(objects, hpa)

	kubeClient := fake.NewClientBuilder().
		WithScheme(s).
		WithObjects(objects...).
		Build()

	fakeMetricsClientset := &metricsfake.Clientset{}
	fakeMetricsClientset.AddReactor("list", "pods", func(action core.Action) (bool, runtime.Object, error) {
		return true, tc.buildPodMetrics(), nil
	})
	metricsClient := fakeMetricsClientset.MetricsV1beta1()

	fakeScaleClient := &scalefake.FakeScaleClient{}
	fakeScaleClient.AddReactor("get", "deployments", func(action core.Action) (bool, runtime.Object, error) {
		return true, tc.buildScale(), nil
	})

	mapper := k8s.RESTMapper()
	return NewCMProvider(kubeClient, mapper, 0, metricsClient, fakeScaleClient)
}

func getTiDBCPUScaleInGateMetric(
	t *testing.T,
	p provider.MetricsProvider,
) (int64, error) {
	t.Helper()

	info := provider.CustomMetricInfo{
		GroupResource: schema.GroupResource{
			Group:    "autoscaling",
			Resource: "horizontalpodautoscalers",
		},
		Metric:     MetricNameExtTiDBCPUScaleInGate,
		Namespaced: true,
	}

	result, err := p.GetMetricByName(
		context.Background(),
		types.NamespacedName{Namespace: testNamespace, Name: testHPAName},
		info,
		labels.Everything(),
	)
	if err != nil {
		return 0, err
	}
	return result.Value.Value(), nil
}

func TestExtTiDBCPUScaleInGate_AllowsScaleInBelowThreshold(t *testing.T) {
	tc := extCPUTestCase{
		specReplicas:      4,
		statusReplicas:    4,
		reportedCPUMillis: []int64{250, 250, 250, 250},
		cpuRequestsMillis: []int64{1000, 1000, 1000, 1000},
	}

	p := setupTiDBCPUScaleInGateProvider(t, &tc, "30")
	value, err := getTiDBCPUScaleInGateMetric(t, p)
	require.NoError(t, err)
	assert.Equal(t, int64(0), value)
}

func TestExtTiDBCPUScaleInGate_BlocksScaleInAtOrAboveThreshold(t *testing.T) {
	tc := extCPUTestCase{
		specReplicas:      4,
		statusReplicas:    4,
		reportedCPUMillis: []int64{350, 350, 350, 350},
		cpuRequestsMillis: []int64{1000, 1000, 1000, 1000},
	}

	p := setupTiDBCPUScaleInGateProvider(t, &tc, "30")
	value, err := getTiDBCPUScaleInGateMetric(t, p)
	require.NoError(t, err)
	assert.Equal(t, int64(1), value)
}

func TestExtTiDBCPUScaleInGate_BlocksScaleInAtThreshold(t *testing.T) {
	tc := extCPUTestCase{
		specReplicas:      4,
		statusReplicas:    4,
		reportedCPUMillis: []int64{300, 300, 300, 300},
		cpuRequestsMillis: []int64{1000, 1000, 1000, 1000},
	}

	p := setupTiDBCPUScaleInGateProvider(t, &tc, "30")
	value, err := getTiDBCPUScaleInGateMetric(t, p)
	require.NoError(t, err)
	assert.Equal(t, int64(1), value)
}

func TestGetTiDBCPUScaleInThreshold(t *testing.T) {
	tests := []struct {
		name     string
		hpa      *autoscalingv2.HorizontalPodAutoscaler
		expected int64
	}{
		{
			name:     "nil hpa uses default",
			hpa:      nil,
			expected: DefaultTiDBCPUScaleInThreshold,
		},
		{
			name:     "missing annotation uses default",
			hpa:      &autoscalingv2.HorizontalPodAutoscaler{},
			expected: DefaultTiDBCPUScaleInThreshold,
		},
		{
			name: "empty annotation uses default",
			hpa: &autoscalingv2.HorizontalPodAutoscaler{
				ObjectMeta: metav1.ObjectMeta{
					Annotations: map[string]string{
						AnnotationTiDBCPUScaleInThreshold: "",
					},
				},
			},
			expected: DefaultTiDBCPUScaleInThreshold,
		},
		{
			name: "parses valid annotation",
			hpa: &autoscalingv2.HorizontalPodAutoscaler{
				ObjectMeta: metav1.ObjectMeta{
					Annotations: map[string]string{
						AnnotationTiDBCPUScaleInThreshold: "25",
					},
				},
			},
			expected: 25,
		},
		{
			name: "invalid string falls back to default",
			hpa: &autoscalingv2.HorizontalPodAutoscaler{
				ObjectMeta: metav1.ObjectMeta{
					Annotations: map[string]string{
						AnnotationTiDBCPUScaleInThreshold: "abc",
					},
				},
			},
			expected: DefaultTiDBCPUScaleInThreshold,
		},
		{
			name: "zero falls back to default",
			hpa: &autoscalingv2.HorizontalPodAutoscaler{
				ObjectMeta: metav1.ObjectMeta{
					Annotations: map[string]string{
						AnnotationTiDBCPUScaleInThreshold: "0",
					},
				},
			},
			expected: DefaultTiDBCPUScaleInThreshold,
		},
		{
			name: "out of range falls back to default",
			hpa: &autoscalingv2.HorizontalPodAutoscaler{
				ObjectMeta: metav1.ObjectMeta{
					Annotations: map[string]string{
						AnnotationTiDBCPUScaleInThreshold: "101",
					},
				},
			},
			expected: DefaultTiDBCPUScaleInThreshold,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			assert.Equal(t, tt.expected, getCPUScaleInThreshold(tt.hpa, TiDBCPUScaleInGateConfig))
		})
	}
}

func TestExtTiDBCPUScaleInGate_ZeroThresholdFallsBackToDefault(t *testing.T) {
	tc := extCPUTestCase{
		specReplicas:      4,
		statusReplicas:    4,
		reportedCPUMillis: []int64{250, 250, 250, 250},
		cpuRequestsMillis: []int64{1000, 1000, 1000, 1000},
	}

	p := setupTiDBCPUScaleInGateProvider(t, &tc, "0")
	value, err := getTiDBCPUScaleInGateMetric(t, p)
	require.NoError(t, err)
	assert.Equal(t, int64(0), value)
}

func TestListAllMetricsIncludesExtTiProxyCPUScaleInGate(t *testing.T) {
	p := &cmProvider{}
	for _, metric := range p.ListAllMetrics() {
		if metric.Metric == MetricNameExtTiProxyCPUScaleInGate {
			assert.Equal(t, schema.GroupResource{
				Group:    "autoscaling",
				Resource: "horizontalpodautoscalers",
			}, metric.GroupResource)
			assert.True(t, metric.Namespaced)
			return
		}
	}
	t.Fatalf("metric %s is not listed", MetricNameExtTiProxyCPUScaleInGate)
}
