package activitytemplate

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

	"hyapp-admin-server/internal/integration/activityclient"
	"hyapp-admin-server/internal/integration/statisticsclient"
	"hyapp-admin-server/internal/integration/userclient"
	"hyapp-admin-server/internal/middleware"
	activityv1 "hyapp.local/api/proto/activity/v1"

	"github.com/gin-gonic/gin"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	"google.golang.org/protobuf/proto"
)

type revisionConflictClient struct {
	activityclient.Client
}

type activityTemplateDataOwnerClient struct {
	activityclient.Client
	leaderboard     *activityv1.AdminListActivityTemplateLeaderboardResponse
	rewards         *activityv1.ListActivityTemplateRewardJobsResponse
	retryReward     *activityv1.RetryActivityTemplateRewardJobResponse
	claims          *activityv1.ListActivityTemplateTaskClaimsResponse
	retryClaim      *activityv1.RetryActivityTemplateTaskClaimResponse
	lastClaims      *activityv1.ListActivityTemplateTaskClaimsRequest
	lastRetry       *activityv1.RetryActivityTemplateTaskClaimRequest
	lastRewards     *activityv1.ListActivityTemplateRewardJobsRequest
	lastRewardRetry *activityv1.RetryActivityTemplateRewardJobRequest
}

type activityTemplateVersionsOwnerClient struct {
	activityclient.Client
	response *activityv1.ListActivityTemplateVersionsResponse
}

func (f activityTemplateVersionsOwnerClient) ListActivityTemplateVersions(context.Context, *activityv1.ListActivityTemplateVersionsRequest) (*activityv1.ListActivityTemplateVersionsResponse, error) {
	return f.response, nil
}

func (f activityTemplateDataOwnerClient) ListActivityTemplateLeaderboard(context.Context, *activityv1.AdminListActivityTemplateLeaderboardRequest) (*activityv1.AdminListActivityTemplateLeaderboardResponse, error) {
	return f.leaderboard, nil
}

func (f *activityTemplateDataOwnerClient) ListActivityTemplateRewardJobs(_ context.Context, req *activityv1.ListActivityTemplateRewardJobsRequest) (*activityv1.ListActivityTemplateRewardJobsResponse, error) {
	f.lastRewards = req
	return f.rewards, nil
}

func (f *activityTemplateDataOwnerClient) RetryActivityTemplateRewardJob(_ context.Context, req *activityv1.RetryActivityTemplateRewardJobRequest) (*activityv1.RetryActivityTemplateRewardJobResponse, error) {
	f.lastRewardRetry = req
	return f.retryReward, nil
}

func (f *activityTemplateDataOwnerClient) ListActivityTemplateTaskClaims(_ context.Context, req *activityv1.ListActivityTemplateTaskClaimsRequest) (*activityv1.ListActivityTemplateTaskClaimsResponse, error) {
	f.lastClaims = req
	return f.claims, nil
}

func (f *activityTemplateDataOwnerClient) RetryActivityTemplateTaskClaim(_ context.Context, req *activityv1.RetryActivityTemplateTaskClaimRequest) (*activityv1.RetryActivityTemplateTaskClaimResponse, error) {
	f.lastRetry = req
	return f.retryClaim, nil
}

type captureActivityTemplateUsers struct {
	calls   int
	userIDs []int64
	users   map[int64]*userclient.User
	err     error
}

func (f *captureActivityTemplateUsers) BatchGetUsers(_ context.Context, req userclient.BatchGetUsersRequest) (map[int64]*userclient.User, error) {
	f.calls++
	f.userIDs = append([]int64(nil), req.UserIDs...)
	return f.users, f.err
}

type captureActivityTemplateDataClient struct {
	query statisticsclient.ActivityTemplateDataQuery
}

func (c *captureActivityTemplateDataClient) ActivityTemplateData(_ context.Context, query statisticsclient.ActivityTemplateDataQuery) (statisticsclient.ActivityTemplateData, error) {
	c.query = query
	return statisticsclient.ActivityTemplateData{
		Overview: statisticsclient.ActivityTemplateOverview{TemplateID: query.TemplateID, PublishedVersion: query.PublishedVersion, VisitCount: 13},
		Trend:    []statisticsclient.ActivityTemplateTrendPoint{{StatDay: "2026-07-14", VisitCount: 7}}, Gifts: []statisticsclient.ActivityTemplateGiftStat{},
		Tasks: []statisticsclient.ActivityTemplateTaskStat{}, Granularity: "day", Timezone: "UTC",
		StartMS: query.StartMS, EndMS: query.EndMS, RegionID: query.RegionID,
	}, nil
}

