package activitytemplate

import (
	"context"
	"errors"
	"strings"
	"testing"
	"time"

	"google.golang.org/grpc"
	"google.golang.org/protobuf/proto"
	roomeventsv1 "hyapp.local/api/proto/events/room/v1"
	walletv1 "hyapp.local/api/proto/wallet/v1"
	"hyapp/pkg/appcode"
	"hyapp/pkg/xerr"
	domain "hyapp/services/activity-service/internal/domain/activitytemplate"
	messageservice "hyapp/services/activity-service/internal/service/message"
)

type leaderboardRuntimeRepository struct {
	RuntimeRepository
	currentRuntime  domain.Runtime
	currentFound    bool
	periodRuntime   domain.Runtime
	periodFound     bool
	currentCalls    int
	periodCalls     int
	periodVersionNo int64
	leaderboardCall int
	giftEvents      []domain.GiftEvent
}

func (f *leaderboardRuntimeRepository) FindCurrentActivityTemplateRuntime(context.Context, string, int64, int64) (domain.Runtime, bool, error) {
	f.currentCalls++
	return f.currentRuntime, f.currentFound, nil
}

func (f *leaderboardRuntimeRepository) ConsumeActivityTemplateGiftEvent(_ context.Context, event domain.GiftEvent, _ int64) (domain.EventResult, error) {
	f.giftEvents = append(f.giftEvents, event)
	return domain.EventResult{EventID: event.EventID, Status: domain.EventStatusConsumed}, nil
}

func (f *leaderboardRuntimeRepository) FindActivityTemplateRuntimeForPeriod(_ context.Context, _ string, versionNo int64, _ int64, _ string, _ string, _ int64) (domain.Runtime, bool, error) {
	f.periodCalls++
	f.periodVersionNo = versionNo
	return f.periodRuntime, f.periodFound, nil
}

func (f *leaderboardRuntimeRepository) FindActivityTemplateRuntimeForExactLeaderboard(_ context.Context, _ string, versionNo int64, _ string, _ string, _ int64) (domain.Runtime, bool, error) {
	f.periodCalls++
	f.periodVersionNo = versionNo
	return f.periodRuntime, f.periodFound, nil
}

func (f *leaderboardRuntimeRepository) ListActivityTemplateRuntimeLeaderboard(_ context.Context, runtime domain.Runtime, query domain.LeaderboardQuery) (domain.LeaderboardResult, error) {
	f.leaderboardCall++
	return domain.LeaderboardResult{
		Runtime: runtime,
		Period: domain.RankPeriod{
			TemplateID: runtime.Template.TemplateID, TemplateCode: runtime.Template.TemplateCode,
			VersionNo: runtime.VersionNo, BoardType: query.BoardType, PeriodKey: query.PeriodKey,
		},
		Entries: []domain.LeaderboardEntry{},
	}, nil
}

func TestExactLeaderboardAllowsNonParticipantCrossRegionViewerAfterActivityEnded(t *testing.T) {
	now := time.Date(2026, 7, 20, 0, 0, 0, 0, time.UTC)
	ended := domain.Runtime{
		Template:  domain.Template{TemplateID: "acttpl_1", TemplateCode: "summer_gifts", EndMS: now.Add(-time.Hour).UnixMilli()},
		VersionNo: 4, RuntimeFromMS: now.Add(-7 * 24 * time.Hour).UnixMilli(), RuntimeToMS: now.Add(-time.Hour).UnixMilli(),
	}
	repository := &leaderboardRuntimeRepository{periodRuntime: ended, periodFound: true}
	svc := New(&fakeRepository{}, nil, nil)
	svc.BindRuntime(repository, nil)
	svc.now = func() time.Time { return now }

	got, err := svc.ListRuntimeLeaderboard(context.Background(), domain.LeaderboardQuery{
		// RegionID is deliberately zero: the gateway does not resolve the viewer's mutable current region
		// for exact historical links, and repository resolution does not require participation.
		TemplateCode: "summer_gifts", VersionNo: 4, UserID: 70001, RegionID: 0, BoardType: domain.BoardTypeTotal,
		PeriodKey: "total", Page: 1, PageSize: 20,
	})
	if err != nil {
		t.Fatalf("ListRuntimeLeaderboard() error = %v", err)
	}
	if got.Runtime.VersionNo != 4 || repository.periodVersionNo != 4 || repository.periodCalls != 1 || repository.currentCalls != 0 || repository.leaderboardCall != 1 {
		t.Fatalf("historical leaderboard resolution = %+v; repository=%+v", got.Runtime, repository)
	}
}

