package activityapi

import (
	"context"
	"encoding/json"
	"errors"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"

	activityv1 "hyapp.local/api/proto/activity/v1"
	userv1 "hyapp.local/api/proto/user/v1"
	"hyapp/pkg/appcode"
	"hyapp/services/gateway-service/internal/auth"
)

func TestGetCurrentActivityTemplateUsesAuthenticatedProfileRegionAndPublishedDTO(t *testing.T) {
	runtimeClient := &fakeActivityTemplateRuntimeClient{
		currentResp: &activityv1.GetCurrentActivityTemplateResponse{
			Found: true,
			Runtime: &activityv1.ActivityTemplateRuntime{
				VersionNo: 4, RuntimeFromMs: 1000, RuntimeToMs: 9000, ServerTimeMs: 2000,
				Template: &activityv1.ActivityTemplate{
					TemplateId: "tpl-1", TemplateCode: "ramadan-2026", Name: "admin-only-name",
					ActivityType: "gift_challenge", Status: "published", LifecycleStatus: "running",
					StartMs: 1000, EndMs: 9000, AllRegions: false, RegionIds: []int64{901},
					Locales: []*activityv1.ActivityTemplateLocale{
						{Locale: "en", Title: "Gift challenge", Rules: "rules-en"},
						{Locale: "ar", Title: "تحدي الهدايا", Rules: "rules-ar"},
					},
					Gifts:             []*activityv1.ActivityTemplateGift{{GiftId: "gift-1", Name: "Rose", IconUrl: "rose.png", CoinPrice: 20, PriceVersion: "v3"}},
					Tasks:             []*activityv1.ActivityTemplateTask{{TaskKey: "visit", TaskType: "daily_visit", TargetValue: 1, RewardResourceGroupId: 81}},
					RankRewards:       []*activityv1.ActivityTemplateRankReward{{BoardType: "daily", RankFrom: 1, RankTo: 1, ResourceGroupId: 82}},
					Assets:            []*activityv1.ActivityTemplateAsset{{AssetKey: "hero", Locale: "ar", Url: "hero.webp", MediaType: "webp"}},
					DisplayConfigJson: `{"theme":"gold"}`, Revision: 99, CreatedByAdminId: 7,
				},
			},
		},
	}
	profileClient := &fakeActivityTemplateProfileClient{regionID: 901}
	handler := New(Config{ActivityTemplateRuntime: runtimeClient, UserProfileClient: profileClient})
	request := activityTemplateRequest(http.MethodGet, "/api/v1/activities/templates/current?locale=ar-EG&region_id=999999", "", 42001)
	recorder := httptest.NewRecorder()

	handler.getCurrentActivityTemplate(recorder, request)

	if recorder.Code != http.StatusOK {
		t.Fatalf("status mismatch: got=%d body=%s", recorder.Code, recorder.Body.String())
	}
	if profileClient.lastGet.GetUserId() != 42001 || profileClient.lastGet.GetMeta().GetAppCode() != "lalu" {
		t.Fatalf("trusted region profile request mismatch: %+v", profileClient.lastGet)
	}
	if runtimeClient.lastCurrent.GetUserId() != 42001 || runtimeClient.lastCurrent.GetRegionId() != 901 || runtimeClient.lastCurrent.GetLocale() != "ar-EG" {
		t.Fatalf("runtime request mismatch: %+v", runtimeClient.lastCurrent)
	}
	if runtimeClient.lastCurrent.GetMeta().GetAppCode() != "lalu" || runtimeClient.lastCurrent.GetMeta().GetRequestId() == "" {
		t.Fatalf("activity meta mismatch: %+v", runtimeClient.lastCurrent.GetMeta())
	}

	var envelope struct {
		Code string `json:"code"`
		Data struct {
			Found   bool `json:"found"`
			Runtime struct {
				TemplateID string `json:"template_id"`
				VersionNo  int64  `json:"version_no"`
				Locale     struct {
					Locale string `json:"locale"`
					Title  string `json:"title"`
				} `json:"locale"`
				DisplayConfig map[string]any `json:"display_config"`
			} `json:"runtime"`
		} `json:"data"`
	}
	if err := json.Unmarshal(recorder.Body.Bytes(), &envelope); err != nil {
		t.Fatalf("decode response: %v", err)
	}
	if envelope.Code != "OK" || !envelope.Data.Found || envelope.Data.Runtime.TemplateID != "tpl-1" || envelope.Data.Runtime.VersionNo != 4 {
		t.Fatalf("runtime envelope mismatch: %+v", envelope)
	}
	if envelope.Data.Runtime.Locale.Locale != "ar" || envelope.Data.Runtime.Locale.Title != "تحدي الهدايا" || envelope.Data.Runtime.DisplayConfig["theme"] != "gold" {
		t.Fatalf("locale/display config mismatch: %+v", envelope.Data.Runtime)
	}
	var rawEnvelope map[string]any
	if err := json.Unmarshal(recorder.Body.Bytes(), &rawEnvelope); err != nil {
		t.Fatalf("decode raw response: %v", err)
	}
	runtime := rawEnvelope["data"].(map[string]any)["runtime"].(map[string]any)
	for _, adminField := range []string{"name", "revision", "created_by_admin_id", "updated_by_admin_id", "published_by_admin_id"} {
		if _, exists := runtime[adminField]; exists {
			t.Fatalf("app runtime leaked admin field %q: %+v", adminField, runtime)
		}
	}
}