func (revisionConflictClient) UpdateActivityTemplate(context.Context, *activityv1.UpdateActivityTemplateRequest) (*activityv1.UpdateActivityTemplateResponse, error) {
	return nil, status.Error(codes.FailedPrecondition, "activity template revision changed")
}

func TestUpdateMapsRevisionConflictToHTTP409(t *testing.T) {
	gin.SetMode(gin.TestMode)
	recorder := httptest.NewRecorder()
	ctx, _ := gin.CreateTestContext(recorder)
	ctx.Request = httptest.NewRequest(http.MethodPut, "/api/v1/admin/activities/templates/acttpl_1", strings.NewReader(`{
		"template_code":"gift_challenge_2026",
		"name":"Gift Challenge",
		"activity_type":"gift_challenge",
		"expected_revision":7
	}`))
	ctx.Request.Header.Set("Content-Type", "application/json")
	ctx.Params = gin.Params{{Key: "template_id", Value: "acttpl_1"}}

	New(revisionConflictClient{}, nil).Update(ctx)

	if recorder.Code != http.StatusConflict {
		t.Fatalf("status = %d, want %d; body=%s", recorder.Code, http.StatusConflict, recorder.Body.String())
	}
	var body struct {
		Code int `json:"code"`
	}
	if err := json.Unmarshal(recorder.Body.Bytes(), &body); err != nil {
		t.Fatalf("decode response: %v", err)
	}
	if body.Code != 40900 {
		t.Fatalf("response code = %d, want 40900", body.Code)
	}
}

func TestListRejectsMalformedTimeFilterBeforeCallingOwnerService(t *testing.T) {
	gin.SetMode(gin.TestMode)
	recorder := httptest.NewRecorder()
	ctx, _ := gin.CreateTestContext(recorder)
	ctx.Request = httptest.NewRequest(http.MethodGet, "/api/v1/admin/activities/templates?start_ms=not-a-time", nil)

	New(nil, nil).List(ctx)

	if recorder.Code != http.StatusBadRequest {
		t.Fatalf("status = %d, want %d; body=%s", recorder.Code, http.StatusBadRequest, recorder.Body.String())
	}
}

func TestVersionsExposeActualRuntimeWindow(t *testing.T) {
	gin.SetMode(gin.TestMode)
	recorder := httptest.NewRecorder()
	ctx, _ := gin.CreateTestContext(recorder)
	ctx.Request = httptest.NewRequest(http.MethodGet, "/api/v1/admin/activities/templates/acttpl_1/versions?page=1&page_size=20", nil)
	ctx.Params = gin.Params{{Key: "template_id", Value: "acttpl_1"}}
	client := activityTemplateVersionsOwnerClient{response: &activityv1.ListActivityTemplateVersionsResponse{
		Total: 1,
		Versions: []*activityv1.ActivityTemplateVersion{{
			TemplateId: "acttpl_1", VersionNo: 1, Snapshot: &activityv1.ActivityTemplate{TemplateId: "acttpl_1", LifecycleStatus: "disabled"},
			PublishedByAdminId: 90001, PublishedAtMs: 100, RuntimeFromMs: 100, RuntimeToMs: 500,
		}},
	}}

	New(client, nil).Versions(ctx)

	if recorder.Code != http.StatusOK {
		t.Fatalf("status = %d; body=%s", recorder.Code, recorder.Body.String())
	}
	var body struct {
		Data struct {
			Items []struct {
				RuntimeFromMS int64 `json:"runtime_from_ms"`
				RuntimeToMS   int64 `json:"runtime_to_ms"`
				Snapshot      struct {
					LifecycleStatus string `json:"lifecycle_status"`
				} `json:"snapshot"`
			} `json:"items"`
		} `json:"data"`
	}
	if err := json.Unmarshal(recorder.Body.Bytes(), &body); err != nil {
		t.Fatalf("decode response: %v", err)
	}
	if len(body.Data.Items) != 1 || body.Data.Items[0].RuntimeFromMS != 100 || body.Data.Items[0].RuntimeToMS != 500 || body.Data.Items[0].Snapshot.LifecycleStatus != "disabled" {
		t.Fatalf("version runtime response = %+v", body.Data.Items)
	}
}