func TestListRuntimeLeaderboardWithoutPeriodRemainsCurrentOnly(t *testing.T) {
	repository := &leaderboardRuntimeRepository{}
	svc := New(&fakeRepository{}, nil, nil)
	svc.BindRuntime(repository, nil)

	_, err := svc.ListRuntimeLeaderboard(context.Background(), domain.LeaderboardQuery{
		TemplateCode: "summer_gifts", UserID: 70001, RegionID: 8, BoardType: domain.BoardTypeTotal,
	})
	if !xerr.IsCode(err, xerr.NotFound) {
		t.Fatalf("ListRuntimeLeaderboard() error = %v, want not found", err)
	}
	if repository.currentCalls != 1 || repository.periodCalls != 0 {
		t.Fatalf("unexpected lookup calls: current=%d period=%d", repository.currentCalls, repository.periodCalls)
	}
}

func TestHandleRoomEventExcludesEveryRobotGiftClassAndUsesCoinSpent(t *testing.T) {
	repository := &leaderboardRuntimeRepository{}
	svc := New(&fakeRepository{}, nil, nil)
	svc.BindRuntime(repository, nil)
	now := time.Date(2026, 7, 14, 9, 0, 0, 0, time.UTC)
	svc.now = func() time.Time { return now }

	tests := []struct {
		name string
		edit func(*roomeventsv1.RoomGiftSent)
		want int32
	}{
		{name: "is robot gift", edit: func(item *roomeventsv1.RoomGiftSent) { item.IsRobotGift = true }},
		{name: "synthetic lucky gift", edit: func(item *roomeventsv1.RoomGiftSent) { item.SyntheticLuckyGift = true }},
		{name: "real room robot gift", edit: func(item *roomeventsv1.RoomGiftSent) { item.RealRoomRobotGift = true }},
		{name: "real user gift", edit: func(*roomeventsv1.RoomGiftSent) {}, want: 1},
	}
	for index, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			before := len(repository.giftEvents)
			gift := &roomeventsv1.RoomGiftSent{
				SenderUserId: 90001, GiftId: "rose", GiftCount: 3, GiftValue: 999_999,
				CoinSpent: 750, RegionId: 12, VisibleRegionId: 99,
			}
			tt.edit(gift)
			body, err := proto.Marshal(gift)
			if err != nil {
				t.Fatalf("marshal gift: %v", err)
			}
			consumed, err := svc.HandleRoomEvent(context.Background(), &roomeventsv1.EventEnvelope{
				EventId: "evt-" + tt.name, EventType: "RoomGiftSent", AppCode: "hyapp",
				OccurredAtMs: now.Add(time.Duration(index) * time.Millisecond).UnixMilli(), Body: body,
			})
			if err != nil || consumed != tt.want {
				t.Fatalf("HandleRoomEvent() = (%d, %v), want (%d, nil)", consumed, err, tt.want)
			}
			if tt.want == 0 && len(repository.giftEvents) != before {
				t.Fatalf("robot gift reached score repository: %+v", repository.giftEvents[before:])
			}
		})
	}
	if len(repository.giftEvents) != 1 || repository.giftEvents[0].CoinAmount != 750 || repository.giftEvents[0].GiftCount != 3 || repository.giftEvents[0].RegionID != 12 {
		t.Fatalf("real gift score fact = %+v; coin_spent must be source of truth", repository.giftEvents)
	}
}

type retryRewardRepository struct {
	RuntimeRepository
	job            domain.RewardJob
	taskRuntime    domain.Runtime
	taskClaim      domain.TaskClaim
	claimCalls     int
	taskClaimCalls int
	taskVersionNo  int64
	markedGranted  []string
	markedFailed   []string
	taskGranted    []string
	taskFailed     []string
	walletGrantIDs []string
	leaseOwners    []string
}

func (f *retryRewardRepository) FindCurrentActivityTemplateRuntime(context.Context, string, int64, int64) (domain.Runtime, bool, error) {
	return domain.Runtime{}, false, nil
}

