package query

import (
	"context"
	"errors"
	"reflect"
	"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"
	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 }

type panicTarget struct{}

func (panicTarget) Namespace() string         { panic("external metrics must ignore target") }
func (panicTarget) Labels() map[string]string { panic("external metrics must ignore target") }

func TestProviderQueriesSelectorsRangesAndMultipleExpressions(t *testing.T) {
	now := time.Date(2026, 8, 31, 12, 0, 0, 0, time.UTC)
	db := openTestDB(t)
	appendSamples(t, db,
		testSample{metric: podLabels("cpu", "dp-x", "tidb", "tidb", "tidb-0", "main"), timestamp: now.Add(-90 * time.Second), value: 1},
		testSample{metric: podLabels("cpu", "dp-x", "tidb", "tidb", "tidb-0", "main"), timestamp: now.Add(-30 * time.Second), value: 2},
		testSample{metric: podLabels("cpu", "dp-x", "other", "tidb", "other-0", "main"), timestamp: now.Add(-30 * time.Second), value: 10},
		testSample{metric: labels.FromStrings(labels.MetricName, "queue_depth", "queue", "work"), timestamp: now.Add(-20 * time.Second), value: 7},
	)
	provider := newTestProvider(t, db, now)
	target := staticTarget{namespace: "dp-x", labels: map[string]string{
		metricstarget.LabelGroup:     "tidb",
		metricstarget.LabelComponent: "tidb",
	}}

	got, err := provider.Query(context.Background(), target,
		"cpu",
		`cpu{container="main"}[2m]`,
		`cpu{container="missing"}`,
		`queue_depth{queue="work"}`,
	)
	if err != nil {
		t.Fatalf("Query() error = %v", err)
	}
	if len(got) != 4 {
		t.Fatalf("Query() returned %d results, want 4", len(got))
	}
	if len(got[0].Series) != 1 || got[0].Series[0].Ref == nil || got[0].Series[0].Ref.Name != "tidb-0" {
		t.Fatalf("instant result = %+v, want only target Pod", got[0])
	}
	if samples := got[0].Series[0].Samples; len(samples) != 1 || samples[0].Value != 2 || !samples[0].Timestamp.Equal(now) {
		t.Fatalf("instant samples = %+v, want value 2 at evaluation time", samples)
	}
	if samples := got[1].Series[0].Samples; len(samples) != 2 || samples[0].Value != 1 || samples[1].Value != 2 {
		t.Fatalf("range samples = %+v, want [1 2]", samples)
	}
	if len(got[2].Series) != 0 {
		t.Fatalf("unmatched query result = %+v, want empty Metrics", got[2])
	}
	if len(got[3].Series) != 1 || got[3].Series[0].Ref != nil || got[3].Series[0].Samples[0].Value != 7 {
		t.Fatalf("external result = %+v, want ref-less value 7", got[3])
	}
}

func TestProviderIgnoresTargetForExternalMetrics(t *testing.T) {
	now := time.Date(2026, 8, 31, 12, 0, 0, 0, time.UTC)
	db := openTestDB(t)
	appendSamples(t, db, testSample{
		metric: labels.FromStrings(labels.MetricName, "queue_depth"), timestamp: now, value: 3,
	})
	provider := newTestProvider(t, db, now)

	got, err := provider.Query(context.Background(), panicTarget{}, "queue_depth")
	if err != nil {
		t.Fatalf("Query() error = %v", err)
	}
	if len(got) != 1 || len(got[0].Series) != 1 || got[0].Series[0].Ref != nil {
		t.Fatalf("Query() = %+v, want one external series", got)
	}
}