func TestDataNormalizesRequestedWindowToUTCWholeDays(t *testing.T) {
	gin.SetMode(gin.TestMode)
	stats := &captureActivityTemplateDataClient{}
	recorder := httptest.NewRecorder()
	ctx, _ := gin.CreateTestContext(recorder)
	start := time.Date(2026, 7, 14, 12, 30, 0, 0, time.UTC).UnixMilli()
	end := time.Date(2026, 7, 15, 8, 45, 0, 0, time.UTC).UnixMilli()
	ctx.Request = httptest.NewRequest(http.MethodGet, "/api/v1/admin/activities/templates/acttpl_1/data?version_no=3&region_id=7&start_ms="+strconv.FormatInt(start, 10)+"&end_ms="+strconv.FormatInt(end, 10), nil)
	ctx.Params = gin.Params{{Key: "template_id", Value: "acttpl_1"}}

	New(nil, nil, stats).Data(ctx)

	if recorder.Code != http.StatusOK {
		t.Fatalf("status = %d; body=%s", recorder.Code, recorder.Body.String())
	}
	wantStart := time.Date(2026, 7, 14, 0, 0, 0, 0, time.UTC).UnixMilli()
	wantEnd := time.Date(2026, 7, 16, 0, 0, 0, 0, time.UTC).UnixMilli()
	if stats.query.TemplateID != "acttpl_1" || stats.query.PublishedVersion != 3 || stats.query.RegionID != 7 ||
		stats.query.StartMS != wantStart || stats.query.EndMS != wantEnd {
		t.Fatalf("normalized statistics query = %+v", stats.query)
	}
	var body struct {
		Data statisticsclient.ActivityTemplateData `json:"data"`
	}
	if err := json.Unmarshal(recorder.Body.Bytes(), &body); err != nil {
		t.Fatalf("decode response: %v", err)
	}
	if body.Data.StartMS != wantStart || body.Data.EndMS != wantEnd || body.Data.Granularity != "day" || body.Data.Timezone != "UTC" ||
		body.Data.Overview.VisitCount != 13 || len(body.Data.Trend) != 1 || body.Data.Trend[0].VisitCount != 7 {
		t.Fatalf("effective range not echoed: %+v", body.Data)
	}
}

func TestLeaderboardAndRewardsBatchDecorateUsersWithoutOwningProfileFacts(t *testing.T) {
	gin.SetMode(gin.TestMode)
	owner := activityTemplateDataOwnerClient{
		leaderboard: &activityv1.AdminListActivityTemplateLeaderboardResponse{
			Period: &activityv1.ActivityTemplateRankPeriod{TemplateId: "acttpl_1", BoardType: "daily", PeriodKey: "2026-07-14"},
			Entries: []*activityv1.ActivityTemplateLeaderboardEntry{
				{RankNo: 1, UserId: 501, Score: 900}, {RankNo: 2, UserId: 501, Score: 800}, {RankNo: 3, UserId: 502, Score: 700},
			}, Total: 3,
		},
		rewards: &activityv1.ListActivityTemplateRewardJobsResponse{
			Jobs: []*activityv1.ActivityTemplateRewardJob{{RewardJobId: "job-1", UserId: 502, Status: "granted"}}, Total: 1,
		},
	}
	users := &captureActivityTemplateUsers{users: map[int64]*userclient.User{
		501: {UserID: 501, DisplayUserID: "L501", Username: "first", Avatar: "first.png"},
		502: {UserID: 502, DisplayUserID: "L502", Username: "second", Avatar: "second.png"},
	}}
	handler := New(&owner, nil).BindUserBatchGetter(users)

	leaderboardRecorder := httptest.NewRecorder()
	leaderboardContext, _ := gin.CreateTestContext(leaderboardRecorder)
	leaderboardContext.Request = httptest.NewRequest(http.MethodGet, "/api/v1/admin/activities/templates/acttpl_1/leaderboard?version_no=1&board_type=daily&period_key=2026-07-14", nil)
	leaderboardContext.Params = gin.Params{{Key: "template_id", Value: "acttpl_1"}}
	handler.Leaderboard(leaderboardContext)
	if leaderboardRecorder.Code != http.StatusOK || users.calls != 1 || len(users.userIDs) != 2 || users.userIDs[0] != 501 || users.userIDs[1] != 502 {
		t.Fatalf("leaderboard batch enrichment: status=%d calls=%d ids=%v body=%s", leaderboardRecorder.Code, users.calls, users.userIDs, leaderboardRecorder.Body.String())
	}
	var leaderboardBody struct {
		Data struct {
			Items []struct {
				User *activityTemplateUserDTO `json:"user"`
			} `json:"items"`
		} `json:"data"`
	}
	if err := json.Unmarshal(leaderboardRecorder.Body.Bytes(), &leaderboardBody); err != nil || len(leaderboardBody.Data.Items) != 3 || leaderboardBody.Data.Items[0].User == nil || leaderboardBody.Data.Items[0].User.DisplayUserID != "L501" {
		t.Fatalf("leaderboard user decoration mismatch: err=%v body=%s", err, leaderboardRecorder.Body.String())
	}

	users.calls, users.userIDs = 0, nil
	rewardsRecorder := httptest.NewRecorder()
	rewardsContext, _ := gin.CreateTestContext(rewardsRecorder)
	rewardsContext.Request = httptest.NewRequest(http.MethodGet, "/api/v1/admin/activities/templates/acttpl_1/rewards?version_no=1", nil)
	rewardsContext.Params = gin.Params{{Key: "template_id", Value: "acttpl_1"}}
	handler.RewardJobs(rewardsContext)
	if rewardsRecorder.Code != http.StatusOK || users.calls != 1 || len(users.userIDs) != 1 || users.userIDs[0] != 502 || !strings.Contains(rewardsRecorder.Body.String(), `"display_user_id":"L502"`) {
		t.Fatalf("reward user decoration: status=%d calls=%d ids=%v body=%s", rewardsRecorder.Code, users.calls, users.userIDs, rewardsRecorder.Body.String())
	}
}