func TestCurrentActivityTemplateLocaleFallsBackToEnglish(t *testing.T) {
	runtimeClient := &fakeActivityTemplateRuntimeClient{
		currentResp: &activityv1.GetCurrentActivityTemplateResponse{
			Found: true,
			Runtime: &activityv1.ActivityTemplateRuntime{Template: &activityv1.ActivityTemplate{
				TemplateId: "tpl-1",
				Locales: []*activityv1.ActivityTemplateLocale{
					{Locale: "ar", Title: "العربية"},
					{Locale: "en", Title: "English fallback"},
				},
			}},
		},
	}
	profileClient := &fakeActivityTemplateProfileClient{regionID: 901}
	handler := New(Config{ActivityTemplateRuntime: runtimeClient, UserProfileClient: profileClient})
	request := activityTemplateRequest(http.MethodGet, "/api/v1/activities/templates/current", "", 42001)
	request.Header.Set("Accept-Language", "zh-CN,zh;q=0.9")
	recorder := httptest.NewRecorder()

	handler.getCurrentActivityTemplate(recorder, request)

	if recorder.Code != http.StatusOK || runtimeClient.lastCurrent.GetLocale() != "zh-CN" {
		t.Fatalf("requested locale should be normalized before owner call: code=%d req=%+v body=%s", recorder.Code, runtimeClient.lastCurrent, recorder.Body.String())
	}
	var envelope struct {
		Data struct {
			Runtime struct {
				Locale struct {
					Locale string `json:"locale"`
					Title  string `json:"title"`
				} `json:"locale"`
			} `json:"runtime"`
		} `json:"data"`
	}
	if err := json.Unmarshal(recorder.Body.Bytes(), &envelope); err != nil {
		t.Fatalf("decode response: %v", err)
	}
	if envelope.Data.Runtime.Locale.Locale != "en" || envelope.Data.Runtime.Locale.Title != "English fallback" {
		t.Fatalf("unsupported locale must fall back to published English copy: %+v", envelope.Data.Runtime.Locale)
	}
}

