package middleware

import (
	"context"
	"net/http"
	"net/http/httptest"
	"strconv"
	"testing"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/redis/go-redis/v9"
	"github.com/stretchr/testify/assert"
	"github.com/testcontainers/testcontainers-go"
	"github.com/testcontainers/testcontainers-go/wait"
)

// setupTestRedisContainer starts a Redis container for testing using Testcontainers
func setupTestRedisContainer(t *testing.T) (*redis.Client, func()) {
	t.Helper()
	ctx := context.Background()
	req := testcontainers.ContainerRequest{
		Image:        "redis:7-alpine",
		ExposedPorts: []string{"6379/tcp"},
		WaitingFor:   wait.ForLog("Ready to accept connections"),
	}

	redisC, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
		ContainerRequest: req,
		Started:          true,
	})
	if err != nil {
		t.Skipf("Could not start redis container (Docker might be missing): %v", err)
		return nil, func() {}
	}

	endpoint, err := redisC.Endpoint(ctx, "")
	if err != nil {
		t.Fatal(err)
	}

	client := redis.NewClient(&redis.Options{
		Addr: endpoint,
	})

	// Wait for Redis to be ready
	if err := client.Ping(ctx).Err(); err != nil {
		t.Fatal(err)
	}

	cleanup := func() {
		if err := redisC.Terminate(ctx); err != nil {
			t.Logf("failed to terminate container: %s", err)
		}
	}

	return client, cleanup
}

func TestLoginRateLimit_Enforcement(t *testing.T) {
	if testing.Short() {
		t.Skip("Skipping test in short mode")
	}

	redisClient, cleanup := setupTestRedisContainer(t)
	defer cleanup()

	// Setup Config
	config := &EndpointLimiterConfig{
		RedisClient: redisClient,
		KeyPrefix:   "test:login_limit",
	}
	limits := &EndpointLimits{
		LoginAttempts: 5,
		LoginWindow:   1 * time.Minute,
	}

	limiter := NewEndpointLimiter(config, limits)

	// Setup Gin
	gin.SetMode(gin.TestMode)
	router := gin.New()

	// Apply Middleware
	router.POST("/login", limiter.LoginRateLimit(), func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{"status": "ok"})
	})

	// 1. Success calls (5 attempts)
	for i := 0; i < 5; i++ {
		req, _ := http.NewRequest("POST", "/login", nil)
		w := httptest.NewRecorder()
		router.ServeHTTP(w, req)

		assert.Equal(t, http.StatusOK, w.Code, "Attempt %d should succeed", i+1)
		assert.Equal(t, "5", w.Header().Get("X-LoginLimit-Limit"))
		// Remaining should decrease
		assert.Equal(t, strconv.Itoa(4-i), w.Header().Get("X-LoginLimit-Remaining"))
	}

	// 2. Blocked call (6th attempt)
	req, _ := http.NewRequest("POST", "/login", nil)
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

	assert.Equal(t, http.StatusTooManyRequests, w.Code, "Attempt 6 should be blocked")
	assert.Equal(t, "5", w.Header().Get("X-LoginLimit-Limit"))
	assert.Equal(t, "0", w.Header().Get("X-LoginLimit-Remaining"))

	// Assuming formatted error response from middleware
	assert.Contains(t, w.Body.String(), "Too many login attempts")
}

func TestLoginRateLimit_RedisFailure_FailOpen(t *testing.T) {
	// Invalid Redis Client
	invalidClient := redis.NewClient(&redis.Options{
		Addr: "localhost:9999",
	})
	defer invalidClient.Close()

	config := &EndpointLimiterConfig{
		RedisClient: invalidClient,
		KeyPrefix:   "test:login_fail",
	}
	limits := &EndpointLimits{
		LoginAttempts: 1, // Strict limit to prove fail-open passes it
		LoginWindow:   1 * time.Minute,
	}
	limiter := NewEndpointLimiter(config, limits)

	gin.SetMode(gin.TestMode)
	router := gin.New()
	router.POST("/login", limiter.LoginRateLimit(), func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{"status": "ok"})
	})

	// Should pass despite Redis error (Fail Open)
	// We make multiple requests to ensure it never blocks due to error
	for i := 0; i < 3; i++ {
		req, _ := http.NewRequest("POST", "/login", nil)
		w := httptest.NewRecorder()
		router.ServeHTTP(w, req)
		assert.Equal(t, http.StatusOK, w.Code, "Should allow request when Redis is down")

		// Headers should NOT be present or remaining should be effectively infinite/unset
		// implementation detail: if err, we return headers are NOT set usually?
		// Checking implementation: headers ARE NOT set if err != nil (it just calls c.Next() and returns).
		assert.Empty(t, w.Header().Get("X-LoginLimit-Remaining"))
	}
}
