package metricsserver

import (
	"context"
	"testing"
	"time"

	corev1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/api/resource"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	k8stesting "k8s.io/client-go/testing"
	metricv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1"
	metricsfake "k8s.io/metrics/pkg/client/clientset/versioned/fake"
	"sigs.k8s.io/controller-runtime/pkg/client/fake"

	querymetrics "github.com/tidbcloud/cluster-service-ng/internal/capacity-planner/pkg/prom/query"
	metricsstore "github.com/tidbcloud/cluster-service-ng/internal/capacity-planner/pkg/prom/store"
	metricstarget "github.com/tidbcloud/cluster-service-ng/internal/capacity-planner/pkg/target"
)

type mutableClock struct{ now time.Time }

func (c *mutableClock) Now() time.Time { return c.now }

type staticTarget struct {
	namespace string
	labels    map[string]string
}

func (t staticTarget) Namespace() string         { return t.namespace }
func (t staticTarget) Labels() map[string]string { return t.labels }

type staticDiscovery struct {
	kind    string
	targets []metricstarget.Target
}

func (d *staticDiscovery) Targets(kind string) []metricstarget.Target {
	d.kind = kind
	return d.targets
}

func TestMetricsServerScrapeFeedsPromQLProvider(t *testing.T) {
	ctx := context.Background()
	now := time.Date(2026, 8, 31, 12, 0, 0, 0, time.UTC)
	clock := &mutableClock{now: now}
	pod := &corev1.Pod{ObjectMeta: metav1.ObjectMeta{
		Name:              "tidb-0",
		Namespace:         "dp-x",
		CreationTimestamp: metav1.NewTime(now.Add(-10 * time.Minute)),
		Labels: map[string]string{
			metricstarget.PodLabelGroup:     "tidb",
			metricstarget.PodLabelComponent: "tidb",
		},
	}}
	podMetrics := &metricv1beta1.PodMetrics{
		ObjectMeta: metav1.ObjectMeta{Name: pod.Name, Namespace: pod.Namespace, Labels: map[string]string{
			metricstarget.PodLabelGroup:     "tidb",
			metricstarget.PodLabelComponent: "tidb",
		}},
		Timestamp: metav1.NewTime(now.Add(-10 * time.Second)),
		Window:    metav1.Duration{Duration: time.Minute},
		Containers: []metricv1beta1.ContainerMetrics{
			{Name: "main", Usage: corev1.ResourceList{
				corev1.ResourceCPU:    resource.MustParse("500m"),
				corev1.ResourceMemory: resource.MustParse("512Mi"),
			}},
			{Name: "sidecar", Usage: corev1.ResourceList{
				corev1.ResourceCPU: resource.MustParse("100m"),
			}},
		},
	}
	otherPodMetrics := podMetrics.DeepCopy()
	otherPodMetrics.Name = "other-0"
	otherPodMetrics.Labels[metricstarget.PodLabelGroup] = "other"
	metricsClient := metricsfake.NewSimpleClientset()
	source := &metricsSource{items: map[string]*metricv1beta1.PodMetrics{
		pod.Namespace + "/" + pod.Name:             podMetrics,
		pod.Namespace + "/" + otherPodMetrics.Name: otherPodMetrics,
	}}
	metricsClient.PrependReactor("list", "pods", source.recordList)
	scheme := runtime.NewScheme()
	if err := corev1.AddToScheme(scheme); err != nil {
		t.Fatal(err)
	}
	k8sReader := fake.NewClientBuilder().WithScheme(scheme).WithObjects(pod).Build()
	target := staticTarget{namespace: "dp-x", labels: map[string]string{
		metricstarget.LabelGroup:     "tidb",
		metricstarget.LabelComponent: "tidb",
	}}
	discovered := &staticDiscovery{}

	db, err := metricsstore.Open(&metricsstore.Config{Path: t.TempDir(), Retention: time.Hour})
	if err != nil {
		t.Fatalf("open TSDB: %v", err)
	}
	t.Cleanup(func() {
		if err := db.Close(); err != nil {
			t.Errorf("close TSDB: %v", err)
		}
	})
	metricsScraper, err := New(&Config{
		MetricsClient: metricsClient,
		Discovery:     discovered,
		Reader:        k8sReader,
		TargetKind:    "TiDBGroup",
		Appendable:    db,
		Clock:         clock,
		Resources: map[string]corev1.ResourceName{
			"cpu":    corev1.ResourceCPU,
			"memory": corev1.ResourceMemory,
		},
		MaxAge: 2 * time.Minute,
	})
	if err != nil {
		t.Fatalf("New() error = %v", err)
	}
	provider, err := querymetrics.New(&querymetrics.Config{Queryable: db, Clock: clock})
	if err != nil {
		t.Fatalf("new PromQL provider: %v", err)
	}

	if err := metricsScraper.Scrape(ctx); err != nil {
		t.Fatalf("empty Scrape() error = %v", err)
	}
	if discovered.kind != "TiDBGroup" {
		t.Fatalf("discovery kind = %q, want TiDBGroup", discovered.kind)
	}
	if source.calls != 0 {
		t.Fatalf("PodMetrics list calls = %d, want 0 without targets", source.calls)
	}
	beforeScrape, err := provider.Query(ctx, target, `cpu`, `cpu{container="main"}`, `memory`)
	if err != nil || len(beforeScrape) != 3 {
		t.Fatalf("Query() before first target scrape = (%+v, %v), want three empty results", beforeScrape, err)
	}

	discovered.targets = []metricstarget.Target{target}
	if err := metricsScraper.Scrape(ctx); err != nil {
		t.Fatalf("first target Scrape() error = %v", err)
	}
	wantSelector := "pingcap.com/component=tidb,pingcap.com/group=tidb"
	if source.calls != 1 || source.namespaces[0] != pod.Namespace || source.selectors[0] != wantSelector {
		t.Fatalf("PodMetrics list requests = calls:%d namespaces:%v selectors:%v", source.calls, source.namespaces, source.selectors)
	}
	got, err := provider.Query(ctx, target, `cpu`, `cpu{container="main"}`, `memory`)
	if err != nil {
		t.Fatalf("Query() error = %v", err)
	}
	if len(got) != 3 || len(got[0].Series) != 2 || len(got[1].Series) != 1 || len(got[2].Series) != 1 {
		t.Fatalf("Query() = %+v, want CPU by two containers and one memory series", got)
	}
	cpuValues := map[float64]bool{}
	for _, series := range got[0].Series {
		if series.Ref == nil || series.Ref.Name != "tidb-0" {
			t.Fatalf("CPU series ref = %+v, want tidb-0", series.Ref)
		}
		cpuValues[series.Samples[0].Value] = true
	}
	if !cpuValues[0.5] || !cpuValues[0.1] {
		t.Fatalf("CPU values = %v, want 0.5 and 0.1 cores", cpuValues)
	}
	if got[1].Series[0].Samples[0].Value != 0.5 {
		t.Fatalf("main container CPU = %v, want 0.5", got[1].Series[0].Samples[0].Value)
	}
	if got[2].Series[0].Samples[0].Value != float64(512*1024*1024) {
		t.Fatalf("memory = %v, want bytes", got[2].Series[0].Samples[0].Value)
	}

	clock.now = now.Add(20 * time.Second)
	if err := metricsScraper.Scrape(ctx); err != nil {
		t.Fatalf("second Scrape() error = %v", err)
	}
	rangeResult, err := provider.Query(ctx, target, `cpu{container="main"}[1m]`)
	if err != nil {
		t.Fatalf("range Query() error = %v", err)
	}
	if samples := rangeResult[0].Series[0].Samples; len(samples) != 2 || !samples[0].Timestamp.Equal(now) || !samples[1].Timestamp.Equal(clock.now) {
		t.Fatalf("range samples = %+v, want one sample per scrape", samples)
	}

	clock.now = now.Add(40 * time.Second)
	metricCalls := source.calls
	discovered.targets = nil
	if err := metricsScraper.Scrape(ctx); err != nil {
		t.Fatalf("target removal Scrape() error = %v", err)
	}
	if source.calls != metricCalls {
		t.Fatalf("PodMetrics calls changed from %d to %d for empty targets", metricCalls, source.calls)
	}
	staleResult, err := provider.Query(ctx, target, "cpu")
	if err != nil {
		t.Fatalf("stale Query() error = %v", err)
	}
	if len(staleResult[0].Series) != 0 {
		t.Fatalf("stale Query() = %+v, want disappeared series removed", staleResult[0])
	}
}

