package achievement_test

import (
	"context"
	"fmt"
	"testing"
	"time"

	"google.golang.org/grpc"
	walletv1 "hyapp.local/api/proto/wallet/v1"
	"hyapp/pkg/appcode"
	domain "hyapp/services/activity-service/internal/domain/achievement"
	achievementservice "hyapp/services/activity-service/internal/service/achievement"
	"hyapp/services/activity-service/internal/testutil/mysqltest"
)

func TestAchievementUnlockBadgeAndRewardFlow(t *testing.T) {
	svc, wallet := newAchievementService(t)
	ctx := appcode.WithContext(context.Background(), "lalu")

	definition, created, err := svc.UpsertAchievementDefinition(ctx, domain.DefinitionCommand{
		CollectionID:           domain.CollectionOrdinary,
		CollectionType:         domain.CollectionOrdinary,
		AchievementType:        domain.TypeOrdinary,
		Title:                  "任务达人",
		Status:                 domain.StatusActive,
		PrimaryBadgeResourceID: 7001,
		RewardResourceGroupID:  9001,
		AutoPinPolicy:          domain.AutoPinIfEmpty,
		HonorVisibility:        true,
		OperatorAdminID:        90001,
		Conditions: []domain.ConditionCommand{
			{MetricType: "task_completed_count", TargetValue: 2, TargetUnit: "count"},
		},
	})
	if err != nil || !created {
		t.Fatalf("UpsertAchievementDefinition failed: created=%v err=%v", created, err)
	}

	first, err := svc.ConsumeAchievementEvent(ctx, domain.Event{
		EventID:       "ach-event-1",
		EventType:     "TaskCompleted",
		SourceService: "activity-service",
		UserID:        10001,
		MetricType:    "task_completed_count",
		Value:         1,
		OccurredAtMS:  fixedAchievementNow().UnixMilli(),
	})
	if err != nil {
		t.Fatalf("ConsumeAchievementEvent first failed: %v", err)
	}
	if first.MatchedAchievementCount != 1 || first.UnlockedAchievementCount != 0 {
		t.Fatalf("first event mismatch: %+v", first)
	}

	second, err := svc.ConsumeAchievementEvent(ctx, domain.Event{
		EventID:       "ach-event-2",
		EventType:     "TaskCompleted",
		SourceService: "activity-service",
		UserID:        10001,
		MetricType:    "task_completed_count",
		Value:         1,
		OccurredAtMS:  fixedAchievementNow().UnixMilli(),
	})
	if err != nil {
		t.Fatalf("ConsumeAchievementEvent second failed: %v", err)
	}
	if second.UnlockedAchievementCount != 1 || second.RewardJobCount != 1 {
		t.Fatalf("second event mismatch: %+v", second)
	}

	duplicate, err := svc.ConsumeAchievementEvent(ctx, domain.Event{
		EventID:       "ach-event-2",
		EventType:     "TaskCompleted",
		SourceService: "activity-service",
		UserID:        10001,
		MetricType:    "task_completed_count",
		Value:         1,
		OccurredAtMS:  fixedAchievementNow().UnixMilli(),
	})
	if err != nil || duplicate.Status != domain.EventStatusConsumed {
		t.Fatalf("duplicate event should return existing consumed status: result=%+v err=%v", duplicate, err)
	}

	items, total, err := svc.ListAchievements(ctx, domain.ListQuery{UserID: 10001, CollectionType: domain.CollectionOrdinary})
	if err != nil {
		t.Fatalf("ListAchievements failed: %v", err)
	}
	if total != 1 || len(items) != 1 || items[0].AchievementID != definition.AchievementID || items[0].UserStatus != domain.UnlockStatusUnlocked || items[0].ProgressValue != 2 {
		t.Fatalf("achievement read mismatch: total=%d items=%+v", total, items)
	}

	profile, err := svc.ListBadgeProfile(ctx, 10001)
	if err != nil {
		t.Fatalf("ListBadgeProfile failed: %v", err)
	}
	if len(profile.HonorBadges) != 1 || profile.HonorBadges[0].ResourceID != 7001 {
		t.Fatalf("honor badge mismatch: %+v", profile)
	}
	if len(profile.ProfileTileBadges) != 1 || profile.ProfileTileBadges[0].ResourceID != 7001 {
		t.Fatalf("profile tile badge mismatch: %+v", profile)
	}

	if _, err := svc.SetBadgeDisplay(ctx, domain.DisplayCommand{
		UserID:    10001,
		Slot:      domain.BadgeSlotProfileTile,
		CommandID: "cmd-badge-display-1",
		Items: []domain.BadgeDisplayItem{
			{ResourceID: 7001},
		},
	}); err != nil {
		t.Fatalf("SetBadgeDisplay failed: %v", err)
	}

	claimed, processed, success, failure, hasMore, err := svc.ProcessAchievementRewardBatch(ctx, "run-1", "worker-1", 10, time.Second)
	if err != nil {
		t.Fatalf("ProcessAchievementRewardBatch failed: %v", err)
	}
	if claimed != 1 || processed != 1 || success != 1 || failure != 0 || hasMore || len(wallet.grants) != 1 {
		t.Fatalf("reward batch mismatch: claimed=%d processed=%d success=%d failure=%d has_more=%v grants=%d", claimed, processed, success, failure, hasMore, len(wallet.grants))
	}
}