func (f *retryRewardRepository) FindActivityTemplateRuntimeForUserTask(_ context.Context, _ string, versionNo int64, _ int64, _ string, _ string) (domain.Runtime, bool, error) {
	f.taskVersionNo = versionNo
	return f.taskRuntime, f.taskRuntime.VersionNo > 0, nil
}

func (f *retryRewardRepository) PrepareActivityTemplateTaskClaim(context.Context, domain.Runtime, domain.ClaimCommand, int64) (domain.TaskClaim, error) {
	f.taskClaimCalls++
	claim := f.taskClaim
	claim.Status = domain.ClaimStatusRunning
	claim.AttemptCount = int32(f.taskClaimCalls)
	return claim, nil
}

func (f *retryRewardRepository) MarkActivityTemplateTaskClaimGranted(_ context.Context, claimID, walletGrantID string, nowMS int64) (domain.TaskClaim, error) {
	f.taskGranted = append(f.taskGranted, claimID)
	claim := f.taskClaim
	claim.Status = domain.ClaimStatusGranted
	claim.WalletGrantID = walletGrantID
	claim.GrantedAtMS = nowMS
	return claim, nil
}

func (f *retryRewardRepository) MarkActivityTemplateTaskClaimFailed(_ context.Context, claimID, _ string, _ int64) error {
	f.taskFailed = append(f.taskFailed, claimID)
	return nil
}

func (f *retryRewardRepository) ClaimActivityTemplateRewardJobForManualRetry(_ context.Context, _, _ string, workerID string, _ int64, _ time.Duration) (domain.RewardJob, error) {
	f.claimCalls++
	f.leaseOwners = append(f.leaseOwners, workerID)
	job := f.job
	job.Status = domain.ClaimStatusRunning
	job.AttemptCount = int32(f.claimCalls)
	return job, nil
}

func (f *retryRewardRepository) ExtendActivityTemplateRewardJobLease(_ context.Context, _, workerID string, _ int64, _ time.Duration) error {
	f.leaseOwners = append(f.leaseOwners, workerID)
	return nil
}

func (f *retryRewardRepository) MarkActivityTemplateRewardJobGrantedWithLease(_ context.Context, jobID, workerID, walletGrantID string, _ int64) error {
	f.markedGranted = append(f.markedGranted, jobID)
	f.leaseOwners = append(f.leaseOwners, workerID)
	f.walletGrantIDs = append(f.walletGrantIDs, walletGrantID)
	return nil
}

func (f *retryRewardRepository) MarkActivityTemplateRewardJobFailedWithLease(_ context.Context, jobID, workerID, _ string, _ int64) error {
	f.markedFailed = append(f.markedFailed, jobID)
	f.leaseOwners = append(f.leaseOwners, workerID)
	return nil
}

type sequenceRuntimeWallet struct {
	requests []*walletv1.GrantPinnedResourceGroupRequest
	errors   []error
}

func (f *sequenceRuntimeWallet) GrantPinnedResourceGroup(_ context.Context, req *walletv1.GrantPinnedResourceGroupRequest, _ ...grpc.CallOption) (*walletv1.ResourceGrantResponse, error) {
	f.requests = append(f.requests, req)
	index := len(f.requests) - 1
	if index < len(f.errors) && f.errors[index] != nil {
		return nil, f.errors[index]
	}
	return &walletv1.ResourceGrantResponse{Grant: &walletv1.ResourceGrant{GrantId: "wallet-grant-1"}}, nil
}

type captureRuntimeNotice struct {
	commands []messageservice.NoticeCommand
}

type settlementLeaseRepository struct {
	RuntimeRepository
	periods     []domain.RankPeriod
	finalizable []domain.RankPeriod
	jobs        []domain.RewardJob
	periodOwner string
	jobOwner    string
	leaseOwners []string
	prepared    []string
	finished    []string
	granted     []string
	failed      []string
}

func (f *settlementLeaseRepository) ClaimDueActivityTemplateRankPeriods(_ context.Context, workerID string, _ int64, _ time.Duration, _ int32) ([]domain.RankPeriod, error) {
	f.periodOwner = workerID
	return append([]domain.RankPeriod(nil), f.periods...), nil
}

func (f *settlementLeaseRepository) PrepareActivityTemplateRankSettlement(_ context.Context, period domain.RankPeriod, _ int64) ([]domain.RewardJob, error) {
	f.prepared = append(f.prepared, period.BoardType+":"+period.PeriodKey)
	return []domain.RewardJob{}, nil
}