func TestActivityTemplateRequestLocalePreservesCanonicalBCP47AndSourcePriority(t *testing.T) {
	tests := []struct {
		name           string
		target         string
		appLanguage    string
		acceptLanguage string
		want           string
	}{
		{
			name: "query keeps Chinese region", target: "/current?locale=zh-cn",
			appLanguage: "pt-BR", acceptLanguage: "ar;q=1", want: "zh-CN",
		},
		{
			name: "app header keeps Portuguese region", target: "/current",
			appLanguage: "pt-br", acceptLanguage: "zh-CN;q=1", want: "pt-BR",
		},
		{
			name: "accept language uses highest quality", target: "/current",
			acceptLanguage: "fr-FR;q=0.2, zh-CN;q=0.7, pt-BR;q=0.9", want: "pt-BR",
		},
		{
			name: "equal quality keeps client order", target: "/current",
			acceptLanguage: "zh-CN;q=0.8, pt-BR;q=0.8", want: "zh-CN",
		},
		{
			name: "invalid tags and q zero are never forwarded", target: "/current?locale=bad_tag",
			appLanguage: "pt@@BR", acceptLanguage: "zh_CN;q=1, pt-BR;q=0, ar;q=NaN, en-US;q=0.5", want: "en-US",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			request := httptest.NewRequest(http.MethodGet, tt.target, nil)
			request.Header.Set("X-App-Language", tt.appLanguage)
			request.Header.Set("Accept-Language", tt.acceptLanguage)
			if got := activityTemplateRequestLocale(request); got != tt.want {
				t.Fatalf("activityTemplateRequestLocale() = %q, want %q", got, tt.want)
			}
		})
	}
}

func TestSelectActivityTemplateLocaleUsesExactBaseSameLanguageThenEnglish(t *testing.T) {
	tests := []struct {
		name      string
		requested string
		locales   []activityTemplateLocaleData
		want      string
	}{
		{
			name: "exact region", requested: "zh-CN",
			locales: []activityTemplateLocaleData{{Locale: "zh-TW"}, {Locale: "zh-CN"}, {Locale: "en"}}, want: "zh-CN",
		},
		{
			name: "language only before regional candidate", requested: "pt-AO",
			locales: []activityTemplateLocaleData{{Locale: "pt-BR"}, {Locale: "pt"}, {Locale: "en"}}, want: "pt",
		},
		{
			name: "same language uses configured order", requested: "pt-AO",
			locales: []activityTemplateLocaleData{{Locale: "pt-BR"}, {Locale: "pt-PT"}, {Locale: "en"}}, want: "pt-BR",
		},
		{
			name: "English after no language match", requested: "zh-CN",
			locales: []activityTemplateLocaleData{{Locale: "ar"}, {Locale: "en"}}, want: "en",
		},
		{
			name: "first after no English", requested: "zh-CN",
			locales: []activityTemplateLocaleData{{Locale: "ar"}, {Locale: "pt-BR"}}, want: "ar",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			selected, found := selectActivityTemplateLocale(tt.locales, tt.requested)
			if !found || selected.Locale != tt.want {
				t.Fatalf("selectActivityTemplateLocale() = (%+v, %v), want locale %q", selected, found, tt.want)
			}
		})
	}
}

func TestActivityTemplateRuntimeDropsInvalidPublishedLocaleTags(t *testing.T) {
	runtime := activityTemplateRuntimeFromProto(&activityv1.ActivityTemplateRuntime{Template: &activityv1.ActivityTemplate{
		TemplateId: "tpl-1",
		Locales: []*activityv1.ActivityTemplateLocale{
			{Locale: "bad_tag", Title: "must not leak"},
			{Locale: "pt-br", Title: "Português"},
		},
		Assets: []*activityv1.ActivityTemplateAsset{
			{AssetKey: "invalid", Locale: "zh_CN", Url: "invalid.webp"},
			{AssetKey: "valid", Locale: "pt-br", Url: "valid.webp"},
		},
	}}, "pt-BR")
	if len(runtime.Locales) != 1 || runtime.Locales[0].Locale != "pt-BR" || runtime.Locale == nil || runtime.Locale.Locale != "pt-BR" {
		t.Fatalf("runtime locale sanitization mismatch: %+v", runtime)
	}
	if len(runtime.Assets) != 1 || runtime.Assets[0].Locale != "pt-BR" || runtime.Assets[0].AssetKey != "valid" {
		t.Fatalf("runtime asset locale sanitization mismatch: %+v", runtime.Assets)
	}
}