func TestMetricsServerScrapeDeduplicatesTargets(t *testing.T) {
	now := time.Now()
	pod := &corev1.Pod{ObjectMeta: metav1.ObjectMeta{
		Name: "tidb-0", Namespace: "dp-x", CreationTimestamp: metav1.NewTime(now.Add(-time.Minute)),
		Labels: map[string]string{
			metricstarget.PodLabelGroup:     "tidb",
			metricstarget.PodLabelComponent: "tidb",
		},
	}}
	podMetrics := &metricv1beta1.PodMetrics{
		ObjectMeta: metav1.ObjectMeta{Name: pod.Name, Namespace: pod.Namespace, Labels: map[string]string{
			metricstarget.PodLabelGroup:     "tidb",
			metricstarget.PodLabelComponent: "tidb",
		}},
		Timestamp: metav1.NewTime(now),
		Containers: []metricv1beta1.ContainerMetrics{{Name: "main", Usage: corev1.ResourceList{
			corev1.ResourceCPU: resource.MustParse("1"),
		}}},
	}
	metricsClient := metricsfake.NewSimpleClientset()
	source := &metricsSource{items: map[string]*metricv1beta1.PodMetrics{
		pod.Namespace + "/" + pod.Name: podMetrics,
	}}
	metricsClient.PrependReactor("list", "pods", source.recordList)
	scheme := runtime.NewScheme()
	if err := corev1.AddToScheme(scheme); err != nil {
		t.Fatal(err)
	}
	k8sReader := fake.NewClientBuilder().WithScheme(scheme).WithObjects(pod).Build()
	target := staticTarget{namespace: pod.Namespace, labels: map[string]string{
		metricstarget.LabelGroup:     "tidb",
		metricstarget.LabelComponent: "tidb",
	}}
	db, err := metricsstore.Open(&metricsstore.Config{Path: t.TempDir(), Retention: time.Hour})
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { _ = db.Close() })
	scraper, err := New(&Config{
		MetricsClient: metricsClient,
		Discovery:     &staticDiscovery{targets: []metricstarget.Target{target, target}},
		Reader:        k8sReader,
		Appendable:    db,
		Clock:         &mutableClock{now: now},
		Resources:     map[string]corev1.ResourceName{"cpu": corev1.ResourceCPU},
		MaxAge:        time.Minute,
	})
	if err != nil {
		t.Fatal(err)
	}
	if err := scraper.Scrape(context.Background()); err != nil {
		t.Fatalf("Scrape() error = %v", err)
	}
	if source.calls != 1 {
		t.Fatalf("PodMetrics calls = %d, want 1", source.calls)
	}
}

