package utils

import (
	"fmt"
	"testing"
	"time"

	"github.com/go-logr/logr"
	. "github.com/onsi/gomega"
	"github.com/prometheus/client_golang/prometheus"
	dto "github.com/prometheus/client_model/go"
	"github.com/tidbcloud/aws-shared-provider/pkg/metrics"
	"k8s.io/apimachinery/pkg/util/errors"
	"sigs.k8s.io/controller-runtime/pkg/log"
	"sigs.k8s.io/controller-runtime/pkg/reconcile"
)

func TestReconcileErr(t *testing.T) {
	metrics.ReconcileFailedCounter.Reset()

	unexpected := fmt.Errorf("something unexpected")

	allRequeue := errors.NewAggregate([]error{
		&RequeueAfterError{RequeueAfter: 30 * time.Second},
		&RequeueAfterError{RequeueAfter: 15 * time.Second},
		&RequeueAfterError{RequeueAfter: 5 * time.Second},
	})

	nested := errors.NewAggregate([]error{
		&RequeueAfterError{RequeueAfter: 60 * time.Second},
		allRequeue,
	})

	containUnexpected := errors.NewAggregate([]error{
		unexpected,
		&RequeueAfterError{RequeueAfter: 30 * time.Second},
	})

	cases := map[string]struct {
		input  error
		result reconcile.Result
		err    error
	}{
		"Requeue": {
			input: &RequeueAfterError{RequeueAfter: 1 * time.Minute},
			result: reconcile.Result{
				Requeue:      true,
				RequeueAfter: 1 * time.Minute,
			},
		},
		"UnexpectedError": {
			input:  unexpected,
			result: reconcile.Result{},
			err:    unexpected,
		},
		"AggregatedUseMinimumRequeueInterval": {
			input: allRequeue,
			result: reconcile.Result{
				Requeue:      true,
				RequeueAfter: 5 * time.Second,
			},
		},
		"NestedAggregation": {
			input: nested,
			result: reconcile.Result{
				Requeue:      true,
				RequeueAfter: 5 * time.Second,
			},
		},
		"AggregationContainsUnexpected": {
			input:  containUnexpected,
			result: reconcile.Result{},
			err:    containUnexpected,
		},
	}
	for name, tc := range cases {
		t.Run(name, func(t *testing.T) {
			g := NewGomegaWithT(t)
			result, err := ReconcileErr(logr.New(log.NullLogSink{}), "Cluster", "resource-1", tc.input)
			g.Expect(result).To(Equal(tc.result))
			if tc.err == nil {
				g.Expect(err).To(BeNil())
			} else {
				g.Expect(err).To(Equal(tc.err))
			}
		})
	}
}

func TestReconcileErrMetrics(t *testing.T) {
	resourceName := "resource-metric"
	controllerName := "Cluster"

	metrics.ReconcileFailedCounter.DeleteLabelValues(resourceName, controllerName)
	defer metrics.ReconcileFailedCounter.DeleteLabelValues(resourceName, controllerName)

	_, err := ReconcileErr(logr.New(log.NullLogSink{}), controllerName, resourceName, fmt.Errorf("boom"))
	if err == nil {
		t.Fatal("expected error")
	}

	if got := counterValue(t, metrics.ReconcileFailedCounter.WithLabelValues(resourceName, controllerName)); got != 1 {
		t.Fatalf("expected reconcile failure counter to be 1, got %v", got)
	}
}

func TestReconcileErrClearsMetricsOnSuccess(t *testing.T) {
	resourceName := "resource-success"
	controllerName := "Cluster"

	metrics.ReconcileFailedCounter.DeleteLabelValues(resourceName, controllerName)
	metrics.ObserveReconcileFailure(resourceName, controllerName)

	result, err := ReconcileErr(logr.New(log.NullLogSink{}), controllerName, resourceName, nil)
	if err != nil {
		t.Fatalf("expected nil error, got %v", err)
	}
	if result != (reconcile.Result{}) {
		t.Fatalf("expected empty result, got %#v", result)
	}
	if got := counterValue(t, metrics.ReconcileFailedCounter.WithLabelValues(resourceName, controllerName)); got != 0 {
		t.Fatalf("expected reconcile failure counter to be 0, got %v", got)
	}
}

func TestReconcileErrKeepsMetricsOnRequeue(t *testing.T) {
	resourceName := "resource-requeue"
	controllerName := "Cluster"

	metrics.ReconcileFailedCounter.DeleteLabelValues(resourceName, controllerName)
	metrics.ObserveReconcileFailure(resourceName, controllerName)

	result, err := ReconcileErr(
		logr.New(log.NullLogSink{}),
		controllerName,
		resourceName,
		&RequeueAfterError{RequeueAfter: time.Second},
	)
	if err != nil {
		t.Fatalf("expected nil error, got %v", err)
	}
	if !result.Requeue {
		t.Fatalf("expected requeue result, got %#v", result)
	}
	if got := counterValue(t, metrics.ReconcileFailedCounter.WithLabelValues(resourceName, controllerName)); got != 1 {
		t.Fatalf("expected reconcile failure counter to stay 1, got %v", got)
	}
}

func counterValue(t *testing.T, counter prometheus.Counter) float64 {
	t.Helper()

	metric := &dto.Metric{}
	if err := counter.Write(metric); err != nil {
		t.Fatalf("write metric: %v", err)
	}
	return metric.GetCounter().GetValue()
}
