package httpapi

import (
	"context"
	"net/http"
	"net/http/httptest"
	"strings"
	"sync"
	"testing"
	"time"
)

// okHandler is a sentinel upstream that records it was reached.
func okHandler(t *testing.T, reached *bool) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		*reached = true
		w.WriteHeader(http.StatusOK)
	})
}

func TestRateLimiterDisabledWhenRPSZero(t *testing.T) {
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	rl := newRateLimiterFromConfig(ctx, 0, 0)
	if rl.rps != 0 {
		t.Fatalf("rps should be 0 when disabled, got %v", rl.rps)
	}
	// Disabled limiter is a pass-through: requests always reach upstream.
	reached := false
	h := rl.middleware(okHandler(t, &reached))
	for i := 0; i < 50; i++ {
		rec := httptest.NewRecorder()
		h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/api/v1/entities", nil))
		if rec.Code != http.StatusOK {
			t.Fatalf("disabled limiter request %d: want 200, got %d", i, rec.Code)
		}
	}
	if !reached {
		t.Fatal("disabled limiter never reached upstream")
	}
}

func TestRateLimiterThrottlesAfterBurst(t *testing.T) {
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	rl := newRateLimiter(ctx, 1, 3) // 1 rps, burst 3
	reached := false
	h := rl.middleware(okHandler(t, &reached))

	var last429, okCount int
	for i := 0; i < 6; i++ {
		rec := httptest.NewRecorder()
		h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/api/v1/entities", nil))
		switch rec.Code {
		case http.StatusOK:
			okCount++
		case http.StatusTooManyRequests:
			last429 = i
		}
	}
	if okCount < 1 {
		t.Fatal("expected at least one request through within burst")
	}
	if last429 == 0 {
		t.Fatal("expected at least one 429 once burst exhausted")
	}
	if !reached {
		t.Fatal("upstream never reached")
	}
}

func TestRateLimiterExemptsHealthz(t *testing.T) {
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	rl := newRateLimiter(ctx, 1, 1) // tiny burst
	reached := false
	h := rl.middleware(okHandler(t, &reached))
	// /healthz must never be throttled, even under a flood.
	for i := 0; i < 20; i++ {
		rec := httptest.NewRecorder()
		h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/healthz", nil))
		if rec.Code != http.StatusOK {
			t.Fatalf("healthz request %d throttled: want 200, got %d", i, rec.Code)
		}
	}
	if !reached {
		t.Fatal("healthz never reached upstream")
	}
}

func TestClientIPTakesRightmostXFF(t *testing.T) {
	// The reverse proxy appends the real client as the LAST hop; earlier hops
	// are spoofable and must be ignored.
	cases := []struct {
		name   string
		xff    string
		remote string
		wantIP string
	}{
		{"single xff", "203.0.113.7", "10.0.0.1:4000", "203.0.113.7"},
		{"multi hop takes rightmost", "spoofed-attacker, 203.0.113.7", "10.0.0.1:4000", "203.0.113.7"},
		{"no xff falls back to remote", "", "198.51.100.2:4000", "198.51.100.2"},
		{"blank xff falls back to remote", "  ", "198.51.100.2:4000", "198.51.100.2"},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			req := httptest.NewRequest(http.MethodGet, "/", nil)
			req.RemoteAddr = tc.remote
			if strings.TrimSpace(tc.xff) != "" {
				req.Header.Set("X-Forwarded-For", tc.xff)
			}
			if got := clientIP(req); got != tc.wantIP {
				t.Fatalf("clientIP: want %q, got %q", tc.wantIP, got)
			}
		})
	}
}

func TestRateLimiterConcurrency(t *testing.T) {
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	rl := newRateLimiter(ctx, 1000, 100) // generous; ensures no deadlock/panic under contention
	h := rl.middleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusOK)
	}))

	var wg sync.WaitGroup
	for i := 0; i < 20; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			rec := httptest.NewRecorder()
			h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/api/v1/entities", nil))
		}()
	}
	wg.Wait()
}

func TestNewRateLimiterFromConfigBurstDefault(t *testing.T) {
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()                             // release the sweep goroutine + ticker
	rl := newRateLimiterFromConfig(ctx, 10, 0) // burst unset → defaults to 2x
	if rl.burst != 20 {
		t.Fatalf("default burst should be 2x rps (20), got %d", rl.burst)
	}
}

// TestSweepStopsOnContextCancel verifies the ticker is released when the
// server context is cancelled (no process-lifetime goroutine/ticker leak).
func TestSweepStopsOnContextCancel(t *testing.T) {
	ctx, cancel := context.WithCancel(context.Background())
	rl := newRateLimiter(ctx, 1, 1)
	cancel()
	// Give the sweeper a moment to observe cancellation. It must return
	// without blocking; the deferred ticker.Stop() fires on return.
	time.Sleep(20 * time.Millisecond)
	// Limiter remains usable for the brief test lifetime.
	if !rl.get("10.0.0.1").Allow() {
		t.Fatal("limiter should still allow within burst after sweep stops")
	}
}
