package executor

import (
	"errors"
	"math"
	"testing"
	"time"

	"github.com/prometheus/client_golang/prometheus/testutil"
	"github.com/tidbcloud/cluster-service-ng/pkg/runtime/actions"
	"github.com/tidbcloud/cluster-service-ng/pkg/runtime/dag"
)

func newTestActionTask(execID, actionName string) *ActionTask {
	return &ActionTask{
		ExecTask: &ExecutorTask{
			ExecId:    execID,
			Args:      actions.ActionArgs{},
			StartTime: time.Now(),
		},
		DagAction: dag.NewDagAction(&actions.MockActionBase{
			ActionName: actionName,
		}, nil),
	}
}

func runningGaugeValue(executorName, taskName, actionName string) float64 {
	return testutil.ToFloat64(
		MetricsDagActionRunningWalltimeSecsGauge.WithLabelValues(
			executorName, taskName, actionName, runningWalltimeResultLabel))
}

func TestMetricsRecorderRunningWalltimeCleanedOnActionResult(t *testing.T) {
	MetricsDagActionRunningWalltimeSecsGauge.Reset()
	recorder := NewMetricsRecorder("test-metrics-recorder-action-result")
	recorder.walltimeUpdateMinPeriod = 0

	actionTask := newTestActionTask("exec-1", "a")
	recorder.OnEvent(EventActionTaskStart, EventDataWithType(EventDataActionTask, actionTask))
	recorder.OnEvent(EventExecutorRunLoopStart)

	if got := testutil.CollectAndCount(MetricsDagActionRunningWalltimeSecsGauge); got != 1 {
		t.Fatalf("unexpected running walltime series count before action result: got %d, want 1", got)
	}

	recorder.OnEvent(EventExecutorRunLoopOnActionResult, EventDataWithType(EventDataActionResult, &ActionTaskStatus{
		ActionTask: actionTask,
		StartTime:  time.Now().Add(-time.Second),
		Elapsed:    time.Second,
		Running:    false,
		Queued:     true,
		Error:      nil,
	}))

	if got := testutil.CollectAndCount(MetricsDagActionRunningWalltimeSecsGauge); got != 0 {
		t.Fatalf("unexpected running walltime series count after action result: got %d, want 0", got)
	}
}

func TestMetricsRecorderRunningWalltimeCleanedOnExecutorDone(t *testing.T) {
	MetricsDagActionRunningWalltimeSecsGauge.Reset()
	recorder := NewMetricsRecorder("test-metrics-recorder-executor-done")
	recorder.walltimeUpdateMinPeriod = 0

	actionTask := newTestActionTask("exec-2", "b")
	recorder.OnEvent(EventActionTaskStart, EventDataWithType(EventDataActionTask, actionTask))
	recorder.OnEvent(EventExecutorRunLoopStart)

	if got := testutil.CollectAndCount(MetricsDagActionRunningWalltimeSecsGauge); got != 1 {
		t.Fatalf("unexpected running walltime series count before executor done: got %d, want 1", got)
	}

	recorder.OnEvent(EventExecutorDone)

	if got := testutil.CollectAndCount(MetricsDagActionRunningWalltimeSecsGauge); got != 0 {
		t.Fatalf("unexpected running walltime series count after executor done: got %d, want 0", got)
	}
}

func TestMetricsRecorderRunningWalltimeUpdateIsThrottled(t *testing.T) {
	MetricsDagActionRunningWalltimeSecsGauge.Reset()
	recorder := NewMetricsRecorder("test-metrics-recorder-throttled")
	recorder.walltimeUpdateMinPeriod = time.Hour

	recorder.OnEvent(EventExecutorRunLoopStart)
	if !recorder.lastWalltimeUpdateAt.IsZero() {
		t.Fatal("walltime update timestamp should stay zero when there are no running actions")
	}

	actionTask := newTestActionTask("exec-3", "c")
	recorder.OnEvent(EventActionTaskStart, EventDataWithType(EventDataActionTask, actionTask))
	recorder.OnEvent(EventExecutorRunLoopStart)
	first := recorder.lastWalltimeUpdateAt
	if first.IsZero() {
		t.Fatal("first walltime update timestamp should not be zero")
	}

	recorder.OnEvent(EventExecutorRunLoopStart)
	second := recorder.lastWalltimeUpdateAt

	if !second.Equal(first) {
		t.Fatalf("walltime update timestamp changed within throttle window: first=%v second=%v", first, second)
	}
}

