package framework

import (
	"fmt"
	"testing"

	"github.com/prometheus/client_golang/prometheus"
	dto "github.com/prometheus/client_model/go"
	"github.com/stretchr/testify/assert"
)

func TestPrometheusEventReceiver_DecisionEvents(t *testing.T) {
	rec := NewPrometheusEventReceiver()

	tests := []struct {
		eventName EventName
		decision  string
	}{
		{"ScalerTiKVCapacityV1.NoAction", "no_action"},
		{"ScalerTiKVCapacityV1.Disk.PathA", "disk_path_a"},
		{"ScalerTiKVCapacityV1.Disk.PathB", "disk_path_b"},
		{"ScalerTiKVCapacityV1.CPU.ScaleOut", "cpu_scale_out"},
		{"ScalerTiKVCapacityV1.IOPS.Expand", "iops_expand"},
		{"ScalerTiKVCapacityV1.Throughput.Expand", "throughput_expand"},
	}

	for i, tt := range tests {
		t.Run(string(tt.eventName), func(t *testing.T) {
			poolID := fmt.Sprintf("decision-pool-%d", i)
			rec.OnEvent(tt.eventName,
				EventDataWithName("SharedPoolId", CloneableString(poolID)),
			)
			val := getCounterValue(autoscaleTiKVDecisionTotal, poolID, tt.decision)
			assert.InDelta(t, float64(1), val, 0.001, "decision metric for %s", tt.decision)
		})
	}
}

func TestPrometheusEventReceiver_CooldownEvents(t *testing.T) {
	rec := NewPrometheusEventReceiver()

	tests := []struct {
		eventName EventName
		reason    string
	}{
		{"ScalerTiKVCapacityV1.VolumeModify.Cooldown", "volume_modify"},
		{"ScalerTiKVCapacityV1.Disk.PathB.Cooldown", "disk_path_b"},
		{"ScalerTiKVCapacityV1.CPU.ScaleOut.Cooldown", "cpu_scale_out"},
	}

	for i, tt := range tests {
		t.Run(string(tt.eventName), func(t *testing.T) {
			poolID := fmt.Sprintf("cooldown-pool-%d", i)
			rec.OnEvent(tt.eventName,
				EventDataWithName("SharedPoolId", CloneableString(poolID)),
			)
			val := getCounterValue(autoscaleTiKVCooldownSkipTotal, poolID, tt.reason)
			assert.InDelta(t, float64(1), val, 0.001, "cooldown metric for %s", tt.reason)
		})
	}
}

func TestPrometheusEventReceiver_ActionEvents(t *testing.T) {
	rec := NewPrometheusEventReceiver()

	tests := []struct {
		name       string
		eventName  EventName
		actionType string
		action     string
		result     string
	}{
		{"add_nodes_success", "OutputCapacityReservationV1.CreateReservation.Success", "add_nodes", "add_nodes", "success"},
		{"add_nodes_error", "OutputCapacityReservationV1.CreateReservation.Error", "add_nodes", "add_nodes", "error"},
		{"add_nodes_wait_error", "OutputCapacityReservationV1.CreateReservation.WaitError", "add_nodes", "add_nodes", "error"},
		{"expand_disk_success", "OutputCapacityReservationV1.ExpandDisk.Success", "expand_disk", "expand_disk", "success"},
		{"expand_disk_error", "OutputCapacityReservationV1.ExpandDisk.Error", "expand_disk", "expand_disk", "error"},
		{"expand_disk_fallback_create_error", "OutputCapacityReservationV1.ExpandDisk.Fallback.CreateError", "expand_disk", "expand_disk", "error"},
		{"expand_disk_fallback_wait_error", "OutputCapacityReservationV1.ExpandDisk.Fallback.WaitError", "expand_disk", "expand_disk", "error"},
		{"expand_disk_fallback_success", "OutputCapacityReservationV1.CreateReservation.Success", "expand_disk", "expand_disk", "fallback"},
		{"expand_iops_success", "OutputCapacityReservationV1.ExpandIOPS.Success", "expand_iops", "expand_iops", "success"},
		{"expand_iops_error", "OutputCapacityReservationV1.ExpandIOPS.Error", "expand_iops", "expand_iops", "error"},
		{"expand_throughput_success", "OutputCapacityReservationV1.ExpandThroughput.Success", "expand_throughput", "expand_throughput", "success"},
		{"expand_throughput_error", "OutputCapacityReservationV1.ExpandThroughput.Error", "expand_throughput", "expand_throughput", "error"},
	}

	for i, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			poolID := fmt.Sprintf("action-pool-%d", i)
			rec.OnEvent(tt.eventName,
				EventDataWithName("SharedPoolId", CloneableString(poolID)),
				EventDataWithName("ActionType", CloneableString(tt.actionType)),
			)
			val := getCounterValue(autoscaleTiKVActionTotal, poolID, tt.action, tt.result)
			assert.InDelta(t, float64(1), val, 0.001, "action metric for %s/%s", tt.action, tt.result)
		})
	}
}