func (f *settlementLeaseRepository) ClaimFinalizableActivityTemplateRankPeriods(_ context.Context, _ string, _ int64, _ time.Duration, _ int32) ([]domain.RankPeriod, error) {
	return append([]domain.RankPeriod(nil), f.finalizable...), nil
}

func (f *settlementLeaseRepository) FinishActivityTemplateRankPeriodIfComplete(_ context.Context, period domain.RankPeriod, _ int64) (bool, error) {
	f.finished = append(f.finished, period.BoardType+":"+period.PeriodKey)
	return true, nil
}

func (f *settlementLeaseRepository) ClaimActivityTemplateRewardJobs(_ context.Context, workerID string, _ int64, _ time.Duration, _ int32) ([]domain.RewardJob, error) {
	f.jobOwner = workerID
	return append([]domain.RewardJob(nil), f.jobs...), nil
}

func (f *settlementLeaseRepository) ExtendActivityTemplateRewardJobLease(_ context.Context, _ string, workerID string, _ int64, _ time.Duration) error {
	f.leaseOwners = append(f.leaseOwners, workerID)
	return nil
}

func (f *settlementLeaseRepository) MarkActivityTemplateRewardJobGrantedWithLease(_ context.Context, jobID, workerID, _ string, _ int64) error {
	f.leaseOwners = append(f.leaseOwners, workerID)
	f.granted = append(f.granted, jobID)
	return nil
}

func (f *settlementLeaseRepository) MarkActivityTemplateRewardJobFailedWithLease(_ context.Context, jobID, workerID, _ string, _ int64) error {
	f.leaseOwners = append(f.leaseOwners, workerID)
	f.failed = append(f.failed, jobID)
	return nil
}

func TestSettlementSeparatesPeriodMaterializationFromBoundedLeasedJobDelivery(t *testing.T) {
	periodDaily := domain.RankPeriod{AppCode: "hyapp", TemplateID: "tpl-1", TemplateCode: "summer", VersionNo: 3, BoardType: domain.BoardTypeDaily, PeriodKey: "2026-07-14"}
	periodTotal := domain.RankPeriod{AppCode: "hyapp", TemplateID: "tpl-1", TemplateCode: "summer", VersionNo: 3, BoardType: domain.BoardTypeTotal, PeriodKey: "total"}
	job := domain.RewardJob{
		AppCode: "hyapp", RewardJobID: "job-1", TemplateID: "tpl-1", TemplateCode: "summer", VersionNo: 3,
		BoardType: domain.BoardTypeDaily, PeriodKey: "2026-07-14", UserID: 1001, ResourceGroupID: 81,
		WalletCommandID: "wallet-stable", RewardSnapshot: domain.RewardSnapshot{SnapshotID: "rgs-1", SnapshotHash: strings.Repeat("d", 64)},
	}
	repository := &settlementLeaseRepository{periods: []domain.RankPeriod{periodDaily, periodTotal}, jobs: []domain.RewardJob{job}}
	wallet := &sequenceRuntimeWallet{}
	svc := New(&fakeRepository{}, nil, nil)
	svc.BindRuntime(repository, wallet)
	ctx := appcode.WithContext(context.Background(), "hyapp")

	claimed, processed, success, failure, hasMore, err := svc.ProcessRuntimeSettlementBatch(ctx, "run-77", "node-a", 2, 30*time.Second)
	if err != nil || claimed != 1 || processed != 1 || success != 1 || failure != 0 || !hasMore {
		t.Fatalf("settlement counters = (%d,%d,%d,%d,%v,%v)", claimed, processed, success, failure, hasMore, err)
	}
	if repository.periodOwner != "node-a:run-77" || repository.jobOwner != repository.periodOwner || len(repository.prepared) != 2 ||
		len(repository.granted) != 1 || len(repository.failed) != 0 || len(wallet.requests) != 1 {
		t.Fatalf("bounded settlement state mismatch: repository=%+v wallet=%+v", repository, wallet.requests)
	}
	for _, owner := range repository.leaseOwners {
		if owner != repository.jobOwner {
			t.Fatalf("job completion escaped lease owner: owners=%v job_owner=%q", repository.leaseOwners, repository.jobOwner)
		}
	}
	if len(repository.finished) != 3 { // two post-materialization checks plus one post-job finalization
		t.Fatalf("period finalization checks = %v", repository.finished)
	}
	if activityTemplateRewardLeaseOwner("node-a", "old-run") == activityTemplateRewardLeaseOwner("node-a", "new-run") {
		t.Fatal("settlement invocations on one node must have distinct fencing owners")
	}
}

