package giftlimit

import (
	"context"
	"fmt"
	"os"
	"testing"
	"time"
)

// TestRedisLimiterIntegration 在真实 Redis 上验证三层聚合、加权扣减和租约时序。
// 常规 CI 没有 Redis 时跳过；本地用 GIFT_LIMIT_REDIS_ADDR=127.0.0.1:13379 显式执行。
func TestRedisLimiterIntegration(t *testing.T) {
	addr := os.Getenv("GIFT_LIMIT_REDIS_ADDR")
	if addr == "" {
		t.Skip("GIFT_LIMIT_REDIS_ADDR is not configured")
	}
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	client, err := NewRedisClient(ctx, addr, os.Getenv("GIFT_LIMIT_REDIS_PASSWORD"), 0)
	if err != nil {
		t.Fatalf("connect Redis: %v", err)
	}
	defer client.Close()

	prefix := fmt.Sprintf("test:gift:capacity:%d:", time.Now().UnixNano())
	t.Cleanup(func() {
		cleanupCtx, cleanupCancel := context.WithTimeout(context.Background(), 2*time.Second)
		defer cleanupCancel()
		var cursor uint64
		for {
			keys, next, scanErr := client.Scan(cleanupCtx, cursor, prefix+"*", 100).Result()
			if scanErr != nil {
				return
			}
			if len(keys) > 0 {
				_ = client.Del(cleanupCtx, keys...).Err()
			}
			cursor = next
			if cursor == 0 {
				return
			}
		}
	})

	// 小容量专门让测试在无等待情况下依次触发 App、房间和用户层；每个场景使用不同
	// app_code，避免前一场景的保守扣减影响后一场景。
	limiter := NewRedisLimiter(client, Config{
		Enabled:                   true,
		KeyPrefix:                 prefix,
		UserBucketCapacity:        2,
		UserRefillTokensPerSecond: 0.1,
		RoomBucketCapacity:        2,
		RoomRefillTokensPerSecond: 0.1,
		AppBucketCapacity:         3,
		AppRefillTokensPerSecond:  0.1,
		MaxInFlight:               1,
		InFlightLease:             300 * time.Millisecond,
		RedisOperationTimeout:     500 * time.Millisecond,
	})

	t.Run("app aggregate", func(t *testing.T) {
		first := mustAcquire(t, limiter, Input{AppCode: "app-total", UserID: 1, RoomID: "room-a", RequestID: "app-1", Weight: 2})
		mustRelease(t, limiter, first.Lease)
		second, err := limiter.Acquire(context.Background(), Input{AppCode: "app-total", UserID: 2, RoomID: "room-b", RequestID: "app-2", Weight: 2})
		if err != nil || second.Allowed || second.RetryAfter <= 0 {
			t.Fatalf("app aggregate must reject cross-room users: decision=%+v err=%v", second, err)
		}
	})

	t.Run("room aggregate across users", func(t *testing.T) {
		first := mustAcquire(t, limiter, Input{AppCode: "room-total", UserID: 11, RoomID: "same-room", RequestID: "room-1", Weight: 1})
		mustRelease(t, limiter, first.Lease)
		second := mustAcquire(t, limiter, Input{AppCode: "room-total", UserID: 12, RoomID: "same-room", RequestID: "room-2", Weight: 1})
		mustRelease(t, limiter, second.Lease)
		third, err := limiter.Acquire(context.Background(), Input{AppCode: "room-total", UserID: 13, RoomID: "same-room", RequestID: "room-3", Weight: 1})
		if err != nil || third.Allowed || third.RetryAfter <= 0 {
			t.Fatalf("room bucket must aggregate different users: decision=%+v err=%v", third, err)
		}
		// room 层拒绝后用户 lease 必须立即释放；用户 token 有意保留扣减，因此剩余的一个
		// token 仍可在另一个未满房间使用。
		afterRoomDenial := mustAcquire(t, limiter, Input{AppCode: "room-total", UserID: 13, RoomID: "other-room", RequestID: "room-4", Weight: 1})
		mustRelease(t, limiter, afterRoomDenial.Lease)
	})

	t.Run("user weighted", func(t *testing.T) {
		first := mustAcquire(t, limiter, Input{AppCode: "user-total", UserID: 21, RoomID: "room-a", RequestID: "user-1", Weight: 1})
		mustRelease(t, limiter, first.Lease)
		weighted, err := limiter.Acquire(context.Background(), Input{AppCode: "user-total", UserID: 21, RoomID: "room-b", RequestID: "user-2", Weight: 2})
		if err != nil || weighted.Allowed || weighted.RetryAfter < time.Second {
			t.Fatalf("user bucket must retain weighted consumption across rooms: decision=%+v err=%v", weighted, err)
		}
	})

	t.Run("in-flight lease does not expire early", func(t *testing.T) {
		leaseLimiter := NewRedisLimiter(client, Config{
			KeyPrefix:                 prefix,
			UserBucketCapacity:        100,
			UserRefillTokensPerSecond: 100,
			RoomBucketCapacity:        100,
			RoomRefillTokensPerSecond: 100,
			AppBucketCapacity:         100,
			AppRefillTokensPerSecond:  100,
			MaxInFlight:               1,
			InFlightLease:             300 * time.Millisecond,
			RedisOperationTimeout:     500 * time.Millisecond,
		})
		first := mustAcquire(t, leaseLimiter, Input{AppCode: "lease", UserID: 31, RoomID: "room-a", RequestID: "lease-1", Weight: 1})
		_ = first // 故意不释放，模拟 gateway 在 RPC 期间仍持有槽位。
		time.Sleep(100 * time.Millisecond)
		beforeExpiry, err := leaseLimiter.Acquire(context.Background(), Input{AppCode: "lease", UserID: 31, RoomID: "room-b", RequestID: "lease-2", Weight: 1})
		if err != nil || beforeExpiry.Allowed || beforeExpiry.RetryAfter < 100*time.Millisecond {
			t.Fatalf("lease expired before configured boundary: decision=%+v err=%v", beforeExpiry, err)
		}
		time.Sleep(230 * time.Millisecond)
		afterExpiry := mustAcquire(t, leaseLimiter, Input{AppCode: "lease", UserID: 31, RoomID: "room-c", RequestID: "lease-3", Weight: 1})
		mustRelease(t, leaseLimiter, afterExpiry.Lease)
	})
}

func mustAcquire(t *testing.T, limiter *RedisLimiter, input Input) Decision {
	t.Helper()
	decision, err := limiter.Acquire(context.Background(), input)
	if err != nil || !decision.Allowed {
		t.Fatalf("Acquire mismatch: input=%+v decision=%+v err=%v", input, decision, err)
	}
	return decision
}

func mustRelease(t *testing.T, limiter *RedisLimiter, lease Lease) {
	t.Helper()
	if err := limiter.Release(context.Background(), lease); err != nil {
		t.Fatalf("Release failed: %v", err)
	}
}
