package userleaderboard

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

func TestQueryWindowUsesExistingUTCBoundaries(t *testing.T) {
	now := time.Date(2026, 5, 20, 15, 30, 0, 0, time.FixedZone("CST", 8*60*60))

	todayStart, todayEnd := QueryWindow(PeriodToday, now)
	if todayStart != time.Date(2026, 5, 20, 0, 0, 0, 0, time.UTC) {
		t.Fatalf("today start = %s", todayStart)
	}
	if !todayEnd.Equal(now.UTC()) {
		t.Fatalf("today end = %s", todayEnd)
	}

	weekStart, _ := QueryWindow(PeriodWeek, now)
	if weekStart != time.Date(2026, 5, 18, 0, 0, 0, 0, time.UTC) {
		t.Fatalf("week start = %s", weekStart)
	}

	monthStart, _ := QueryWindow(PeriodMonth, now)
	if monthStart != time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC) {
		t.Fatalf("month start = %s", monthStart)
	}
}

func TestNormalizeAliases(t *testing.T) {
	if NormalizeBoardType("receiver") != BoardReceived {
		t.Fatal("receiver should normalize to received")
	}
	if NormalizeBoardType("gift_sent") != BoardSent {
		t.Fatal("gift_sent should normalize to sent")
	}
	if NormalizeBoardType("gaming") != BoardGame {
		t.Fatal("gaming should normalize to game")
	}
	if NormalizePeriod("monthly") != PeriodMonth {
		t.Fatal("monthly should normalize to month")
	}
}

func TestStoreKeyPrefixAndBucketAreStable(t *testing.T) {
	store := NewStore(nil, "activity:user_leaderboard:")
	weekStart := time.Date(2026, 12, 28, 0, 0, 0, 0, time.UTC)

	if got := store.scoreKey(" HyApp_Prod ", BoardSent, PeriodWeek, weekStart); got != "activity:user_leaderboard:hyapp_prod:sent:week:202653:scores" {
		t.Fatalf("score key mismatch: %s", got)
	}
	if got := store.itemKey("hyapp_prod", BoardReceived, PeriodToday, time.Date(2026, 6, 26, 0, 0, 0, 0, time.UTC), "10001"); got != "activity:user_leaderboard:hyapp_prod:received:today:20260626:users:10001" {
		t.Fatalf("item key mismatch: %s", got)
	}
	if got := store.scoreKey("huwaa", BoardGame, PeriodMonth, time.Date(2026, 7, 1, 0, 0, 0, 0, time.UTC)); got != "activity:user_leaderboard:huwaa:game:month:202607:scores" {
		t.Fatalf("game score key mismatch: %s", got)
	}
}

func TestApplyGameEventUsesDedicatedRedisBucket(t *testing.T) {
	redisAddr := os.Getenv("HYAPP_TEST_REDIS_ADDR")
	if redisAddr == "" {
		t.Skip("HYAPP_TEST_REDIS_ADDR is not set")
	}
	ctx := context.Background()
	client, err := NewRedisClient(ctx, redisAddr, "", 0)
	if err != nil {
		t.Fatalf("connect test Redis: %v", err)
	}
	defer client.Close()
	prefix := fmt.Sprintf("test:user_leaderboard:%d", time.Now().UnixNano())
	defer func() {
		keys, _, scanErr := client.Scan(ctx, 0, prefix+":*", 100).Result()
		if scanErr == nil && len(keys) > 0 {
			_ = client.Del(ctx, keys...).Err()
		}
	}()
	store := NewStore(client, prefix)
	occurredAt := time.Date(2026, 7, 15, 12, 0, 0, 0, time.UTC)
	event := GameEvent{
		AppCode:      "huwaa",
		EventID:      "game_level:test_order_1",
		UserID:       20002,
		RegionID:     9001,
		CoinSpent:    120,
		OccurredAtMS: occurredAt.UnixMilli(),
	}

	applied, err := store.ApplyGameEvent(ctx, event)
	if err != nil || !applied {
		t.Fatalf("first game event should apply: applied=%v err=%v", applied, err)
	}
	applied, err = store.ApplyGameEvent(ctx, event)
	if err != nil || applied {
		t.Fatalf("duplicate game event should no-op: applied=%v err=%v", applied, err)
	}
	page, err := store.List(ctx, Query{
		AppCode:   "huwaa",
		BoardType: BoardGame,
		Period:    PeriodToday,
		Page:      1,
		PageSize:  20,
		Now:       occurredAt.Add(time.Minute),
	})
	if err != nil {
		t.Fatalf("list game leaderboard: %v", err)
	}
	if page.BoardType != BoardGame || page.Total != 1 || len(page.Items) != 1 || page.Items[0].UserID != "20002" || page.Items[0].GiftValue != 120 || page.Items[0].GiftCount != 0 || page.Items[0].TransactionCount != 1 || page.Items[0].LastGiftAtMS != occurredAt.UnixMilli() {
		t.Fatalf("game leaderboard item mismatch: %+v", page)
	}
	regionalPage, err := store.List(ctx, Query{AppCode: "huwaa", RegionID: 9001, BoardType: BoardGame, Period: PeriodToday, Page: 1, PageSize: 20, Now: occurredAt.Add(time.Minute)})
	if err != nil || regionalPage.Total != 1 || len(regionalPage.Items) != 1 || regionalPage.Items[0].GiftValue != 120 {
		t.Fatalf("game event must maintain the regional projection beside the global projection: page=%+v err=%v", regionalPage, err)
	}
	sent, err := store.List(ctx, Query{AppCode: "huwaa", BoardType: BoardSent, Period: PeriodToday, Page: 1, PageSize: 20, Now: occurredAt.Add(time.Minute)})
	if err != nil || sent.Total != 0 {
		t.Fatalf("game event must not leak into sent board: page=%+v err=%v", sent, err)
	}
}

func TestScopedKeysDependOnlyOnRequestedRegion(t *testing.T) {
	store := NewStore(nil, "activity:user_leaderboard")
	start := time.Date(2026, 7, 21, 0, 0, 0, 0, time.UTC)
	global := store.scopedScoreKey("future-app", 0, BoardSent, PeriodToday, start)
	regional := store.scopedScoreKey("future-app", 7001, BoardSent, PeriodToday, start)
	if global != "activity:user_leaderboard:future-app:sent:today:20260721:scores" {
		t.Fatalf("global key mismatch: %s", global)
	}
	if regional != "activity:user_leaderboard:future-app:region:7001:sent:today:20260721:scores" {
		t.Fatalf("regional key must not depend on a product allowlist: %s", regional)
	}
}