func TestSettlementHasMoreIgnoresFiftyBlockedPeriodsAndDoesNotStarvePending(t *testing.T) {
	// Repository discovery represents the production SQL result after fifty older settling periods with
	// dead/backoff jobs were filtered out: only the later pending period is actionable.
	pending := domain.RankPeriod{
		AppCode: "hyapp", TemplateID: "pending-after-50-blocked", TemplateCode: "summer", VersionNo: 3,
		BoardType: domain.BoardTypeTotal, PeriodKey: "total",
	}
	repository := &settlementLeaseRepository{periods: []domain.RankPeriod{pending}}
	svc := New(&fakeRepository{}, nil, nil)
	svc.BindRuntime(repository, &sequenceRuntimeWallet{})
	ctx := appcode.WithContext(context.Background(), "hyapp")

	claimed, processed, success, failure, hasMore, err := svc.ProcessRuntimeSettlementBatch(
		ctx, "run-no-hot-loop", "node-a", 50, time.Minute,
	)
	if err != nil || claimed != 0 || processed != 0 || success != 0 || failure != 0 {
		t.Fatalf("settlement counters = (%d,%d,%d,%d,%v): %v", claimed, processed, success, failure, hasMore, err)
	}
	if hasMore {
		t.Fatal("blocked settling periods must not keep has_more true after the sole pending period is processed")
	}
	if len(repository.prepared) != 1 || repository.prepared[0] != "total:total" {
		t.Fatalf("later pending period was starved: prepared=%v", repository.prepared)
	}
}

func (f *captureRuntimeNotice) CreateActivityNotice(_ context.Context, cmd messageservice.NoticeCommand) (messageservice.NoticeResult, error) {
	f.commands = append(f.commands, cmd)
	return messageservice.NoticeResult{Created: true}, nil
}

func TestRetryRewardJobReplaysStableWalletCommandAndCreatesIdempotentWinnerNotice(t *testing.T) {
	job := domain.RewardJob{
		RewardJobID: "reward-job-1", TemplateID: "acttpl_1", TemplateCode: "summer", VersionNo: 3,
		BoardType: domain.BoardTypeTotal, PeriodKey: "total", RankNo: 2, UserID: 88001, ResourceGroupID: 77,
		WalletCommandID: "activity-template-rank:stable-command",
		RewardSnapshot:  domain.RewardSnapshot{SnapshotID: "rgs_rank", SnapshotHash: strings.Repeat("a", 64), Name: "Champion reward"},
	}
	repository := &retryRewardRepository{job: job}
	wallet := &sequenceRuntimeWallet{errors: []error{errors.New("wallet timeout"), nil}}
	notices := &captureRuntimeNotice{}
	svc := New(&fakeRepository{}, nil, nil)
	svc.BindRuntime(repository, wallet)
	svc.BindNotice(notices)
	ctx := appcode.WithContext(context.Background(), "hyapp")

	if _, err := svc.RetryRewardJob(ctx, job.TemplateID, job.RewardJobID, 90001); err == nil {
		t.Fatal("first retry must expose wallet failure")
	}
	got, err := svc.RetryRewardJob(ctx, job.TemplateID, job.RewardJobID, 90001)
	if err != nil {
		t.Fatalf("second RetryRewardJob() error = %v", err)
	}
	if len(wallet.requests) != 2 || wallet.requests[0].GetCommandId() != job.WalletCommandID || wallet.requests[1].GetCommandId() != job.WalletCommandID {
		t.Fatalf("wallet commands are not stable: %+v", wallet.requests)
	}
	if got.Status != domain.ClaimStatusGranted || got.WalletGrantID != "wallet-grant-1" || len(repository.markedFailed) != 1 || len(repository.markedGranted) != 1 {
		t.Fatalf("retry result=%+v repository failed=%v granted=%v", got, repository.markedFailed, repository.markedGranted)
	}
	if len(repository.leaseOwners) != 7 || repository.leaseOwners[0] == "" ||
		repository.leaseOwners[0] != repository.leaseOwners[1] || repository.leaseOwners[1] != repository.leaseOwners[2] ||
		repository.leaseOwners[3] == repository.leaseOwners[0] || repository.leaseOwners[3] != repository.leaseOwners[4] ||
		repository.leaseOwners[4] != repository.leaseOwners[5] || repository.leaseOwners[5] != repository.leaseOwners[6] {
		t.Fatalf("manual retry lease fencing mismatch: %+v", repository.leaseOwners)
	}
	if len(notices.commands) != 1 || notices.commands[0].ProducerEventID != job.RewardJobID+":granted" ||
		notices.commands[0].ActionType != "activity_detail" || notices.commands[0].ActionParam != "board_type=total&period_key=total&template_code=summer&version_no=3" {
		t.Fatalf("winner notice is not stable/readable: %+v", notices.commands)
	}
	notice := notices.commands[0]
	if notice.Title != "activity_template.rank_reward.title" || notice.Summary != "activity_template.rank_reward.summary" ||
		strings.Contains(notice.Title, "Daily leaderboard") || notice.Metadata["content_key"] != "activity_template.rank_reward_granted" ||
		notice.Metadata["template_code"] != "summer" || notice.Metadata["board_type"] != domain.BoardTypeTotal ||
		notice.Metadata["period_key"] != "total" || notice.Metadata["reward_name"] != "Champion reward" {
		t.Fatalf("winner notice must use localization keys and immutable metadata: %+v", notice)
	}
}