func TestVisitActivityTemplateKeepsClientCommandSeparateFromRequestID(t *testing.T) {
	runtimeClient := &fakeActivityTemplateRuntimeClient{visitResp: &activityv1.VisitActivityTemplateResponse{
		FirstVisit: true,
		Tasks:      []*activityv1.ActivityTemplateRuntimeTask{{TemplateId: "tpl-1", TemplateCode: "summer", TaskDay: "2026-07-14", TaskKey: "visit", Status: "completed", Claimable: true}},
	}}
	handler := New(Config{ActivityTemplateRuntime: runtimeClient, UserProfileClient: &fakeActivityTemplateProfileClient{regionID: 66}})
	request := activityTemplateRequest(http.MethodPost, "/api/v1/activities/templates/summer/visit", `{"command_id":" visit-device-7 ","version_no":4}`, 777)
	request.SetPathValue("template_code", "summer")
	recorder := httptest.NewRecorder()

	handler.visitActivityTemplate(recorder, request)

	if recorder.Code != http.StatusOK {
		t.Fatalf("status mismatch: got=%d body=%s", recorder.Code, recorder.Body.String())
	}
	req := runtimeClient.lastVisit
	if req.GetUserId() != 777 || req.GetRegionId() != 66 || req.GetTemplateCode() != "summer" || req.GetCommandId() != "visit-device-7" || req.GetVersionNo() != 4 {
		t.Fatalf("visit request mismatch: %+v", req)
	}
	if req.GetMeta().GetRequestId() == "" || req.GetMeta().GetRequestId() == req.GetCommandId() {
		t.Fatalf("request_id and command_id must have independent semantics: meta=%+v command=%q", req.GetMeta(), req.GetCommandId())
	}
	var envelope struct {
		Data struct {
			FirstVisit bool `json:"first_visit"`
			Tasks      []struct {
				TaskDay   string `json:"task_day"`
				Claimable bool   `json:"claimable"`
			} `json:"tasks"`
		} `json:"data"`
	}
	if err := json.Unmarshal(recorder.Body.Bytes(), &envelope); err != nil || !envelope.Data.FirstVisit || len(envelope.Data.Tasks) != 1 || !envelope.Data.Tasks[0].Claimable {
		t.Fatalf("visit response mismatch: err=%v data=%+v", err, envelope.Data)
	}
}

func TestActivityTemplateTasksAcceptOnlyCanonicalUTCDay(t *testing.T) {
	t.Run("forwards canonical UTC day", func(t *testing.T) {
		runtimeClient := &fakeActivityTemplateRuntimeClient{tasksResp: &activityv1.ListActivityTemplateTasksResponse{NextRefreshAtMs: 1784159999000}}
		handler := New(Config{ActivityTemplateRuntime: runtimeClient, UserProfileClient: &fakeActivityTemplateProfileClient{regionID: 22}})
		request := activityTemplateRequest(http.MethodGet, "/api/v1/activities/templates/summer/tasks?task_day=2026-07-14", "", 88)
		request.SetPathValue("template_code", "summer")
		recorder := httptest.NewRecorder()

		handler.listActivityTemplateTasks(recorder, request)

		if recorder.Code != http.StatusOK || runtimeClient.lastTasks.GetTaskDay() != "2026-07-14" {
			t.Fatalf("canonical UTC day mismatch: code=%d req=%+v body=%s", recorder.Code, runtimeClient.lastTasks, recorder.Body.String())
		}
	})

	for _, invalid := range []string{"2026-7-14", "2026-07-14T00:00:00Z", "2026-02-30"} {
		t.Run(invalid, func(t *testing.T) {
			runtimeClient := &fakeActivityTemplateRuntimeClient{}
			handler := New(Config{ActivityTemplateRuntime: runtimeClient, UserProfileClient: &fakeActivityTemplateProfileClient{regionID: 22}})
			request := activityTemplateRequest(http.MethodGet, "/api/v1/activities/templates/summer/tasks?task_day="+invalid, "", 88)
			request.SetPathValue("template_code", "summer")
			recorder := httptest.NewRecorder()

			handler.listActivityTemplateTasks(recorder, request)

			if recorder.Code != http.StatusBadRequest || runtimeClient.lastTasks != nil {
				t.Fatalf("invalid day should stop before gRPC: code=%d req=%+v body=%s", recorder.Code, runtimeClient.lastTasks, recorder.Body.String())
			}
		})
	}
}