func TestRewardJobsRequiresPublishedVersion(t *testing.T) {
	gin.SetMode(gin.TestMode)
	recorder := httptest.NewRecorder()
	ctx, _ := gin.CreateTestContext(recorder)
	ctx.Request = httptest.NewRequest(http.MethodGet, "/api/v1/admin/activities/templates/acttpl_1/rewards", nil)
	ctx.Params = gin.Params{{Key: "template_id", Value: "acttpl_1"}}

	New(&activityTemplateDataOwnerClient{}, nil).RewardJobs(ctx)

	if recorder.Code != http.StatusBadRequest {
		t.Fatalf("status = %d, want %d; body=%s", recorder.Code, http.StatusBadRequest, recorder.Body.String())
	}
}

func TestRewardJobsExposeDeadBackoffAndAllowExplicitRevival(t *testing.T) {
	gin.SetMode(gin.TestMode)
	dead := &activityv1.ActivityTemplateRewardJob{
		RewardJobId: "job-dead", TemplateId: "acttpl_1", VersionNo: 2, UserId: 502,
		Status: "dead", FailureReason: "maximum delivery attempts exhausted", AttemptCount: 8,
		MaxAttempts: 8, NextRetryAtMs: 0,
	}
	running := proto.Clone(dead).(*activityv1.ActivityTemplateRewardJob)
	running.Status, running.AttemptCount, running.MaxAttempts = "running", 9, 9
	owner := &activityTemplateDataOwnerClient{
		rewards:     &activityv1.ListActivityTemplateRewardJobsResponse{Jobs: []*activityv1.ActivityTemplateRewardJob{dead}, Total: 1},
		retryReward: &activityv1.RetryActivityTemplateRewardJobResponse{Job: running},
	}
	handler := New(owner, nil)

	listRecorder := httptest.NewRecorder()
	listContext, _ := gin.CreateTestContext(listRecorder)
	listContext.Request = httptest.NewRequest(http.MethodGet, "/api/v1/admin/activities/templates/acttpl_1/rewards?version_no=2&status=dead", nil)
	listContext.Params = gin.Params{{Key: "template_id", Value: "acttpl_1"}}
	handler.RewardJobs(listContext)
	if listRecorder.Code != http.StatusOK || owner.lastRewards == nil || owner.lastRewards.GetStatus() != "dead" ||
		!strings.Contains(listRecorder.Body.String(), `"max_attempts":8`) || !strings.Contains(listRecorder.Body.String(), `"next_retry_at_ms":0`) {
		t.Fatalf("dead reward list mismatch: status=%d request=%+v body=%s", listRecorder.Code, owner.lastRewards, listRecorder.Body.String())
	}

	retryRecorder := httptest.NewRecorder()
	retryContext, _ := gin.CreateTestContext(retryRecorder)
	retryContext.Request = httptest.NewRequest(http.MethodPost, "/api/v1/admin/activities/templates/acttpl_1/rewards/job-dead/retry", nil)
	retryContext.Params = gin.Params{{Key: "template_id", Value: "acttpl_1"}, {Key: "reward_job_id", Value: "job-dead"}}
	retryContext.Set(middleware.ContextUserID, uint(90001))
	handler.RetryRewardJob(retryContext)
	if retryRecorder.Code != http.StatusOK || owner.lastRewardRetry == nil || owner.lastRewardRetry.GetRewardJobId() != "job-dead" ||
		owner.lastRewardRetry.GetOperatorAdminId() != 90001 || !strings.Contains(retryRecorder.Body.String(), `"max_attempts":9`) {
		t.Fatalf("dead reward revival mismatch: status=%d request=%+v body=%s", retryRecorder.Code, owner.lastRewardRetry, retryRecorder.Body.String())
	}
}

