package metrics

import (
	"net/http"
	"net/http/httptest"
	"testing"

	"github.com/gin-gonic/gin"
	"github.com/prometheus/client_golang/prometheus"
	"github.com/prometheus/client_golang/prometheus/testutil"
	dto "github.com/prometheus/client_model/go"
	"github.com/stretchr/testify/require"
)

func TestHTTPMiddlewareRecordsRequests(t *testing.T) {
	reg := prometheus.NewRegistry()
	middleware, err := RegisterHTTP(reg)
	require.NoError(t, err)

	router := newHTTPMetricsTestRouter(middleware)
	router.GET("/ok", func(ctx *gin.Context) {
		ctx.JSON(http.StatusOK, gin.H{"ok": true})
	})

	serveHTTP(router, http.MethodGet, "/ok")
	serveHTTP(router, http.MethodGet, "/missing")
	serveHTTP(router, http.MethodPost, "/ok")

	require.Equal(t, float64(1), testutil.ToFloat64(middleware.requests.WithLabelValues(http.MethodGet, "/ok", "200")))
	require.Equal(t, float64(1), testutil.ToFloat64(middleware.requests.WithLabelValues(http.MethodGet, routeNotFound, "404")))
	require.Equal(t, float64(1), testutil.ToFloat64(middleware.requests.WithLabelValues(http.MethodPost, routeMethodNotAllowed, "405")))
	require.Equal(t, float64(0), testutil.ToFloat64(middleware.inflight))
	require.Equal(t, uint64(1), histogramCount(t, reg, map[string]string{"method": http.MethodGet, "route": "/ok", "status_class": "2xx"}))
	require.Equal(t, uint64(1), histogramCount(t, reg, map[string]string{"method": http.MethodGet, "route": routeNotFound, "status_class": "4xx"}))
	require.Equal(t, uint64(1), histogramCount(t, reg, map[string]string{"method": http.MethodPost, "route": routeMethodNotAllowed, "status_class": "4xx"}))
}

func TestHTTPMiddlewareSkipsHealthRoutes(t *testing.T) {
	reg := prometheus.NewRegistry()
	middleware, err := RegisterHTTP(reg)
	require.NoError(t, err)

	router := newHTTPMetricsTestRouter(middleware)
	router.GET("/healthz", func(ctx *gin.Context) {
		ctx.JSON(http.StatusOK, gin.H{"status": "ok"})
	})

	serveHTTP(router, http.MethodGet, "/healthz")

	require.Equal(t, 0, testutil.CollectAndCount(middleware.requests))
	require.Equal(t, 0, testutil.CollectAndCount(middleware.duration))
	require.Equal(t, float64(0), testutil.ToFloat64(middleware.inflight))
}

func TestHTTPMiddlewareRecordsPanickingRequests(t *testing.T) {
	reg := prometheus.NewRegistry()
	middleware, err := RegisterHTTP(reg)
	require.NoError(t, err)

	router := newHTTPMetricsTestRouter(middleware)
	router.GET("/panic", func(ctx *gin.Context) {
		panic("boom")
	})

	response := serveHTTP(router, http.MethodGet, "/panic")

	require.Equal(t, http.StatusInternalServerError, response.Code)
	require.Equal(t, float64(1), testutil.ToFloat64(middleware.requests.WithLabelValues(http.MethodGet, "/panic", "500")))
	require.Equal(t, uint64(1), histogramCount(t, reg, map[string]string{"method": http.MethodGet, "route": "/panic", "status_class": "5xx"}))
	require.Equal(t, float64(0), testutil.ToFloat64(middleware.inflight))
}

func TestHTTPMiddlewareRecordPanic(t *testing.T) {
	reg := prometheus.NewRegistry()
	middleware, err := RegisterHTTP(reg)
	require.NoError(t, err)

	middleware.RecordPanic(http.MethodGet, "/panic")

	require.Equal(t, float64(1), testutil.ToFloat64(middleware.panics.WithLabelValues(http.MethodGet, "/panic")))
}

func TestHTTPLabelNormalization(t *testing.T) {
	require.Equal(t, labelUnknown, methodLabel("TRACE"))
	require.Equal(t, labelInvalid, statusCodeLabel(0))
	require.Equal(t, labelInvalid, statusClassLabel(700))
	require.Equal(t, routeUnknown, routeLabelForStatus("", http.StatusInternalServerError))
}

func newHTTPMetricsTestRouter(middleware *HTTPMiddleware) *gin.Engine {
	gin.SetMode(gin.ReleaseMode)
	router := gin.New()
	router.HandleMethodNotAllowed = true
	router.Use(gin.CustomRecovery(func(ctx *gin.Context, _ any) {
		ctx.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "internal server error"})
	}))
	router.Use(middleware.Handler("/healthz", "/readyz"))
	router.NoRoute(func(ctx *gin.Context) {
		ctx.JSON(http.StatusNotFound, gin.H{"error": "not found"})
	})
	router.NoMethod(func(ctx *gin.Context) {
		ctx.JSON(http.StatusMethodNotAllowed, gin.H{"error": "method not allowed"})
	})
	return router
}

func serveHTTP(router *gin.Engine, method string, path string) *httptest.ResponseRecorder {
	response := httptest.NewRecorder()
	request := httptest.NewRequest(method, path, nil)
	router.ServeHTTP(response, request)
	return response
}

func histogramCount(t *testing.T, reg *prometheus.Registry, labels map[string]string) uint64 {
	t.Helper()
	return histogramCountForMetric(t, reg, "agentteam_http_request_duration_seconds", labels)
}

func histogramCountForMetric(t *testing.T, reg *prometheus.Registry, metricName string, labels map[string]string) uint64 {
	t.Helper()
	families, err := reg.Gather()
	require.NoError(t, err)
	for _, family := range families {
		if family.GetName() != metricName {
			continue
		}
		for _, metric := range family.GetMetric() {
			if metricLabelsMatch(metric, labels) {
				return metric.GetHistogram().GetSampleCount()
			}
		}
	}
	return 0
}

func metricLabelsMatch(metric *dto.Metric, labels map[string]string) bool {
	if len(metric.GetLabel()) != len(labels) {
		return false
	}
	for _, label := range metric.GetLabel() {
		if labels[label.GetName()] != label.GetValue() {
			return false
		}
	}
	return true
}