func TestWalletBadgeGrantProjectsDisplaySlots(t *testing.T) {
	svc, wallet := newAchievementService(t)
	ctx := appcode.WithContext(context.Background(), "lalu")
	wallet.activeBadgeResources = []*walletv1.UserResourceEntitlement{
		{EntitlementId: "ent-badge-1", ResourceId: 8001},
		{EntitlementId: "ent-badge-2", ResourceId: 8002},
	}

	result, err := svc.ConsumeAchievementEvent(ctx, domain.Event{
		EventID:        "wallet-badge-event-1",
		EventType:      "ResourceGranted",
		SourceService:  "wallet-service",
		UserID:         10001,
		MetricType:     "wallet_badge_grant",
		Value:          1,
		OccurredAtMS:   fixedAchievementNow().UnixMilli(),
		DimensionsJSON: `{"grant_id":"grant-badge-1","grant_source":"admin","items":[{"resource_id":8001,"resource_type":"badge","entitlement_id":"ent-badge-1","metadata_json":"{\"badge_form\":\"tile\"}"},{"resource_id":8002,"resource_type":"badge","entitlement_id":"ent-badge-2","metadata_json":"{\"badge_form\":\"strip\",\"default_slot\":\"profile_strip\"}"}]}`,
	})
	if err != nil {
		t.Fatalf("ConsumeAchievementEvent wallet badge failed: %v", err)
	}
	if result.Status != domain.EventStatusConsumed {
		t.Fatalf("wallet badge event should be consumed, got %+v", result)
	}

	profile, err := svc.ListBadgeProfile(ctx, 10001)
	if err != nil {
		t.Fatalf("ListBadgeProfile failed: %v", err)
	}
	if len(profile.HonorBadges) != 1 || profile.HonorBadges[0].ResourceID != 8001 || profile.HonorBadges[0].SourceType != domain.SourceSystemGrant {
		t.Fatalf("honor badge projection mismatch: %+v", profile)
	}
	if len(profile.ProfileTileBadges) != 1 || profile.ProfileTileBadges[0].ResourceID != 8001 {
		t.Fatalf("profile tile projection mismatch: %+v", profile)
	}
	if len(profile.StripBadges) != 1 || profile.StripBadges[0].ResourceID != 8002 || profile.StripBadges[0].BadgeForm != domain.BadgeFormStrip {
		t.Fatalf("strip badge projection mismatch: %+v", profile)
	}

	duplicate, err := svc.ConsumeAchievementEvent(ctx, domain.Event{
		EventID:        "wallet-badge-event-1",
		EventType:      "ResourceGranted",
		SourceService:  "wallet-service",
		UserID:         10001,
		MetricType:     "wallet_badge_grant",
		Value:          1,
		OccurredAtMS:   fixedAchievementNow().UnixMilli(),
		DimensionsJSON: `{"grant_id":"grant-badge-1","grant_source":"admin","items":[]}`,
	})
	if err != nil || duplicate.Status != domain.EventStatusConsumed {
		t.Fatalf("duplicate wallet badge event should return existing status: result=%+v err=%v", duplicate, err)
	}

	// 体验卡/限时赠送到期后 wallet 的 active 列表不再包含旧资源；读取必须立即回收三个旧槽。
	wallet.activeBadgeResources = nil
	expiredProfile, err := svc.ListBadgeProfile(ctx, 10001)
	if err != nil {
		t.Fatalf("ListBadgeProfile after badge expiry failed: %v", err)
	}
	if len(expiredProfile.StripBadges) != 0 || len(expiredProfile.ProfileTileBadges) != 0 || len(expiredProfile.HonorBadges) != 0 {
		t.Fatalf("expired entitlement-backed badges must be reclaimed: %+v", expiredProfile)
	}

	// 回收后新的 VIP 体验卡要能重新占用首个长徽章位，不能被旧的过期投影挤到后面或跳过。
	wallet.activeBadgeResources = []*walletv1.UserResourceEntitlement{{EntitlementId: "ent-vip-trial-3", ResourceId: 8003}}
	if _, err := svc.ConsumeAchievementEvent(ctx, domain.Event{
		EventID:        "wallet-badge-event-vip-trial-3",
		EventType:      "ResourceGroupGranted",
		SourceService:  "wallet-service",
		UserID:         10001,
		MetricType:     "wallet_badge_grant",
		Value:          1,
		OccurredAtMS:   fixedAchievementNow().UnixMilli(),
		DimensionsJSON: `{"grant_id":"grant-vip-trial-3","grant_source":"vip_trial","items":[{"resource_id":8003,"resource_type":"badge","entitlement_id":"ent-vip-trial-3","metadata_json":"{\"badge_form\":\"strip\",\"default_slot\":\"profile_strip\"}"}]}`,
	}); err != nil {
		t.Fatalf("project replacement VIP trial badge failed: %v", err)
	}
	replacedProfile, err := svc.ListBadgeProfile(ctx, 10001)
	if err != nil {
		t.Fatalf("ListBadgeProfile replacement VIP badge failed: %v", err)
	}
	if len(replacedProfile.StripBadges) != 1 || replacedProfile.StripBadges[0].Position != 1 ||
		replacedProfile.StripBadges[0].ResourceID != 8003 || replacedProfile.StripBadges[0].SourceType != domain.SourceVIP {
		t.Fatalf("replacement VIP badge projection mismatch: %+v", replacedProfile)
	}
}

