package podwatcher

import (
	"context"
	"testing"
	"time"

	"github.com/prometheus/prometheus/tsdb"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"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 TestPodWatcherScrapeConvertsDiscoveredPodStatuses(t *testing.T) {
	ctx := context.Background()
	now := time.Date(2026, 9, 2, 8, 0, 0, 0, time.UTC)
	clock := &mutableClock{now: now}
	pod := &corev1.Pod{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "tidb-0",
			Namespace: "dp-x",
			Labels: map[string]string{
				metricstarget.PodLabelGroup:     "tidb",
				metricstarget.PodLabelComponent: "tidb",
			},
		},
		Status: corev1.PodStatus{
			Phase: corev1.PodRunning,
			Conditions: []corev1.PodCondition{{
				Type:   corev1.PodReady,
				Status: corev1.ConditionTrue,
			}},
		},
	}
	target := staticTarget{namespace: "dp-x", labels: map[string]string{
		metricstarget.LabelGroup:     "tidb",
		metricstarget.LabelComponent: "tidb",
	}}
	discovered := &staticDiscovery{targets: []metricstarget.Target{target}}
	scheme := runtime.NewScheme()
	if err := corev1.AddToScheme(scheme); err != nil {
		t.Fatal(err)
	}
	reader := fake.NewClientBuilder().WithScheme(scheme).WithObjects(pod).Build()
	db := openTestDB(t)
	watcher, err := New(&Config{
		Discovery:  discovered,
		Reader:     reader,
		TargetKind: "TiDBGroup",
		Appendable: db,
		Clock:      clock,
		Metrics: []MetricConfig{
			{Name: "pod_pending", JSONPath: `.status.phase`, MatchValue: "Pending"},
			{Name: "pod_ready", JSONPath: `.status.conditions[?(@.type=="Ready")].status`, MatchValue: "True"},
			{Name: "pod_running", JSONPath: `.status.phase`, MatchValue: "Running"},
		},
	})
	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 := watcher.Scrape(ctx); err != nil {
		t.Fatalf("first Scrape() error = %v", err)
	}
	if discovered.kind != "TiDBGroup" {
		t.Fatalf("discovery kind = %q, want TiDBGroup", discovered.kind)
	}
	assertMetricValues(t, provider, target, map[string]float64{
		"pod_pending": 0,
		"pod_ready":   1,
		"pod_running": 1,
	})

	clock.now = now.Add(500 * time.Millisecond)
	if err := watcher.Scrape(ctx); err != nil {
		t.Fatalf("second Scrape() error = %v", err)
	}
	rangeResult, err := provider.Query(ctx, target, "pod_running[1m]")
	if err != nil {
		t.Fatalf("range Query() error = %v", err)
	}
	if samples := rangeResult[0].Series[0].Samples; len(samples) != 2 || samples[0].Value != 1 || samples[1].Value != 1 {
		t.Fatalf("range samples = %+v, want two running samples", samples)
	}

	updatedPod := pod.DeepCopy()
	updatedPod.Status.Phase = corev1.PodPending
	updatedPod.Status.Conditions[0].Status = corev1.ConditionFalse
	if err := reader.Status().Update(ctx, updatedPod); err != nil {
		t.Fatalf("update Pod: %v", err)
	}
	clock.now = now.Add(time.Second)
	if err := watcher.Scrape(ctx); err != nil {
		t.Fatalf("updated Scrape() error = %v", err)
	}
	assertMetricValues(t, provider, target, map[string]float64{
		"pod_pending": 1,
		"pod_ready":   0,
		"pod_running": 0,
	})

	clock.now = now.Add(2 * time.Second)
	discovered.targets = nil
	if err := watcher.Scrape(ctx); err != nil {
		t.Fatalf("empty Scrape() error = %v", err)
	}
	assertNoSeries(t, provider, target, "pod_running")
}

