package rules

import (
	"context"
	"strings"
	"testing"
	"time"

	"github.com/prometheus/prometheus/model/labels"
	"github.com/prometheus/prometheus/tsdb"

	decisionmetrics "github.com/tidbcloud/cluster-service-ng/internal/capacity-planner/pkg/metrics"
	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 fixedClock struct{ now time.Time }

func (c fixedClock) 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 }

func TestEvaluatorAggregatesArbitraryMetricsAndPreservesRawSeries(t *testing.T) {
	db := openTestDB(t)
	t0 := time.Date(2026, 9, 7, 8, 0, 0, 0, time.UTC)
	t1 := t0.Add(15 * time.Second)
	appendSamples(t, db,
		testSample{metric: sourceLabels("source_a_total", "Query"), timestamp: t0, value: 10},
		testSample{metric: sourceLabels("source_a_total", "StmtExecute"), timestamp: t0, value: 5},
		testSample{metric: sourceLabels("source_b_total", "Query"), timestamp: t0, value: 30},
		testSample{metric: sourceLabels("source_a_total", "Query"), timestamp: t1, value: 25},
		testSample{metric: sourceLabels("source_a_total", "StmtExecute"), timestamp: t1, value: 10},
		testSample{metric: sourceLabels("source_b_total", "Query"), timestamp: t1, value: 45},
	)
	evaluator, err := New(&Config{
		Interval:      15 * time.Second,
		QueryTimeout:  10 * time.Second,
		MaxSamples:    50000,
		LookbackDelta: 5 * time.Minute,
		Queryable:     db,
		Appendable:    db,
		Records: []Record{{
			Record: "pod:combined:rate1m",
			Expr: `sum by (namespace, group, component, pod) (rate(source_a_total[1m]))
             + sum by (namespace, group, component, pod) (rate(source_b_total[1m]))`,
		}},
	})
	if err != nil {
		t.Fatalf("New() error = %v", err)
	}
	if !evaluator.NeedLeaderElection() {
		t.Fatal("rule evaluator must require leader election")
	}
	if err := evaluator.Evaluate(context.Background(), t1); err != nil {
		t.Fatalf("Evaluate() error = %v", err)
	}

	target := staticTarget{namespace: "dp-x", labels: map[string]string{
		metricstarget.LabelGroup:     "tidb",
		metricstarget.LabelComponent: "tidb",
	}}
	provider, err := querymetrics.New(&querymetrics.Config{Queryable: db, Clock: fixedClock{now: t1}})
	if err != nil {
		t.Fatal(err)
	}
	assertValue(t, provider, target, `source_a_total{type="Query"}`, 25)
	assertValue(t, provider, target, `source_a_total{type="StmtExecute"}`, 10)
	assertValue(t, provider, target, `source_b_total{type="Query"}`, 45)
	result, err := provider.Query(context.Background(), target, "pod:combined:rate1m")
	if err != nil {
		t.Fatalf("query recorded metric: %v", err)
	}
	if len(result) != 1 || len(result[0].Series) != 1 || result[0].Series[0].Samples[0].Value <= 0 {
		t.Fatalf("recorded result = %+v, want one positive series", result)
	}
	for _, query := range []string{
		`pod:combined:rate1m{type=~".+"}`,
		`pod:combined:rate1m{result=~".+"}`,
		`pod:combined:rate1m{resource_group=~".+"}`,
	} {
		result, err := provider.Query(context.Background(), target, query)
		if err != nil {
			t.Fatalf("Query(%s) error = %v", query, err)
		}
		if len(result) != 1 || len(result[0].Series) != 0 {
			t.Fatalf("Query(%s) = %+v, want no series", query, result)
		}
	}
}

func TestEvaluatorOwnsIndependentPeriodicScheduling(t *testing.T) {
	db := openTestDB(t)
	evaluator, err := New(&Config{
		Interval:      5 * time.Millisecond,
		QueryTimeout:  time.Second,
		MaxSamples:    100,
		LookbackDelta: time.Minute,
		Records:       []Record{{Record: "scheduled_metric", Expr: "vector(1)"}},
		Queryable:     db,
		Appendable:    db,
	})
	if err != nil {
		t.Fatalf("New() error = %v", err)
	}
	ctx, cancel := context.WithCancel(context.Background())
	done := make(chan error, 1)
	go func() { done <- evaluator.Start(ctx) }()
	deadline := time.After(time.Second)
	for evaluator.rules[0].GetEvaluationTimestamp().IsZero() {
		select {
		case <-deadline:
			cancel()
			t.Fatal("rule evaluator did not run on its own interval")
		case <-time.After(time.Millisecond):
		}
	}
	cancel()
	if err := <-done; err != nil {
		t.Fatalf("Start() error = %v", err)
	}
}