func TestHistoricalActivityTemplateTasksUseOwnedProgressAttributionInsteadOfCurrentProfileRegion(t *testing.T) {
	runtimeClient := &fakeActivityTemplateRuntimeClient{tasksResp: &activityv1.ListActivityTemplateTasksResponse{}}
	profileClient := &fakeActivityTemplateProfileClient{regionID: 999}
	handler := New(Config{ActivityTemplateRuntime: runtimeClient, UserProfileClient: profileClient})
	request := activityTemplateRequest(http.MethodGet, "/api/v1/activities/templates/summer/tasks?version_no=3&task_day=2026-07-14", "", 88)
	request.SetPathValue("template_code", "summer")
	recorder := httptest.NewRecorder()

	handler.listActivityTemplateTasks(recorder, request)

	if recorder.Code != http.StatusOK || runtimeClient.lastTasks.GetVersionNo() != 3 || runtimeClient.lastTasks.GetRegionId() != 0 {
		t.Fatalf("historical task lookup must be owner-attributed: code=%d req=%+v body=%s", recorder.Code, runtimeClient.lastTasks, recorder.Body.String())
	}
	if profileClient.lastGet != nil {
		t.Fatalf("historical task lookup must not resolve mutable current region: %+v", profileClient.lastGet)
	}
}

func TestClaimActivityTemplateTaskUsesAuthenticatedUserAndExplicitIdempotency(t *testing.T) {
	runtimeClient := &fakeActivityTemplateRuntimeClient{claimResp: &activityv1.ClaimActivityTemplateTaskRewardResponse{
		Claimed: true,
		Claim:   &activityv1.ActivityTemplateTaskClaim{ClaimId: "claim-1", CommandId: "claim-device-1", UserId: 908, TaskDay: "2026-07-14", TaskKey: "gift-10", Status: "granted"},
	}}
	handler := New(Config{ActivityTemplateRuntime: runtimeClient, UserProfileClient: &fakeActivityTemplateProfileClient{regionID: 33}})
	request := activityTemplateRequest(http.MethodPost, "/api/v1/activities/templates/summer/tasks/gift-10/claim", `{"task_day":"2026-07-14","command_id":"claim-device-1","version_no":3,"user_id":1,"region_id":1}`, 908)
	request.SetPathValue("template_code", "summer")
	request.SetPathValue("task_key", "gift-10")
	recorder := httptest.NewRecorder()

	handler.claimActivityTemplateTask(recorder, request)

	if recorder.Code != http.StatusOK {
		t.Fatalf("status mismatch: got=%d body=%s", recorder.Code, recorder.Body.String())
	}
	req := runtimeClient.lastClaim
	if req.GetUserId() != 908 || req.GetRegionId() != 0 || req.GetVersionNo() != 3 || req.GetTaskDay() != "2026-07-14" || req.GetTaskKey() != "gift-10" || req.GetCommandId() != "claim-device-1" {
		t.Fatalf("claim request trusted fields mismatch: %+v", req)
	}
	if profile := handler.userProfileClient.(*fakeActivityTemplateProfileClient); profile.lastGet != nil {
		t.Fatalf("exact-version claim must not resolve mutable current region: %+v", profile.lastGet)
	}
	var envelope struct {
		Data struct {
			Claimed bool `json:"claimed"`
			Claim   struct {
				UserID string `json:"user_id"`
				Status string `json:"status"`
			} `json:"claim"`
		} `json:"data"`
	}
	if err := json.Unmarshal(recorder.Body.Bytes(), &envelope); err != nil || !envelope.Data.Claimed || envelope.Data.Claim.UserID != "908" || envelope.Data.Claim.Status != "granted" {
		t.Fatalf("claim response mismatch: err=%v data=%+v", err, envelope.Data)
	}
}

