package httpapi

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

	"github.com/gin-gonic/gin"
)

func TestRequestMetricsObserveAndSnapshot(t *testing.T) {
	t.Parallel()

	metrics := newRequestMetrics()
	metrics.observe(http.MethodGet, "/v1/health", http.StatusOK, 100*time.Millisecond)
	metrics.observe(http.MethodGet, "/v1/health", http.StatusOK, 200*time.Millisecond)
	metrics.observe(http.MethodPost, "/v1/tasks", http.StatusCreated, 40*time.Millisecond)

	snapshot := metrics.snapshot()
	if snapshot.RequestsTotal != 3 {
		t.Fatalf("requestsTotal = %d, want 3", snapshot.RequestsTotal)
	}
	if snapshot.StatusClassTotals["2xx"] != 3 {
		t.Fatalf("2xx total = %d, want 3", snapshot.StatusClassTotals["2xx"])
	}
	if len(snapshot.Routes) != 2 {
		t.Fatalf("route bucket count = %d, want 2", len(snapshot.Routes))
	}
	if snapshot.UptimeSeconds < 0 {
		t.Fatalf("uptimeSeconds = %d, want >= 0", snapshot.UptimeSeconds)
	}

	health := findRouteMetric(snapshot.Routes, http.MethodGet, "/v1/health", http.StatusOK)
	if health == nil {
		t.Fatal("missing route metric for GET /v1/health 200")
	}
	if health.Count != 2 {
		t.Fatalf("health count = %d, want 2", health.Count)
	}
	if health.AvgLatencyMs != 150 {
		t.Fatalf("health avgLatencyMs = %.2f, want 150", health.AvgLatencyMs)
	}
	if health.MaxLatencyMs != 200 {
		t.Fatalf("health maxLatencyMs = %.2f, want 200", health.MaxLatencyMs)
	}
	if _, err := time.Parse(time.RFC3339Nano, health.LastSeenAt); err != nil {
		t.Fatalf("health lastSeenAt parse error: %v", err)
	}
	if _, err := time.Parse(time.RFC3339Nano, snapshot.GeneratedAt); err != nil {
		t.Fatalf("snapshot generatedAt parse error: %v", err)
	}
}

func TestRequestMetricsMiddlewareSkipsMetricsEndpoint(t *testing.T) {
	t.Parallel()

	gin.SetMode(gin.TestMode)
	metrics := newRequestMetrics()
	router := gin.New()
	router.Use(requestMetricsMiddleware(metrics))
	router.GET("/v1/health", func(c *gin.Context) {
		c.Status(http.StatusOK)
	})
	router.GET("/v1/metrics", func(c *gin.Context) {
		c.Status(http.StatusOK)
	})
	router.GET("/v1/metrics/prometheus", func(c *gin.Context) {
		c.Status(http.StatusOK)
	})

	healthRequest := httptest.NewRequest(http.MethodGet, "/v1/health", nil)
	healthResponse := httptest.NewRecorder()
	router.ServeHTTP(healthResponse, healthRequest)
	if healthResponse.Code != http.StatusOK {
		t.Fatalf("GET /v1/health status = %d, want 200", healthResponse.Code)
	}

	metricsRequest := httptest.NewRequest(http.MethodGet, "/v1/metrics", nil)
	metricsResponse := httptest.NewRecorder()
	router.ServeHTTP(metricsResponse, metricsRequest)
	if metricsResponse.Code != http.StatusOK {
		t.Fatalf("GET /v1/metrics status = %d, want 200", metricsResponse.Code)
	}

	prometheusRequest := httptest.NewRequest(http.MethodGet, "/v1/metrics/prometheus", nil)
	prometheusResponse := httptest.NewRecorder()
	router.ServeHTTP(prometheusResponse, prometheusRequest)
	if prometheusResponse.Code != http.StatusOK {
		t.Fatalf("GET /v1/metrics/prometheus status = %d, want 200", prometheusResponse.Code)
	}

	snapshot := metrics.snapshot()
	if snapshot.RequestsTotal != 1 {
		t.Fatalf("requestsTotal = %d, want 1", snapshot.RequestsTotal)
	}
	if findRouteMetric(snapshot.Routes, http.MethodGet, "/v1/metrics", http.StatusOK) != nil {
		t.Fatal("metrics endpoint request should be excluded from tracking")
	}
	if findRouteMetric(snapshot.Routes, http.MethodGet, "/v1/metrics/prometheus", http.StatusOK) != nil {
		t.Fatal("prometheus metrics endpoint request should be excluded from tracking")
	}
}

func TestSnapshotPrometheus(t *testing.T) {
	t.Parallel()

	metrics := newRequestMetrics()
	metrics.observe(http.MethodGet, "/v1/health", http.StatusOK, 50*time.Millisecond)
	metrics.observe(http.MethodGet, "/v1/tasks", http.StatusNotFound, 25*time.Millisecond)
	metrics.observe(http.MethodGet, `/v1/quoted"path`, http.StatusOK, 35*time.Millisecond)

	output := metrics.snapshotPrometheus()
	expectedFragments := []string{
		"productier_http_uptime_seconds",
		`productier_http_requests_total{status_class="2xx"} 2`,
		`productier_http_requests_total{status_class="4xx"} 1`,
		`productier_http_requests_route_total{method="GET",path="/v1/health",status="200"} 1`,
		`productier_http_request_latency_avg_ms{method="GET",path="/v1/health",status="200"} 50.000`,
		`productier_http_request_latency_max_ms{method="GET",path="/v1/tasks",status="404"} 25.000`,
		`path="/v1/quoted\"path"`,
	}
	for _, fragment := range expectedFragments {
		if !strings.Contains(output, fragment) {
			t.Fatalf("expected prometheus output to contain %q\noutput:\n%s", fragment, output)
		}
	}
}

func TestItoa(t *testing.T) {
	t.Parallel()

	cases := map[int]string{
		0:    "0",
		7:    "7",
		42:   "42",
		-10:  "-10",
		2048: "2048",
	}

	for input, want := range cases {
		if got := itoa(input); got != want {
			t.Fatalf("itoa(%d) = %q, want %q", input, got, want)
		}
	}
}

func findRouteMetric(routes []routeMetricSnapshot, method, path string, status int) *routeMetricSnapshot {
	for _, route := range routes {
		if route.Method == method && route.Path == path && route.Status == status {
			result := route
			return &result
		}
	}
	return nil
}