func TestPrometheusEventReceiver_IgnoresUnmappedEvents(t *testing.T) {
	rec := NewPrometheusEventReceiver()
	rec.OnEvent("SomeUnknownEvent",
		EventDataWithName("SharedPoolId", CloneableString("pool-000")),
	)
}

// TestClearPoolMetrics pins the leak-prevention contract for the
// framework counters. Without ClearPoolMetrics, removed-pool series
// for autoscale_tikv_decision_total / autoscale_tikv_cooldown_skip_total
// / autoscale_tikv_action_total accumulate forever in the
// client_golang registry (counter values do not GC after the source
// stops emitting). AGENTS.md §Metrics design / review checklist pin
// this contract. See cmd/autoscale_engine/main.go's pool cancellation
// paths for the integration test that exercises the cross-package
// wire; this test verifies the helper itself.
func TestClearPoolMetrics(t *testing.T) {
	const poolID = "framework-cleanup-pool"
	const otherPoolID = "framework-keep-pool"

	// Best-effort cleanup if a prior test left state.
	ClearPoolMetrics(poolID)
	ClearPoolMetrics(otherPoolID)
	defer func() {
		ClearPoolMetrics(poolID)
		ClearPoolMetrics(otherPoolID)
	}()

	// Seed each counter family for two pools. Production seeding
	// happens via PrometheusEventReceiver.OnEvent — we exercise
	// the same CounterVec APIs here.
	autoscaleTiKVDecisionTotal.WithLabelValues(poolID, "cpu_scale_out").Inc()
	autoscaleTiKVDecisionTotal.WithLabelValues(poolID, "no_action").Inc()
	autoscaleTiKVDecisionTotal.WithLabelValues(otherPoolID, "cpu_scale_out").Inc()
	autoscaleTiKVCooldownSkipTotal.WithLabelValues(poolID, "disk_path_b").Inc()
	autoscaleTiKVCooldownSkipTotal.WithLabelValues(otherPoolID, "cpu_scale_out").Inc()
	autoscaleTiKVActionTotal.WithLabelValues(poolID, "add_nodes", "success").Inc()
	autoscaleTiKVActionTotal.WithLabelValues(otherPoolID, "expand_disk", "error").Inc()

	// Clear only poolID. otherPoolID must be untouched across
	// all 3 CounterVec families.
	ClearPoolMetrics(poolID)

	if got := countCounterSeriesForPool("autoscale_tikv_decision_total", poolID); got != 0 {
		t.Errorf("autoscale_tikv_decision_total still has %d series for %s after ClearPoolMetrics",
			got, poolID)
	}
	if got := countCounterSeriesForPool("autoscale_tikv_cooldown_skip_total", poolID); got != 0 {
		t.Errorf("autoscale_tikv_cooldown_skip_total still has %d series for %s after ClearPoolMetrics",
			got, poolID)
	}
	if got := countCounterSeriesForPool("autoscale_tikv_action_total", poolID); got != 0 {
		t.Errorf("autoscale_tikv_action_total still has %d series for %s after ClearPoolMetrics",
			got, poolID)
	}
	if got := countCounterSeriesForPool("autoscale_tikv_decision_total", otherPoolID); got == 0 {
		t.Errorf("ClearPoolMetrics(%s) should NOT have cleared %s series",
			poolID, otherPoolID)
	}
	if got := countCounterSeriesForPool("autoscale_tikv_cooldown_skip_total", otherPoolID); got == 0 {
		t.Errorf("ClearPoolMetrics(%s) should NOT have cleared %s series",
			poolID, otherPoolID)
	}
	if got := countCounterSeriesForPool("autoscale_tikv_action_total", otherPoolID); got == 0 {
		t.Errorf("ClearPoolMetrics(%s) should NOT have cleared %s series",
			poolID, otherPoolID)
	}

	// Idempotent.
	ClearPoolMetrics(poolID)
	ClearPoolMetrics(poolID)

	// Unknown poolID is a noop (no panic, no side effect).
	ClearPoolMetrics("never-seen-pool")
}

// countCounterSeriesForPool returns the number of series carrying
// shared_pool_id=<poolID> for the given metric name, gathered from
// the default registry. Counter families with multi-label sets
// (decision/reason, action/result) are all counted together — the
// shared_pool_id sub-LabelSet is what ClearPoolMetrics cleans, so
// "zero series with this poolID for this metric" is the only
// observable that matters for the contract.
func countCounterSeriesForPool(metricName, poolID string) int {
	mfs, err := prometheus.DefaultGatherer.Gather()
	if err != nil {
		return 0
	}
	n := 0
	for _, mf := range mfs {
		if mf.GetName() != metricName {
			continue
		}
		for _, m := range mf.Metric {
			for _, lp := range m.Label {
				if lp.GetName() == "shared_pool_id" && lp.GetValue() == poolID {
					n++
				}
			}
		}
	}
	return n
}

func getCounterValue(cv *prometheus.CounterVec, labels ...string) float64 {
	counter, err := cv.GetMetricWithLabelValues(labels...)
	if err != nil {
		return 0
	}
	var m dto.Metric
	if err := counter.Write(&m); err != nil {
		return 0
	}
	return m.GetCounter().GetValue()
}