func TestActivityTemplateLeaderboardClampsPageAndBatchEnrichesProfiles(t *testing.T) {
	runtimeClient := &fakeActivityTemplateRuntimeClient{leaderboardResp: &activityv1.ListActivityTemplateLeaderboardResponse{
		BoardType: "daily", PeriodKey: "2026-07-14", PeriodStartMs: 1000, PeriodEndMs: 2000, PeriodStatus: "pending", Total: 2,
		Entries: []*activityv1.ActivityTemplateLeaderboardEntry{{RankNo: 1, UserId: 1001, Score: 800, GiftCount: 8, CoinAmount: 800, FirstScoredAtMs: 1100}},
		MyEntry: &activityv1.ActivityTemplateLeaderboardEntry{RankNo: 2, UserId: 2002, Score: 700, GiftCount: 7, CoinAmount: 700, FirstScoredAtMs: 1200},
	}}
	profileClient := &fakeActivityTemplateProfileClient{
		regionID: 45,
		users: map[int64]*userv1.User{
			1001: {UserId: 1001, DisplayUserId: "L1001", Username: "first", Avatar: "first.png"},
			2002: {UserId: 2002, DisplayUserId: "L2002", Username: "me", Avatar: "me.png"},
		},
	}
	handler := New(Config{ActivityTemplateRuntime: runtimeClient, UserProfileClient: profileClient})
	request := activityTemplateRequest(http.MethodGet, "/api/v1/activities/templates/summer/leaderboards/daily?version_no=4&period_key=2026-07-14&page=2&page_size=999", "", 2002)
	request.SetPathValue("template_code", "summer")
	recorder := httptest.NewRecorder()

	handler.listActivityTemplateDailyLeaderboard(recorder, request)

	if recorder.Code != http.StatusOK {
		t.Fatalf("status mismatch: got=%d body=%s", recorder.Code, recorder.Body.String())
	}
	req := runtimeClient.lastLeaderboard
	if req.GetUserId() != 2002 || req.GetRegionId() != 0 || req.GetVersionNo() != 4 || req.GetBoardType() != "daily" || req.GetPeriodKey() != "2026-07-14" || req.GetPage() != 2 || req.GetPageSize() != 100 {
		t.Fatalf("leaderboard request mismatch: %+v", req)
	}
	if profileClient.lastGet != nil {
		t.Fatalf("exact-version leaderboard must not resolve mutable current region: %+v", profileClient.lastGet)
	}
	if got := profileClient.lastBatch.GetUserIds(); len(got) != 2 || got[0] != 1001 || got[1] != 2002 {
		t.Fatalf("profile batch should contain page and my-entry users once: %+v", got)
	}
	var envelope struct {
		Data struct {
			Page     int32 `json:"page"`
			PageSize int32 `json:"page_size"`
			Entries  []struct {
				UserID string `json:"user_id"`
				User   struct {
					DisplayUserID string `json:"display_user_id"`
					Username      string `json:"username"`
				} `json:"user"`
			} `json:"entries"`
			MyEntry struct {
				User struct {
					Avatar string `json:"avatar"`
				} `json:"user"`
			} `json:"my_entry"`
		} `json:"data"`
	}
	if err := json.Unmarshal(recorder.Body.Bytes(), &envelope); err != nil {
		t.Fatalf("decode response: %v", err)
	}
	if envelope.Data.Page != 2 || envelope.Data.PageSize != 100 || len(envelope.Data.Entries) != 1 || envelope.Data.Entries[0].UserID != "1001" || envelope.Data.Entries[0].User.DisplayUserID != "L1001" || envelope.Data.MyEntry.User.Avatar != "me.png" {
		t.Fatalf("leaderboard response mismatch: %+v", envelope.Data)
	}
}