func TestDirectBadgeAchievementWithoutConditions(t *testing.T) {
	svc, _ := newAchievementService(t)
	ctx := appcode.WithContext(context.Background(), "lalu")

	if _, _, err := svc.UpsertAchievementDefinition(ctx, domain.DefinitionCommand{
		CollectionType:         domain.CollectionEvent,
		AchievementType:        domain.TypeActivityLimited,
		Title:                  "活动徽章",
		Description:            "活动达成后展示",
		Status:                 domain.StatusActive,
		PrimaryBadgeResourceID: 8101,
		OperatorAdminID:        90001,
	}); err != nil {
		t.Fatalf("UpsertAchievementDefinition without conditions failed: %v", err)
	}
	if _, err := svc.ConsumeAchievementEvent(ctx, domain.Event{
		EventID:        "wallet-badge-event-direct",
		EventType:      "ResourceGranted",
		SourceService:  "wallet-service",
		UserID:         10002,
		MetricType:     "wallet_badge_grant",
		Value:          1,
		OccurredAtMS:   fixedAchievementNow().UnixMilli(),
		DimensionsJSON: `{"grant_id":"grant-badge-direct","grant_source":"admin","items":[{"resource_id":8101,"resource_type":"badge","entitlement_id":"ent-badge-direct","metadata_json":"{\"badge_form\":\"tile\"}"}]}`,
	}); err != nil {
		t.Fatalf("project badge resource failed: %v", err)
	}
	items, total, err := svc.ListAchievements(ctx, domain.ListQuery{UserID: 10002, CollectionType: domain.CollectionEvent})
	if err != nil {
		t.Fatalf("ListAchievements direct badge failed: %v", err)
	}
	if total != 1 || len(items) != 1 || items[0].UserStatus != domain.UnlockStatusUnlocked || items[0].ProgressValue != 1 || items[0].TargetValue != 1 {
		t.Fatalf("direct badge achievement read mismatch: total=%d items=%+v", total, items)
	}

	deleted, err := svc.DeleteAchievementDefinition(ctx, items[0].AchievementID, 90001)
	if err != nil || deleted.Status != domain.StatusArchived {
		t.Fatalf("DeleteAchievementDefinition failed: item=%+v err=%v", deleted, err)
	}
	adminItems, adminTotal, err := svc.ListAchievementDefinitions(ctx, domain.ListQuery{CollectionType: domain.CollectionEvent})
	if err != nil {
		t.Fatalf("ListAchievementDefinitions after delete failed: %v", err)
	}
	if adminTotal != 0 || len(adminItems) != 0 {
		t.Fatalf("archived achievement should be hidden by default: total=%d items=%+v", adminTotal, adminItems)
	}
}