func TestMetricsRecorderRunningWalltimeAccumulatesAcrossRetries(t *testing.T) {
	MetricsDagActionRunningWalltimeSecsGauge.Reset()
	recorder := NewMetricsRecorder("test-metrics-recorder-retry")
	recorder.walltimeUpdateMinPeriod = 0

	actionTask := newTestActionTask("exec-4", "d")
	recorder.OnEvent(EventActionTaskStart, EventDataWithType(EventDataActionTask, actionTask))
	recorder.OnEvent(EventExecutorRunLoopOnActionResult, EventDataWithType(EventDataActionResult, &ActionTaskStatus{
		ActionTask: actionTask,
		StartTime:  time.Now(),
		Elapsed:    2 * time.Second,
		Error:      errors.New("retry"),
	}))

	if got := runningGaugeValue(recorder.executorName, "exec-4", "d"); math.Abs(got-2.0) > 1e-9 {
		t.Fatalf("unexpected gauge after first failed attempt: got %.3f, want 2.0", got)
	}

	recorder.OnEvent(EventActionTaskStart, EventDataWithType(EventDataActionTask, actionTask))
	if got := runningGaugeValue(recorder.executorName, "exec-4", "d"); math.Abs(got-2.0) > 1e-9 {
		t.Fatalf("unexpected gauge at retry start: got %.3f, want 2.0", got)
	}
	recorder.OnEvent(EventExecutorRunLoopOnActionResult, EventDataWithType(EventDataActionResult, &ActionTaskStatus{
		ActionTask: actionTask,
		StartTime:  time.Now(),
		Elapsed:    3 * time.Second,
		Error:      nil,
	}))

	if got := testutil.CollectAndCount(MetricsDagActionRunningWalltimeSecsGauge); got != 0 {
		t.Fatalf("unexpected series count after final success: got %d, want 0", got)
	}
}

func TestMetricsRecorderRunningWalltimeCleanedOnExecTaskDone(t *testing.T) {
	MetricsDagActionRunningWalltimeSecsGauge.Reset()
	recorder := NewMetricsRecorder("test-metrics-recorder-exec-done")
	recorder.walltimeUpdateMinPeriod = 0

	actionTask := newTestActionTask("exec-5", "e")
	recorder.OnEvent(EventActionTaskStart, EventDataWithType(EventDataActionTask, actionTask))
	recorder.OnEvent(EventExecutorRunLoopOnActionResult, EventDataWithType(EventDataActionResult, &ActionTaskStatus{
		ActionTask: actionTask,
		StartTime:  time.Now(),
		Elapsed:    time.Second,
		Error:      errors.New("aborted"),
	}))

	if got := testutil.CollectAndCount(MetricsDagActionRunningWalltimeSecsGauge); got != 1 {
		t.Fatalf("unexpected series count before exec done cleanup: got %d, want 1", got)
	}

	recorder.OnEvent(EventExecTaskDone, EventDataWithType(EventDataExecTask, actionTask.ExecTask))

	if got := testutil.CollectAndCount(MetricsDagActionRunningWalltimeSecsGauge); got != 0 {
		t.Fatalf("unexpected series count after exec done cleanup: got %d, want 0", got)
	}
}

func TestMetricsRecorderZeroElapsedDoesNotFallbackWhenStartTimeExists(t *testing.T) {
	MetricsDagActionRunningWalltimeSecsGauge.Reset()
	recorder := NewMetricsRecorder("test-metrics-recorder-zero-elapsed")
	recorder.walltimeUpdateMinPeriod = 0

	actionTask := newTestActionTask("exec-6", "f")
	recorder.OnEvent(EventActionTaskStart, EventDataWithType(EventDataActionTask, actionTask))

	// Simulate time passing before result event handling.
	time.Sleep(20 * time.Millisecond)

	recorder.OnEvent(EventExecutorRunLoopOnActionResult, EventDataWithType(EventDataActionResult, &ActionTaskStatus{
		ActionTask: actionTask,
		StartTime:  time.Now(),
		Elapsed:    0,
		Error:      errors.New("retry"),
	}))

	if got := testutil.CollectAndCount(MetricsDagActionRunningWalltimeSecsGauge); got != 1 {
		t.Fatalf("unexpected series count after zero-elapsed failed attempt: got %d, want 1", got)
	}

	if got := runningGaugeValue(recorder.executorName, "exec-6", "f"); math.Abs(got-0) > 1e-9 {
		t.Fatalf("unexpected gauge after zero-elapsed failed attempt: got %.6f, want 0", got)
	}
}