func TestDailyLeaderboardForwardsExplicitHistoricalUTCPeriod(t *testing.T) {
	runtimeClient := &fakeActivityTemplateRuntimeClient{leaderboardResp: &activityv1.ListActivityTemplateLeaderboardResponse{
		BoardType: "daily", PeriodKey: "2026-06-01", PeriodStatus: "settled",
		Entries: []*activityv1.ActivityTemplateLeaderboardEntry{{RankNo: 1, UserId: 1001, Snapshot: true}},
	}}
	handler := New(Config{ActivityTemplateRuntime: runtimeClient, UserProfileClient: &fakeActivityTemplateProfileClient{regionID: 45}})
	request := activityTemplateRequest(http.MethodGet, "/api/v1/activities/templates/summer/leaderboards/daily?period_key=2026-06-01", "", 2002)
	request.SetPathValue("template_code", "summer")
	recorder := httptest.NewRecorder()

	handler.listActivityTemplateDailyLeaderboard(recorder, request)

	if recorder.Code != http.StatusOK {
		t.Fatalf("settled historical period should reach owner: code=%d body=%s", recorder.Code, recorder.Body.String())
	}
	if runtimeClient.lastLeaderboard.GetPeriodKey() != "2026-06-01" || runtimeClient.lastLeaderboard.GetBoardType() != "daily" {
		t.Fatalf("gateway must preserve explicit historical UTC period: %+v", runtimeClient.lastLeaderboard)
	}
}

func TestDailyLeaderboardRejectsNonCanonicalUTCPeriod(t *testing.T) {
	for _, invalid := range []string{"2026-6-1", "2026-06-01T00:00:00Z", "2026-02-30"} {
		t.Run(invalid, func(t *testing.T) {
			runtimeClient := &fakeActivityTemplateRuntimeClient{}
			handler := New(Config{ActivityTemplateRuntime: runtimeClient, UserProfileClient: &fakeActivityTemplateProfileClient{regionID: 45}})
			request := activityTemplateRequest(http.MethodGet, "/api/v1/activities/templates/summer/leaderboards/daily?period_key="+invalid, "", 2002)
			request.SetPathValue("template_code", "summer")
			recorder := httptest.NewRecorder()

			handler.listActivityTemplateDailyLeaderboard(recorder, request)

			if recorder.Code != http.StatusBadRequest || runtimeClient.lastLeaderboard != nil {
				t.Fatalf("non-canonical UTC period must stop before owner: code=%d req=%+v body=%s", recorder.Code, runtimeClient.lastLeaderboard, recorder.Body.String())
			}
		})
	}
}

func TestActivityTemplateLeaderboardProfileDecorationFailureDegradesToFacts(t *testing.T) {
	runtimeClient := &fakeActivityTemplateRuntimeClient{leaderboardResp: &activityv1.ListActivityTemplateLeaderboardResponse{
		BoardType: "total", PeriodKey: "total", Total: 1,
		Entries: []*activityv1.ActivityTemplateLeaderboardEntry{{RankNo: 1, UserId: 1001, Score: 800}},
	}}
	profileClient := &fakeActivityTemplateProfileClient{regionID: 45, batchErr: errors.New("user profile unavailable")}
	handler := New(Config{ActivityTemplateRuntime: runtimeClient, UserProfileClient: profileClient})
	request := activityTemplateRequest(http.MethodGet, "/api/v1/activities/templates/summer/leaderboards/total", "", 2002)
	request.SetPathValue("template_code", "summer")
	recorder := httptest.NewRecorder()

	handler.listActivityTemplateTotalLeaderboard(recorder, request)

	if recorder.Code != http.StatusOK || runtimeClient.lastLeaderboard.GetPeriodKey() != "total" {
		t.Fatalf("profile decoration must not hide leaderboard facts: code=%d req=%+v body=%s", recorder.Code, runtimeClient.lastLeaderboard, recorder.Body.String())
	}
	var envelope struct {
		Data struct {
			Entries []map[string]any `json:"entries"`
		} `json:"data"`
	}
	if err := json.Unmarshal(recorder.Body.Bytes(), &envelope); err != nil || len(envelope.Data.Entries) != 1 {
		t.Fatalf("decode facts response: err=%v data=%+v", err, envelope.Data)
	}
	if _, decorated := envelope.Data.Entries[0]["user"]; decorated {
		t.Fatalf("failed best-effort profile lookup should omit user decoration: %+v", envelope.Data.Entries[0])
	}
}