func TestPodWatcherScrapeDeduplicatesOverlappingTargets(t *testing.T) {
	clock := &mutableClock{now: time.Now()}
	target := staticTarget{namespace: "dp-x", labels: map[string]string{
		metricstarget.LabelGroup:     "tidb",
		metricstarget.LabelComponent: "tidb",
	}}
	pod := &corev1.Pod{
		ObjectMeta: metav1.ObjectMeta{Name: "tidb-0", Namespace: "dp-x", Labels: map[string]string{
			metricstarget.PodLabelGroup:     "tidb",
			metricstarget.PodLabelComponent: "tidb",
		}},
		Status: corev1.PodStatus{Phase: corev1.PodRunning},
	}
	scheme := runtime.NewScheme()
	if err := corev1.AddToScheme(scheme); err != nil {
		t.Fatal(err)
	}
	db := openTestDB(t)
	watcher, err := New(&Config{
		Discovery:  &staticDiscovery{targets: []metricstarget.Target{target, target}},
		Reader:     fake.NewClientBuilder().WithScheme(scheme).WithObjects(pod).Build(),
		Appendable: db,
		Clock:      clock,
		Metrics:    []MetricConfig{{Name: "pod_running", JSONPath: `.status.phase`, MatchValue: "Running"}},
	})
	if err != nil {
		t.Fatalf("New() error = %v", err)
	}
	if err := watcher.Scrape(context.Background()); err != nil {
		t.Fatalf("Scrape() error = %v", err)
	}
	if len(watcher.activeSeries) != 1 {
		t.Fatalf("active series = %d, want 1", len(watcher.activeSeries))
	}
}

func TestNewRejectsInvalidConfig(t *testing.T) {
	db := openTestDB(t)
	base := &Config{
		Discovery:  &staticDiscovery{},
		Reader:     fake.NewClientBuilder().Build(),
		Appendable: db,
		Clock:      &mutableClock{now: time.Now()},
		Metrics:    []MetricConfig{{Name: "pod_running", JSONPath: `.status.phase`, MatchValue: "Running"}},
	}
	if _, err := New(base); err != nil {
		t.Fatalf("New(valid) error = %v", err)
	}

	tests := []struct {
		name   string
		mutate func(*Config)
	}{
		{name: "missing metrics", mutate: func(cfg *Config) { cfg.Metrics = nil }},
		{name: "invalid name", mutate: func(cfg *Config) {
			cfg.Metrics = []MetricConfig{{Name: "bad-name", JSONPath: `.status.phase`, MatchValue: "Running"}}
		}},
		{name: "missing JSONPath", mutate: func(cfg *Config) {
			cfg.Metrics = []MetricConfig{{Name: "pod_status"}}
		}},
		{name: "invalid JSONPath", mutate: func(cfg *Config) {
			cfg.Metrics = []MetricConfig{{Name: "pod_status", JSONPath: `.status.phase[`}}
		}},
		{name: "outer braces", mutate: func(cfg *Config) {
			cfg.Metrics = []MetricConfig{{Name: "pod_status", JSONPath: `{.status.phase}`}}
		}},
		{name: "duplicate name", mutate: func(cfg *Config) {
			cfg.Metrics = []MetricConfig{
				{Name: "pod_running", JSONPath: `.status.phase`, MatchValue: "Running"},
				{Name: "pod_running", JSONPath: `.status.phase`, MatchValue: "Pending"},
			}
		}},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			cfg := *base
			tt.mutate(&cfg)
			if _, err := New(&cfg); err == nil {
				t.Fatalf("New(%s) must reject invalid config", tt.name)
			}
		})
	}
}

func assertMetricValues(t *testing.T, provider *querymetrics.Provider, target metricstarget.Target, values map[string]float64) {
	t.Helper()
	queries := make([]string, 0, len(values))
	for metricName := range values {
		queries = append(queries, metricName)
	}
	result, err := provider.Query(context.Background(), target, queries...)
	if err != nil {
		t.Fatalf("Query(%v) error = %v", queries, err)
	}
	for i, metricName := range queries {
		if len(result[i].Series) != 1 || result[i].Series[0].Ref == nil || result[i].Series[0].Ref.Name != "tidb-0" || len(result[i].Series[0].Samples) != 1 {
			t.Fatalf("Query(%s) = %+v, want one tidb-0 sample", metricName, result[i])
		}
		if got := result[i].Series[0].Samples[0].Value; got != values[metricName] {
			t.Fatalf("Query(%s) value = %v, want %v", metricName, got, values[metricName])
		}
	}
}

func assertNoSeries(t *testing.T, provider *querymetrics.Provider, target metricstarget.Target, metricName string) {
	t.Helper()
	result, err := provider.Query(context.Background(), target, metricName)
	if err != nil {
		t.Fatalf("Query(%s) error = %v", metricName, err)
	}
	if len(result) != 1 || len(result[0].Series) != 0 {
		t.Fatalf("Query(%s) = %+v, want no series", metricName, result)
	}
}

func openTestDB(t *testing.T) *tsdb.DB {
	t.Helper()
	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)
		}
	})
	return db
}