func TestValidSampleRejectsMissingAndStaleData(t *testing.T) {
	now := time.Date(2026, 8, 31, 12, 0, 0, 0, time.UTC)
	pod := &corev1.Pod{ObjectMeta: metav1.ObjectMeta{CreationTimestamp: metav1.NewTime(now.Add(-time.Minute))}}
	if !validSample(now.Add(-30*time.Second), pod, now, time.Minute) {
		t.Fatal("fresh sample must be valid")
	}
	for _, timestamp := range []time.Time{{}, now.Add(-2 * time.Minute), now.Add(-90 * time.Second)} {
		candidatePod := pod
		if timestamp.Equal(now.Add(-90 * time.Second)) {
			candidatePod = &corev1.Pod{ObjectMeta: metav1.ObjectMeta{CreationTimestamp: metav1.NewTime(now.Add(-time.Minute))}}
		}
		if validSample(timestamp, candidatePod, now, time.Minute) {
			t.Fatalf("timestamp %v must be invalid", timestamp)
		}
	}
	if validSample(now, nil, now, time.Minute) {
		t.Fatal("sample without live Pod must be invalid")
	}
}

func TestNewMetricsServerRejectsInvalidConfig(t *testing.T) {
	if _, err := New(nil); err == nil {
		t.Fatal("nil config must be rejected")
	}
	db, err := metricsstore.Open(&metricsstore.Config{Path: t.TempDir(), Retention: time.Hour})
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { _ = db.Close() })
	base := &Config{
		MetricsClient: metricsfake.NewSimpleClientset(),
		Discovery:     &staticDiscovery{},
		Reader:        fake.NewClientBuilder().Build(),
		Appendable:    db,
		Clock:         &mutableClock{now: time.Now()},
		Resources:     map[string]corev1.ResourceName{"cpu": corev1.ResourceCPU},
		MaxAge:        time.Minute,
	}
	if _, err := New(base); err != nil {
		t.Fatalf("valid config error = %v", err)
	}
	invalid := *base
	invalid.Resources = map[string]corev1.ResourceName{"cpu": ""}
	if _, err := New(&invalid); err == nil {
		t.Fatal("metrics-server metric without resource must be rejected")
	}
}

type metricsSource struct {
	calls      int
	namespaces []string
	selectors  []string
	items      map[string]*metricv1beta1.PodMetrics
}

func (s *metricsSource) recordList(action k8stesting.Action) (bool, runtime.Object, error) {
	s.calls++
	s.namespaces = append(s.namespaces, action.GetNamespace())
	s.selectors = append(s.selectors, action.(k8stesting.ListAction).GetListRestrictions().Labels.String())
	list := &metricv1beta1.PodMetricsList{}
	for _, item := range s.items {
		if item.Namespace == action.GetNamespace() {
			list.Items = append(list.Items, *item.DeepCopy())
		}
	}
	return true, list, nil
}