func TestTaskClaimsFilterEnrichAndRetryPreserveImmutableAttribution(t *testing.T) {
	gin.SetMode(gin.TestMode)
	rewardItem := &activityv1.ActivityTemplateRewardItem{ItemType: "resource", ResourceId: 91, ResourceType: "avatar_frame", Name: "Frame", Quantity: 1}
	claim := &activityv1.ActivityTemplateTaskClaim{
		ClaimId: "claim-1", CommandId: "device-command", TemplateId: "acttpl_1", TemplateCode: "summer",
		VersionNo: 3, TaskDay: "2026-07-14", TaskKey: "send_gift", UserId: 503,
		RewardResourceGroupId: 81, WalletCommandId: "wallet-command", WalletGrantId: "wallet-grant",
		Status: "failed", FailureReason: "timeout", AttemptCount: 2,
		RewardSnapshotId: "rgs-task", RewardSnapshotHash: strings.Repeat("a", 64), RewardName: "Task reward",
		RewardItems: []*activityv1.ActivityTemplateRewardItem{rewardItem}, EarningRegionId: 210, AttributionAtMs: 1784073000000,
	}
	owner := &activityTemplateDataOwnerClient{
		claims:     &activityv1.ListActivityTemplateTaskClaimsResponse{Claims: []*activityv1.ActivityTemplateTaskClaim{claim}, Total: 1},
		retryClaim: &activityv1.RetryActivityTemplateTaskClaimResponse{Claim: claim},
	}
	users := &captureActivityTemplateUsers{users: map[int64]*userclient.User{
		503: {UserID: 503, DisplayUserID: "L503", Username: "winner", Avatar: "winner.png"},
	}}
	handler := New(owner, nil).BindUserBatchGetter(users)

	listRecorder := httptest.NewRecorder()
	listContext, _ := gin.CreateTestContext(listRecorder)
	listContext.Request = httptest.NewRequest(http.MethodGet,
		"/api/v1/admin/activities/templates/acttpl_1/task-claims?version_no=3&task_day=2026-07-14&task_key=send_gift&user_id=503&status=failed&page=2&page_size=10", nil)
	listContext.Params = gin.Params{{Key: "template_id", Value: "acttpl_1"}}
	handler.TaskClaims(listContext)

	if listRecorder.Code != http.StatusOK || owner.lastClaims == nil || owner.lastClaims.GetVersionNo() != 3 ||
		owner.lastClaims.GetTaskDay() != "2026-07-14" || owner.lastClaims.GetTaskKey() != "send_gift" ||
		owner.lastClaims.GetUserId() != 503 || owner.lastClaims.GetStatus() != "failed" || owner.lastClaims.GetPage() != 2 || owner.lastClaims.GetPageSize() != 10 {
		t.Fatalf("task claim filters mismatch: status=%d request=%+v body=%s", listRecorder.Code, owner.lastClaims, listRecorder.Body.String())
	}
	var listBody struct {
		Data struct {
			Items []taskClaimDTO `json:"items"`
		} `json:"data"`
	}
	if err := json.Unmarshal(listRecorder.Body.Bytes(), &listBody); err != nil || len(listBody.Data.Items) != 1 {
		t.Fatalf("decode task claims: err=%v body=%s", err, listRecorder.Body.String())
	}
	item := listBody.Data.Items[0]
	if item.RewardSnapshotHash != strings.Repeat("a", 64) || item.RewardName != "Task reward" || len(item.RewardItems) != 1 ||
		item.EarningRegionID != 210 || item.AttributionAtMS != 1784073000000 || item.User == nil || item.User.DisplayUserID != "L503" {
		t.Fatalf("task claim immutable/admin view mismatch: %+v", item)
	}

	retryRecorder := httptest.NewRecorder()
	retryContext, _ := gin.CreateTestContext(retryRecorder)
	retryContext.Request = httptest.NewRequest(http.MethodPost, "/api/v1/admin/activities/templates/acttpl_1/task-claims/claim-1/retry", nil)
	retryContext.Params = gin.Params{{Key: "template_id", Value: "acttpl_1"}, {Key: "claim_id", Value: "claim-1"}}
	retryContext.Set(middleware.ContextUserID, uint(90001))
	handler.RetryTaskClaim(retryContext)
	if retryRecorder.Code != http.StatusOK || owner.lastRetry == nil || owner.lastRetry.GetTemplateId() != "acttpl_1" ||
		owner.lastRetry.GetClaimId() != "claim-1" || owner.lastRetry.GetOperatorAdminId() != 90001 {
		t.Fatalf("task claim retry mismatch: status=%d request=%+v body=%s", retryRecorder.Code, owner.lastRetry, retryRecorder.Body.String())
	}
}