func TestAchievementListsReturnHighSortOrderFirst(t *testing.T) {
	svc, _ := newAchievementService(t)
	ctx := appcode.WithContext(context.Background(), "lalu")
	definitions := []domain.DefinitionCommand{
		{Title: "低优先级徽章", SortOrder: 10, PrimaryBadgeResourceID: 8201},
		{Title: "高优先级徽章", SortOrder: 30, PrimaryBadgeResourceID: 8202},
		{Title: "中优先级徽章", SortOrder: 20, PrimaryBadgeResourceID: 8203},
	}
	for _, definition := range definitions {
		definition.CollectionType = domain.CollectionOrdinary
		definition.AchievementType = domain.TypeOrdinary
		definition.Status = domain.StatusActive
		definition.OperatorAdminID = 90001
		if _, _, err := svc.UpsertAchievementDefinition(ctx, definition); err != nil {
			t.Fatalf("upsert sorted achievement failed: %v", err)
		}
	}

	adminItems, adminTotal, err := svc.ListAchievementDefinitions(ctx, domain.ListQuery{CollectionType: domain.CollectionOrdinary})
	if err != nil {
		t.Fatalf("ListAchievementDefinitions sorted failed: %v", err)
	}
	if adminTotal != 3 || len(adminItems) != 3 {
		t.Fatalf("admin sorted achievement count mismatch: total=%d items=%+v", adminTotal, adminItems)
	}
	if adminItems[0].SortOrder != 30 || adminItems[1].SortOrder != 20 || adminItems[2].SortOrder != 10 {
		t.Fatalf("admin achievements should be sorted by sort_order desc: %+v", adminItems)
	}

	userItems, userTotal, err := svc.ListAchievements(ctx, domain.ListQuery{UserID: 10003, CollectionType: domain.CollectionOrdinary})
	if err != nil {
		t.Fatalf("ListAchievements sorted failed: %v", err)
	}
	if userTotal != 3 || len(userItems) != 3 {
		t.Fatalf("user sorted achievement count mismatch: total=%d items=%+v", userTotal, userItems)
	}
	if userItems[0].SortOrder != 30 || userItems[1].SortOrder != 20 || userItems[2].SortOrder != 10 {
		t.Fatalf("user achievements should be sorted by sort_order desc: %+v", userItems)
	}
}

func newAchievementService(t *testing.T) (*achievementservice.Service, *fakeAchievementWallet) {
	t.Helper()
	repository := mysqltest.NewRepository(t)
	wallet := &fakeAchievementWallet{}
	svc := achievementservice.New(repository, wallet)
	svc.SetClock(fixedAchievementNow)
	return svc, wallet
}

func fixedAchievementNow() time.Time {
	return time.Date(2026, 5, 14, 4, 0, 0, 0, time.UTC)
}

type fakeAchievementWallet struct {
	grants               []*walletv1.GrantResourceGroupRequest
	activeBadgeResources []*walletv1.UserResourceEntitlement
}

func (f *fakeAchievementWallet) GrantResourceGroup(_ context.Context, req *walletv1.GrantResourceGroupRequest, _ ...grpc.CallOption) (*walletv1.ResourceGrantResponse, error) {
	f.grants = append(f.grants, req)
	return &walletv1.ResourceGrantResponse{
		Grant: &walletv1.ResourceGrant{
			GrantId: fmt.Sprintf("achievement-grant-%d", len(f.grants)),
			Status:  "succeeded",
		},
	}, nil
}

func (f *fakeAchievementWallet) ListUserResources(_ context.Context, _ *walletv1.ListUserResourcesRequest, _ ...grpc.CallOption) (*walletv1.ListUserResourcesResponse, error) {
	return &walletv1.ListUserResourcesResponse{Resources: f.activeBadgeResources}, nil
}