func TestProviderRejectsPromQLOutsideSelectorSubset(t *testing.T) {
	now := time.Date(2026, 8, 31, 12, 0, 0, 0, time.UTC)
	db := openTestDB(t)
	provider := newTestProvider(t, db, now)
	target := staticTarget{namespace: "dp-x", labels: map[string]string{metricstarget.LabelGroup: "tidb"}}

	tests := []struct {
		query   string
		wantErr error
	}{
		{query: "sum(cpu)", wantErr: ErrUnsupportedQuery},
		{query: "rate(cpu[1m])", wantErr: ErrUnsupportedQuery},
		{query: "cpu + cpu", wantErr: ErrUnsupportedQuery},
		{query: "cpu offset 1m", wantErr: ErrUnsupportedQuery},
		{query: "cpu @ 1", wantErr: ErrUnsupportedQuery},
		{query: `{container="main"}`, wantErr: ErrUnsupportedQuery},
		{query: "cpu{", wantErr: ErrInvalidQuery},
	}
	for _, tt := range tests {
		t.Run(tt.query, func(t *testing.T) {
			_, err := provider.Query(context.Background(), target, tt.query)
			if !errors.Is(err, tt.wantErr) {
				t.Fatalf("Query() error = %v, want errors.Is(%v)", err, tt.wantErr)
			}
		})
	}
}

func TestProviderReturnsEmptyForUnknownMetric(t *testing.T) {
	now := time.Date(2026, 8, 31, 12, 0, 0, 0, time.UTC)
	db := openTestDB(t)
	provider := newTestProvider(t, db, now)

	unknown, err := provider.Query(context.Background(), nil, "unknown")
	if err != nil || len(unknown) != 1 || len(unknown[0].Series) != 0 {
		t.Fatalf("unknown metric Query() = (%+v, %v), want one empty result", unknown, err)
	}
	got, err := provider.Query(context.Background(), nil)
	if err != nil || !reflect.DeepEqual(got, []decisionmetrics.Metrics{}) {
		t.Fatalf("empty Query() = (%v, %v), want non-nil empty result", got, err)
	}
}

func TestProviderRequiresValidTargetOnlyForPodMetrics(t *testing.T) {
	now := time.Date(2026, 8, 31, 12, 0, 0, 0, time.UTC)
	db := openTestDB(t)
	appendSamples(t, db, testSample{
		metric:    podLabels("cpu", "dp-x", "tidb", "tidb", "tidb-0", "main"),
		timestamp: now,
		value:     1,
	})
	provider := newTestProvider(t, db, now)

	for _, target := range []metricstarget.Target{
		nil,
		staticTarget{},
		staticTarget{namespace: "dp-x"},
		staticTarget{namespace: "dp-x", labels: map[string]string{labels.MetricName: "cpu"}},
	} {
		if _, err := provider.Query(context.Background(), target, "cpu"); err == nil {
			t.Fatalf("Query() target %v must be rejected", target)
		}
	}
}

func TestNewRejectsInvalidConfig(t *testing.T) {
	now := time.Date(2026, 8, 31, 12, 0, 0, 0, time.UTC)
	db := openTestDB(t)
	valid := &Config{
		Queryable: db,
		Clock:     fixedClock{now: now},
	}
	if _, err := New(valid); err != nil {
		t.Fatalf("New(valid) error = %v", err)
	}
	for _, cfg := range []*Config{
		nil,
		{Clock: valid.Clock},
		{Queryable: db},
	} {
		if _, err := New(cfg); err == nil {
			t.Fatalf("New(%+v) must reject invalid config", cfg)
		}
	}
}

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

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 test DB: %v", err)
	}
	t.Cleanup(func() {
		if err := db.Close(); err != nil {
			t.Errorf("close test DB: %v", err)
		}
	})
	return db
}

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 test sample: %v", err)
		}
	}
	if err := appender.Commit(); err != nil {
		t.Fatalf("commit test samples: %v", err)
	}
}

func newTestProvider(t *testing.T, db *tsdb.DB, now time.Time) *Provider {
	t.Helper()
	provider, err := New(&Config{
		Queryable: db,
		Clock:     fixedClock{now: now},
	})
	if err != nil {
		t.Fatalf("New() error = %v", err)
	}
	return provider
}

func podLabels(metricName, namespace, group, component, pod, container string) labels.Labels {
	return labels.FromStrings(
		labels.MetricName, metricName,
		metricstarget.LabelComponent, component,
		decisionmetrics.LabelContainer, container,
		metricstarget.LabelGroup, group,
		decisionmetrics.LabelNamespace, namespace,
		decisionmetrics.LabelPod, pod,
	)
}