func TestAdminRewardDTOsExposePublishedSnapshots(t *testing.T) {
	item := &activityv1.ActivityTemplateRewardItem{ItemType: "wallet_asset", WalletAssetType: "coin", WalletAssetAmount: 500, Name: "Coins"}
	template := templateFromProto(&activityv1.ActivityTemplate{
		Tasks:       []*activityv1.ActivityTemplateTask{{TaskKey: "visit", RewardSnapshotId: "task-snapshot", RewardSnapshotHash: strings.Repeat("b", 64), RewardName: "Visit reward", RewardItems: []*activityv1.ActivityTemplateRewardItem{item}}},
		RankRewards: []*activityv1.ActivityTemplateRankReward{{BoardType: "total", RankFrom: 1, RankTo: 1, RewardSnapshotId: "rank-snapshot", RewardSnapshotHash: strings.Repeat("c", 64), RewardName: "Champion", RewardItems: []*activityv1.ActivityTemplateRewardItem{item}}},
	})
	job := rewardJobFromProto(&activityv1.ActivityTemplateRewardJob{
		RewardJobId: "job-1", RewardSnapshotId: "rank-snapshot", RewardSnapshotHash: strings.Repeat("c", 64), RewardName: "Champion", RewardItems: []*activityv1.ActivityTemplateRewardItem{item},
	}, nil)
	if len(template.Tasks) != 1 || template.Tasks[0].RewardSnapshotHash != strings.Repeat("b", 64) || len(template.Tasks[0].RewardItems) != 1 ||
		len(template.RankRewards) != 1 || template.RankRewards[0].RewardSnapshotHash != strings.Repeat("c", 64) || len(template.RankRewards[0].RewardItems) != 1 ||
		job.RewardSnapshotID != "rank-snapshot" || job.RewardSnapshotHash != strings.Repeat("c", 64) || len(job.RewardItems) != 1 {
		t.Fatalf("published reward snapshots were dropped: template=%+v job=%+v", template, job)
	}
}

func TestLeaderboardProfileFailureDoesNotHideRankFacts(t *testing.T) {
	gin.SetMode(gin.TestMode)
	owner := activityTemplateDataOwnerClient{leaderboard: &activityv1.AdminListActivityTemplateLeaderboardResponse{
		Period:  &activityv1.ActivityTemplateRankPeriod{TemplateId: "acttpl_1", BoardType: "total", PeriodKey: "total"},
		Entries: []*activityv1.ActivityTemplateLeaderboardEntry{{RankNo: 1, UserId: 501, Score: 900}}, Total: 1,
	}}
	users := &captureActivityTemplateUsers{err: errors.New("user service unavailable")}
	handler := New(&owner, nil).BindUserBatchGetter(users)
	recorder := httptest.NewRecorder()
	ctx, _ := gin.CreateTestContext(recorder)
	ctx.Request = httptest.NewRequest(http.MethodGet, "/api/v1/admin/activities/templates/acttpl_1/leaderboard?version_no=1&board_type=total", nil)
	ctx.Params = gin.Params{{Key: "template_id", Value: "acttpl_1"}}

	handler.Leaderboard(ctx)

	if recorder.Code != http.StatusOK || users.calls != 1 || !strings.Contains(recorder.Body.String(), `"score":900`) || strings.Contains(recorder.Body.String(), `"user":`) {
		t.Fatalf("profile degradation hid or changed rank facts: status=%d body=%s", recorder.Code, recorder.Body.String())
	}
}