func TestNewRejectsInvalidConfig(t *testing.T) {
	db := openTestDB(t)
	base := &Config{
		Interval:      time.Second,
		QueryTimeout:  time.Second,
		MaxSamples:    100,
		LookbackDelta: time.Minute,
		Records:       []Record{{Record: "derived", Expr: "vector(1)"}},
		Queryable:     db,
		Appendable:    db,
	}
	tests := []struct {
		name   string
		mutate func(*Config)
		want   string
	}{
		{name: "interval", mutate: func(c *Config) { c.Interval = 0 }, want: "interval"},
		{name: "timeout", mutate: func(c *Config) { c.QueryTimeout = 0 }, want: "query timeout"},
		{name: "samples", mutate: func(c *Config) { c.MaxSamples = 0 }, want: "max samples"},
		{name: "lookback", mutate: func(c *Config) { c.LookbackDelta = 0 }, want: "lookback"},
		{name: "records", mutate: func(c *Config) { c.Records = nil }, want: "records"},
		{name: "queryable", mutate: func(c *Config) { c.Queryable = nil }, want: "queryable"},
		{name: "appendable", mutate: func(c *Config) { c.Appendable = nil }, want: "appendable"},
		{name: "record name", mutate: func(c *Config) { c.Records[0].Record = "bad-name" }, want: "invalid metric"},
		{name: "expression", mutate: func(c *Config) { c.Records[0].Expr = "sum (" }, want: "invalid PromQL"},
		{name: "label", mutate: func(c *Config) { c.Records[0].Labels = map[string]string{"bad-label": "value"} }, want: "invalid label"},
		{name: "metric label", mutate: func(c *Config) { c.Records[0].Labels = map[string]string{"__name__": "value"} }, want: "metric name"},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			configured := *base
			configured.Records = append([]Record(nil), base.Records...)
			tt.mutate(&configured)
			if _, err := New(&configured); err == nil || !strings.Contains(err.Error(), tt.want) {
				t.Fatalf("New() error = %v, want %q", err, tt.want)
			}
		})
	}
	if _, err := New(nil); err == nil {
		t.Fatal("New(nil) must reject missing config")
	}
}

type testSample struct {
	metric    labels.Labels
	timestamp time.Time
	value     float64
}

func sourceLabels(name, queryType string) labels.Labels {
	return labels.FromStrings(
		labels.MetricName, name,
		metricstarget.LabelComponent, "tidb",
		metricstarget.LabelGroup, "tidb",
		decisionmetrics.LabelNamespace, "dp-x",
		decisionmetrics.LabelPod, "tidb-0",
		"resource_group", "default",
		"result", "OK",
		"type", queryType,
	)
}

func appendSamples(t *testing.T, db *tsdb.DB, samples ...testSample) {
	t.Helper()
	appender := db.Appender(context.Background())
	for _, sample := range samples {
		if _, err := appender.Append(0, sample.metric, sample.timestamp.UnixMilli(), sample.value); err != nil {
			_ = appender.Rollback()
			t.Fatalf("append sample: %v", err)
		}
	}
	if err := appender.Commit(); err != nil {
		t.Fatalf("commit samples: %v", err)
	}
}

func assertValue(
	t *testing.T,
	provider *querymetrics.Provider,
	target metricstarget.Target,
	query string,
	want float64,
) {
	t.Helper()
	result, err := provider.Query(context.Background(), target, query)
	if err != nil {
		t.Fatalf("Query(%s) error = %v", query, err)
	}
	if len(result) != 1 || len(result[0].Series) != 1 || len(result[0].Series[0].Samples) != 1 {
		t.Fatalf("Query(%s) = %+v, want one sample", query, result)
	}
	if got := result[0].Series[0].Samples[0].Value; got != want {
		t.Fatalf("Query(%s) value = %v, want %v", query, got, want)
	}
}

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
}