func TestTaskClaimWalletFailureRemainsRetryableAfterActivityEnds(t *testing.T) {
	taskDay := "2026-07-14"
	endedAt := time.Date(2026, 7, 14, 23, 59, 0, 0, time.UTC)
	runtime := domain.Runtime{
		Template:  domain.Template{TemplateID: "acttpl_1", TemplateCode: "summer", EndMS: endedAt.UnixMilli()},
		VersionNo: 3, RuntimeFromMS: endedAt.Add(-24 * time.Hour).UnixMilli(), RuntimeToMS: endedAt.UnixMilli(),
	}
	claim := domain.TaskClaim{
		ClaimID: "task-claim-1", TemplateID: runtime.Template.TemplateID, TemplateCode: runtime.Template.TemplateCode,
		VersionNo: runtime.VersionNo, TaskDay: taskDay, TaskKey: "gift-10", UserID: 77001,
		RewardResourceGroupID: 88, WalletCommandID: "activity-template-task:stable-command",
		RewardSnapshot: domain.RewardSnapshot{SnapshotID: "rgs_task", SnapshotHash: strings.Repeat("b", 64)},
	}
	repository := &retryRewardRepository{taskRuntime: runtime, taskClaim: claim}
	wallet := &sequenceRuntimeWallet{errors: []error{errors.New("wallet timeout"), nil}}
	svc := New(&fakeRepository{}, nil, nil)
	svc.BindRuntime(repository, wallet)
	svc.now = func() time.Time { return endedAt.Add(2 * time.Hour) }
	command := domain.ClaimCommand{
		UserID: claim.UserID, RegionID: 6, TemplateCode: claim.TemplateCode, VersionNo: claim.VersionNo, TaskDay: taskDay,
		TaskKey: claim.TaskKey, CommandID: "client-command-1",
	}
	ctx := appcode.WithContext(context.Background(), "hyapp")

	if _, err := svc.ClaimRuntimeTaskReward(ctx, command); err == nil {
		t.Fatal("first claim must expose wallet failure")
	}
	got, err := svc.ClaimRuntimeTaskReward(ctx, command)
	if err != nil {
		t.Fatalf("retry after activity end error = %v", err)
	}
	if repository.taskVersionNo != claim.VersionNo || repository.taskClaimCalls != 2 || len(repository.taskFailed) != 1 || len(repository.taskGranted) != 1 || got.Status != domain.ClaimStatusGranted {
		t.Fatalf("task compensation result=%+v repository=%+v", got, repository)
	}
	if len(wallet.requests) != 2 || wallet.requests[0].GetCommandId() != claim.WalletCommandID || wallet.requests[1].GetCommandId() != claim.WalletCommandID {
		t.Fatalf("task wallet command changed across retries: %+v", wallet.requests)
	}
}
