package metrics

import (
	"net/http"
	"strconv"
	"strings"
	"testing"
	"time"

	"github.com/go-kit/kit/metrics"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestPilotCounter(t *testing.T) {
	rootCounter := newPilotCounter("rootCounter")

	// Checks that a counter without labels can be incremented.
	rootCounter.Add(1)
	assertPilotCounterValue(t, 1.0, "", rootCounter)

	// Checks that a counter with labels can be incremented.
	counterWithLabels := rootCounter.With("foo", "bar", "foo", "buz")

	counterWithLabels.Add(1)
	assertPilotCounterValue(t, 1.0, "foo,bar,foo,buz", counterWithLabels)

	// Checks that the derived counter value has not changed.
	assertPilotCounterValue(t, 1.0, "", rootCounter)

	// Checks that an existing counter (with the same labels) can be incremented.
	existingCounterWithLabels := rootCounter.With("foo", "bar").With("foo", "buz")

	existingCounterWithLabels.Add(1)
	assertPilotCounterValue(t, 2.0, "foo,bar,foo,buz", existingCounterWithLabels)
}

func assertPilotCounterValue(t *testing.T, expValue float64, labels string, c metrics.Counter) {
	t.Helper()
	counter, ok := c.(*pilotCounter).counters.Load(labels)

	require.True(t, ok)
	assert.Equal(t, expValue, counter.(*pilotCounter).c.Value())
}

func TestPilotGauge(t *testing.T) {
	rootGauge := newPilotGauge("rootGauge")

	// Checks that a gauge without labels can be incremented.
	rootGauge.Add(1)

	assertPilotGaugeValue(t, 1.0, "", rootGauge)

	// Checks that a gauge (without labels) value can be set.
	rootGauge.Set(5.0)

	assertPilotGaugeValue(t, 5.0, "", rootGauge)

	// Checks that a gauge with labels can be incremented.
	gaugeWithLabels := rootGauge.With("foo", "bar", "foo", "buz")
	gaugeWithLabels.Add(1)

	assertPilotGaugeValue(t, 1.0, "foo,bar,foo,buz", gaugeWithLabels)

	// Checks that the derived gauge value has not changed.
	assertPilotGaugeValue(t, 5.0, "", rootGauge)

	// Checks that an existing gauge (with the same labels) can be incremented.
	existingGaugeWithLabels := rootGauge.With("foo", "bar").With("foo", "buz")
	existingGaugeWithLabels.Add(1)

	assertPilotGaugeValue(t, 2.0, "foo,bar,foo,buz", existingGaugeWithLabels)
}

func assertPilotGaugeValue(t *testing.T, expValue float64, labels string, g metrics.Gauge) {
	t.Helper()
	gauge, ok := g.(*pilotGauge).gauges.Load(labels)

	require.True(t, ok)
	assert.Equal(t, expValue, gauge.(*pilotGauge).g.Value())
}

func TestPilotHistogram(t *testing.T) {
	rootHistogram := newPilotHistogram("rootHistogram")

	// Checks that an histogram without labels can be updated.
	rootHistogram.Observe(1)

	assertPilotHistogramValues(t, 1.0, 1.0, "", rootHistogram)

	rootHistogram.Observe(2)

	assertPilotHistogramValues(t, 2.0, 3.0, "", rootHistogram)

	// Checks that an histogram with labels can be updated.
	histogramWithLabels := rootHistogram.With("foo", "bar", "foo", "buz")
	histogramWithLabels.Observe(1)

	assertPilotHistogramValues(t, 1.0, 1.0, "foo,bar,foo,buz", histogramWithLabels)

	// Checks that the derived histogram has not changed.
	assertPilotHistogramValues(t, 2.0, 3.0, "", rootHistogram)

	// Checks that an existing histogram (with the same labels) can be updated.
	existingHistogramWithLabels := rootHistogram.With("foo", "bar").With("foo", "buz")
	existingHistogramWithLabels.Observe(1)

	assertPilotHistogramValues(t, 2.0, 2.0, "foo,bar,foo,buz", existingHistogramWithLabels)
}

func assertPilotHistogramValues(t *testing.T, expCount, expTotal float64, labels string, h metrics.Histogram) {
	t.Helper()
	histogram, ok := h.(*pilotHistogram).histograms.Load(labels)

	require.True(t, ok)
	assert.Equal(t, expCount, histogram.(*pilotHistogram).count.Value())
	assert.Equal(t, expTotal, histogram.(*pilotHistogram).total.Value())
}

func TestPilotMetrics(t *testing.T) {
	pilotRegistry := RegisterPilot()

	if !pilotRegistry.IsEpEnabled() || !pilotRegistry.IsSvcEnabled() {
		t.Errorf("PilotRegistry should return true for IsEnabled() and IsSvcEnabled()")
	}

	pilotRegistry.ConfigReloadsCounter().Add(1)
	pilotRegistry.ConfigReloadsFailureCounter().Add(1)
	pilotRegistry.LastConfigReloadSuccessGauge().Set(float64(time.Now().Unix()))
	pilotRegistry.LastConfigReloadFailureGauge().Set(float64(time.Now().Unix()))

	pilotRegistry.
		EntryPointReqsCounter().
		With("code", strconv.Itoa(http.StatusOK), "method", http.MethodGet, "protocol", "http", "entrypoint", "http").
		Add(1)
	pilotRegistry.
		EntryPointReqDurationHistogram().
		With("code", strconv.Itoa(http.StatusOK), "method", http.MethodGet, "protocol", "http", "entrypoint", "http").
		Observe(1)
	pilotRegistry.
		EntryPointOpenConnsGauge().
		With("method", http.MethodGet, "protocol", "http", "entrypoint", "http").
		Set(1)

	pilotRegistry.
		ServiceReqsCounter().
		With("service", "service1", "code", strconv.Itoa(http.StatusOK), "method", http.MethodGet, "protocol", "http").
		Add(1)
	pilotRegistry.
		ServiceReqDurationHistogram().
		With("service", "service1", "code", strconv.Itoa(http.StatusOK), "method", http.MethodGet, "protocol", "http").
		Observe(10000)
	pilotRegistry.
		ServiceOpenConnsGauge().
		With("service", "service1", "method", http.MethodGet, "protocol", "http").
		Set(1)
	pilotRegistry.
		ServiceRetriesCounter().
		With("service", "service1").
		Add(1)
	pilotRegistry.
		ServiceServerUpGauge().
		With("service", "service1", "url", "http://127.0.0.10:80").
		Set(1)

	data := pilotRegistry.Data()

	testCases := []struct {
		name   string
		labels map[string]string
		assert func(*PilotMetric)
	}{
		{
			name:   pilotConfigReloadsTotalName,
			assert: buildPilotCounterAssert(t, pilotConfigReloadsTotalName, 1),
		},
		{
			name:   pilotConfigReloadsFailuresTotalName,
			assert: buildPilotCounterAssert(t, pilotConfigReloadsFailuresTotalName, 1),
		},
		{
			name:   pilotConfigLastReloadSuccessName,
			assert: buildPilotTimestampAssert(t, pilotConfigLastReloadSuccessName),
		},
		{
			name:   pilotConfigLastReloadFailureName,
			assert: buildPilotTimestampAssert(t, pilotConfigLastReloadFailureName),
		},
		{
			name: pilotEntryPointReqsTotalName,
			labels: map[string]string{
				"code":       "200",
				"method":     http.MethodGet,
				"protocol":   "http",
				"entrypoint": "http",
			},
			assert: buildPilotCounterAssert(t, pilotEntryPointReqsTotalName, 1),
		},
		{
			name: pilotEntryPointReqDurationName,
			labels: map[string]string{
				"code":       "200",
				"method":     http.MethodGet,
				"protocol":   "http",
				"entrypoint": "http",
			},
			assert: buildPilotHistogramAssert(t, pilotEntryPointReqDurationName, 1),
		},
		{
			name: pilotEntryPointOpenConnsName,
			labels: map[string]string{
				"method":     http.MethodGet,
				"protocol":   "http",
				"entrypoint": "http",
			},
			assert: buildPilotGaugeAssert(t, pilotEntryPointOpenConnsName, 1),
		},
		{
			name: pilotServiceReqsTotalName,
			labels: map[string]string{
				"code":     "200",
				"method":   http.MethodGet,
				"protocol": "http",
				"service":  "service1",
			},
			assert: buildPilotCounterAssert(t, pilotServiceReqsTotalName, 1),
		},
		{
			name: pilotServiceReqDurationName,
			labels: map[string]string{
				"code":     "200",
				"method":   http.MethodGet,
				"protocol": "http",
				"service":  "service1",
			},
			assert: buildPilotHistogramAssert(t, pilotServiceReqDurationName, 1),
		},
		{
			name: pilotServiceOpenConnsName,
			labels: map[string]string{
				"method":   http.MethodGet,
				"protocol": "http",
				"service":  "service1",
			},
			assert: buildPilotGaugeAssert(t, pilotServiceOpenConnsName, 1),
		},
		{
			name: pilotServiceRetriesTotalName,
			labels: map[string]string{
				"service": "service1",
			},
			assert: buildPilotGreaterThanCounterAssert(t, pilotServiceRetriesTotalName, 1),
		},
		{
			name: pilotServiceServerUpName,
			labels: map[string]string{
				"service": "service1",
				"url":     "http://127.0.0.10:80",
			},
			assert: buildPilotGaugeAssert(t, pilotServiceServerUpName, 1),
		},
	}

	for _, test := range testCases {
		test := test
		t.Run(test.name, func(t *testing.T) {
			metric := findPilotMetric(test.name, data)
			if metric == nil {
				t.Errorf("metrics do not contain %q", test.name)
				return
			}

			for labels := range metric.Observations {
				if len(labels)%2 == 0 {
					splitLabels := strings.Split(labels, ",")
					for i := 0; i < len(splitLabels); i += 2 {
						label := splitLabels[i]
						value := splitLabels[i+1]
						val, ok := test.labels[label]
						if !ok {
							t.Errorf("%q metric contains unexpected label %q", test.name, label)
						} else if val != value {
							t.Errorf("label %q in metric %q has wrong value %q, expected %q", label, test.name, value, val)
						}
					}
				}
			}
			test.assert(metric)
		})
	}
}

func findPilotMetric(name string, metrics []PilotMetric) *PilotMetric {
	for _, metric := range metrics {
		if metric.Name == name {
			return &metric
		}
	}
	return nil
}

func buildPilotCounterAssert(t *testing.T, metricName string, expectedValue float64) func(metric *PilotMetric) {
	t.Helper()

	return func(metric *PilotMetric) {
		for _, value := range metric.Observations {
			if cv := value.(float64); cv != expectedValue {
				t.Errorf("metric %s has value %f, want %f", metricName, cv, expectedValue)
			}
			break
		}
	}
}

func buildPilotGreaterThanCounterAssert(t *testing.T, metricName string, expectedMinValue float64) func(metric *PilotMetric) {
	t.Helper()

	return func(metric *PilotMetric) {
		for _, value := range metric.Observations {
			if cv := value.(float64); cv < expectedMinValue {
				t.Errorf("metric %s has value %f, want at least %f", metricName, cv, expectedMinValue)
			}
			break
		}
	}
}

func buildPilotHistogramAssert(t *testing.T, metricName string, expectedSampleCount float64) func(metric *PilotMetric) {
	t.Helper()

	return func(metric *PilotMetric) {
		for _, value := range metric.Observations {
			if pho := value.(*pilotHistogramObservation); pho.Count != expectedSampleCount {
				t.Errorf("metric %s has sample count value %f, want %f", metricName, pho, expectedSampleCount)
			}
			break
		}
	}
}

func buildPilotGaugeAssert(t *testing.T, metricName string, expectedValue float64) func(metric *PilotMetric) {
	t.Helper()

	return func(metric *PilotMetric) {
		for _, value := range metric.Observations {
			if gv := value.(float64); gv != expectedValue {
				t.Errorf("metric %s has value %f, want %f", metricName, gv, expectedValue)
			}
			break
		}
	}
}

func buildPilotTimestampAssert(t *testing.T, metricName string) func(metric *PilotMetric) {
	t.Helper()

	return func(metric *PilotMetric) {
		for _, value := range metric.Observations {
			if ts := time.Unix(int64(value.(float64)), 0); time.Since(ts) > time.Minute {
				t.Errorf("metric %s has wrong timestamp %v", metricName, ts)
			}
			break
		}
	}
}