func activityTemplateRequest(method, target, body string, userID int64) *http.Request {
	request := httptest.NewRequest(method, target, strings.NewReader(body))
	ctx := appcode.WithContext(request.Context(), "lalu")
	ctx = auth.WithUserID(ctx, userID)
	return request.WithContext(ctx)
}

type fakeActivityTemplateRuntimeClient struct {
	currentResp     *activityv1.GetCurrentActivityTemplateResponse
	visitResp       *activityv1.VisitActivityTemplateResponse
	tasksResp       *activityv1.ListActivityTemplateTasksResponse
	claimResp       *activityv1.ClaimActivityTemplateTaskRewardResponse
	leaderboardResp *activityv1.ListActivityTemplateLeaderboardResponse

	lastCurrent     *activityv1.GetCurrentActivityTemplateRequest
	lastVisit       *activityv1.VisitActivityTemplateRequest
	lastTasks       *activityv1.ListActivityTemplateTasksRequest
	lastClaim       *activityv1.ClaimActivityTemplateTaskRewardRequest
	lastLeaderboard *activityv1.ListActivityTemplateLeaderboardRequest
}

func (f *fakeActivityTemplateRuntimeClient) GetCurrentActivityTemplate(_ context.Context, req *activityv1.GetCurrentActivityTemplateRequest) (*activityv1.GetCurrentActivityTemplateResponse, error) {
	f.lastCurrent = req
	if f.currentResp != nil {
		return f.currentResp, nil
	}
	return &activityv1.GetCurrentActivityTemplateResponse{}, nil
}

func (f *fakeActivityTemplateRuntimeClient) VisitActivityTemplate(_ context.Context, req *activityv1.VisitActivityTemplateRequest) (*activityv1.VisitActivityTemplateResponse, error) {
	f.lastVisit = req
	if f.visitResp != nil {
		return f.visitResp, nil
	}
	return &activityv1.VisitActivityTemplateResponse{}, nil
}

func (f *fakeActivityTemplateRuntimeClient) ListActivityTemplateTasks(_ context.Context, req *activityv1.ListActivityTemplateTasksRequest) (*activityv1.ListActivityTemplateTasksResponse, error) {
	f.lastTasks = req
	if f.tasksResp != nil {
		return f.tasksResp, nil
	}
	return &activityv1.ListActivityTemplateTasksResponse{}, nil
}

func (f *fakeActivityTemplateRuntimeClient) ClaimActivityTemplateTaskReward(_ context.Context, req *activityv1.ClaimActivityTemplateTaskRewardRequest) (*activityv1.ClaimActivityTemplateTaskRewardResponse, error) {
	f.lastClaim = req
	if f.claimResp != nil {
		return f.claimResp, nil
	}
	return &activityv1.ClaimActivityTemplateTaskRewardResponse{}, nil
}

func (f *fakeActivityTemplateRuntimeClient) ListActivityTemplateLeaderboard(_ context.Context, req *activityv1.ListActivityTemplateLeaderboardRequest) (*activityv1.ListActivityTemplateLeaderboardResponse, error) {
	f.lastLeaderboard = req
	if f.leaderboardResp != nil {
		return f.leaderboardResp, nil
	}
	return &activityv1.ListActivityTemplateLeaderboardResponse{}, nil
}

type fakeActivityTemplateProfileClient struct {
	regionID  int64
	users     map[int64]*userv1.User
	batchErr  error
	lastGet   *userv1.GetUserRequest
	lastBatch *userv1.BatchGetUsersRequest
}

func (f *fakeActivityTemplateProfileClient) GetUser(_ context.Context, req *userv1.GetUserRequest) (*userv1.GetUserResponse, error) {
	f.lastGet = req
	return &userv1.GetUserResponse{User: &userv1.User{UserId: req.GetUserId(), RegionId: f.regionID}}, nil
}

func (f *fakeActivityTemplateProfileClient) BatchGetUsers(_ context.Context, req *userv1.BatchGetUsersRequest) (*userv1.BatchGetUsersResponse, error) {
	f.lastBatch = req
	if f.batchErr != nil {
		return nil, f.batchErr
	}
	return &userv1.BatchGetUsersResponse{Users: f.users}, nil
}
