package growth_test

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

	"google.golang.org/grpc"
	"google.golang.org/protobuf/proto"
	activityevents "hyapp.local/api/proto/events/room/v1"
	walletv1 "hyapp.local/api/proto/wallet/v1"
	"hyapp/pkg/appcode"
	domain "hyapp/services/activity-service/internal/domain/growth"
	growthservice "hyapp/services/activity-service/internal/service/growth"
	"hyapp/services/activity-service/internal/testutil/mysqltest"
)

func TestGrowthLevelEventRewardFlow(t *testing.T) {
	svc, wallet := newGrowthService(t)
	ctx := appcode.WithContext(context.Background(), "lalu")
	seedGrowthRules(t, ctx, svc, domain.TrackWealth)

	result, err := svc.ConsumeLevelEvent(ctx, domain.ValueEvent{
		EventID:         "level-event-1",
		SourceEventID:   "room-event-1",
		SourceService:   "room-service",
		SourceEventType: "RoomGiftSent",
		UserID:          10001,
		Track:           domain.TrackWealth,
		MetricType:      domain.MetricGiftSpendCoin,
		ValueDelta:      250,
		OccurredAtMS:    fixedGrowthNow().UnixMilli(),
		DimensionsJSON:  `{"room_id":"room-1"}`,
	})
	if err != nil {
		t.Fatalf("ConsumeLevelEvent failed: %v", err)
	}
	if !result.LevelChanged || result.PreviousLevel != 0 || result.NewLevel != 2 || result.RewardJobCount != 2 {
		t.Fatalf("level result mismatch: %+v", result)
	}

	duplicate, err := svc.ConsumeLevelEvent(ctx, domain.ValueEvent{
		EventID:         "level-event-1",
		SourceEventID:   "room-event-1",
		SourceService:   "room-service",
		SourceEventType: "RoomGiftSent",
		UserID:          10001,
		Track:           domain.TrackWealth,
		MetricType:      domain.MetricGiftSpendCoin,
		ValueDelta:      250,
		OccurredAtMS:    fixedGrowthNow().UnixMilli(),
		DimensionsJSON:  `{"room_id":"room-1"}`,
	})
	if err != nil || duplicate.Status != domain.EventStatusDuplicate {
		t.Fatalf("duplicate event mismatch: result=%+v err=%v", duplicate, err)
	}

	overview, err := svc.GetMyLevelOverview(ctx, 10001)
	if err != nil {
		t.Fatalf("GetMyLevelOverview failed: %v", err)
	}
	wealth := findTrackOverview(overview.Tracks, domain.TrackWealth)
	if wealth.Level != 2 || wealth.TierID == 0 || wealth.TotalValue != 250 || wealth.DisplayAvatarFrameResourceID != 502 || wealth.DisplayBadgeResourceID != 702 || wealth.DisplayBadgeSourceLevel != 2 || wealth.RewardPendingCount != 2 {
		t.Fatalf("wealth overview mismatch: %+v", wealth)
	}
	profiles, err := svc.BatchGetUserLevelDisplayProfiles(ctx, []int64{10001})
	if err != nil {
		t.Fatalf("BatchGetUserLevelDisplayProfiles failed: %v", err)
	}
	if len(profiles.Profiles) != 1 || profiles.Profiles[0].Wealth.BadgeResourceID != 702 || profiles.Profiles[0].Wealth.BadgeSourceLevel != 2 {
		t.Fatalf("display profile should use highest configured rule badge: %+v", profiles.Profiles)
	}

	claimed, processed, success, failure, hasMore, err := svc.ProcessLevelRewardBatch(ctx, "run-1", "worker-1", 10, time.Second)
	if err != nil {
		t.Fatalf("ProcessLevelRewardBatch failed: %v", err)
	}
	if claimed != 2 || processed != 2 || success != 2 || failure != 0 || hasMore || len(wallet.grants) != 2 {
		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))
	}
	rewards, total, err := svc.ListLevelRewards(ctx, domain.RewardQuery{UserID: 10001, Status: domain.RewardStatusGranted})
	if err != nil {
		t.Fatalf("ListLevelRewards failed: %v", err)
	}
	if total != 2 || len(rewards) != 2 {
		t.Fatalf("granted rewards mismatch: total=%d rewards=%+v", total, rewards)
	}
}

func TestGrowthLevelRewardGrantsLevelResources(t *testing.T) {
	svc, wallet := newGrowthService(t)
	ctx := appcode.WithContext(context.Background(), "lalu")
	if _, created, err := svc.UpsertLevelRule(ctx, domain.RuleCommand{
		Track:             domain.TrackWealth,
		Level:             1,
		RequiredValue:     100,
		Name:              "wealth 1",
		Status:            domain.StatusActive,
		SortOrder:         1,
		DisplayConfigJSON: `{"avatar_frame_resource_id":801,"short_badge_resource_id":901}`,
		OperatorAdminID:   90001,
	}); err != nil || !created {
		t.Fatalf("seed level resource rule failed: created=%v err=%v", created, err)
	}

	result, err := svc.ConsumeLevelEvent(ctx, domain.ValueEvent{
		EventID:         "level-avatar-frame-event-1",
		SourceEventID:   "room-event-avatar-frame-1",
		SourceService:   "room-service",
		SourceEventType: "RoomGiftSent",
		UserID:          10002,
		Track:           domain.TrackWealth,
		MetricType:      domain.MetricGiftSpendCoin,
		ValueDelta:      100,
		OccurredAtMS:    fixedGrowthNow().UnixMilli(),
		DimensionsJSON:  `{"room_id":"room-1"}`,
	})
	if err != nil {
		t.Fatalf("ConsumeLevelEvent failed: %v", err)
	}
	if result.RewardJobCount != 1 {
		t.Fatalf("level resources should create one reward job: %+v", result)
	}
	overview, err := svc.GetMyLevelOverview(ctx, 10002)
	if err != nil {
		t.Fatalf("GetMyLevelOverview failed: %v", err)
	}
	wealth := findTrackOverview(overview.Tracks, domain.TrackWealth)
	if wealth.DisplayAvatarFrameResourceID != 801 {
		t.Fatalf("level avatar frame should override display profile: %+v", wealth)
	}
	profiles, err := svc.BatchGetUserLevelDisplayProfiles(ctx, []int64{10002})
	if err != nil {
		t.Fatalf("BatchGetUserLevelDisplayProfiles failed: %v", err)
	}
	if len(profiles.Profiles) != 1 || profiles.Profiles[0].Wealth.AvatarFrameResourceID != 801 {
		t.Fatalf("room display profile should use level avatar frame: %+v", profiles.Profiles)
	}

	claimed, processed, success, failure, hasMore, err := svc.ProcessLevelRewardBatch(ctx, "run-avatar-frame", "worker-1", 10, time.Second)
	if err != nil {
		t.Fatalf("ProcessLevelRewardBatch failed: %v", err)
	}
	if claimed != 1 || processed != 1 || success != 1 || failure != 0 || hasMore {
		t.Fatalf("reward batch mismatch: claimed=%d processed=%d success=%d failure=%d has_more=%v", claimed, processed, success, failure, hasMore)
	}
	if len(wallet.grants) != 0 || len(wallet.resourceGrants) != 2 {
		t.Fatalf("level resource reward should grant two resources only: groups=%d resources=%d", len(wallet.grants), len(wallet.resourceGrants))
	}
	grantedResources := make(map[int64]*walletv1.GrantResourceRequest, len(wallet.resourceGrants))
	for _, req := range wallet.resourceGrants {
		grantedResources[req.GetResourceId()] = req
	}
	for _, resourceID := range []int64{801, 901} {
		req := grantedResources[resourceID]
		if req == nil || req.GetDurationMs() != 9999*24*60*60*1000 || req.GetQuantity() != 1 || req.GetGrantSource() != domain.GrantSourceGrowthLevel {
			t.Fatalf("level resource grant mismatch: id=%d grants=%+v", resourceID, wallet.resourceGrants)
		}
	}
}

func TestGrowthRoomGiftEventFeedsWealthAndCharm(t *testing.T) {
	svc, _ := newGrowthService(t)
	ctx := appcode.WithContext(context.Background(), "lalu")
	seedGrowthRules(t, ctx, svc, domain.TrackWealth)
	seedGrowthRules(t, ctx, svc, domain.TrackCharm)

	body, err := proto.Marshal(&activityevents.RoomGiftSent{
		SenderUserId:     20001,
		TargetUserId:     20002,
		GiftId:           "gift-1",
		GiftCount:        1,
		GiftValue:        150,
		BillingReceiptId: "bill-1",
		VisibleRegionId:  86,
		CommandId:        "cmd-gift-1",
	})
	if err != nil {
		t.Fatalf("marshal room gift failed: %v", err)
	}
	consumed, err := svc.HandleRoomEvent(ctx, &activityevents.EventEnvelope{
		EventId:      "room-gift-event-1",
		RoomId:       "room-1",
		EventType:    "RoomGiftSent",
		RoomVersion:  7,
		OccurredAtMs: fixedGrowthNow().UnixMilli(),
		Body:         body,
		AppCode:      "lalu",
	})
	if err != nil {
		t.Fatalf("HandleRoomEvent failed: %v", err)
	}
	if consumed != 2 {
		t.Fatalf("room gift should create wealth and charm events, consumed=%d", consumed)
	}
	sender, err := svc.GetLevelTrack(ctx, 20001, domain.TrackWealth)
	if err != nil {
		t.Fatalf("GetLevelTrack sender failed: %v", err)
	}
	target, err := svc.GetLevelTrack(ctx, 20002, domain.TrackCharm)
	if err != nil {
		t.Fatalf("GetLevelTrack target failed: %v", err)
	}
	if sender.Overview.Level != 1 || sender.Overview.TotalValue != 150 {
		t.Fatalf("sender wealth mismatch: %+v", sender.Overview)
	}
	if target.Overview.Level != 1 || target.Overview.TotalValue != 150 {
		t.Fatalf("target charm mismatch: %+v", target.Overview)
	}
}

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

	body, err := proto.Marshal(&activityevents.RoomGiftSent{
		SenderUserId:     20001,
		TargetUserId:     20002,
		GiftId:           "gift-zero-heat",
		GiftCount:        1,
		GiftValue:        0,
		BillingReceiptId: "bill-zero-heat",
		VisibleRegionId:  86,
		CommandId:        "cmd-gift-zero-heat",
	})
	if err != nil {
		t.Fatalf("marshal zero heat room gift failed: %v", err)
	}
	consumed, err := svc.HandleRoomEvent(ctx, &activityevents.EventEnvelope{
		EventId:      "room-gift-event-zero-heat",
		RoomId:       "room-1",
		EventType:    "RoomGiftSent",
		RoomVersion:  8,
		OccurredAtMs: fixedGrowthNow().UnixMilli(),
		Body:         body,
		AppCode:      "lalu",
	})
	if err != nil {
		t.Fatalf("zero heat RoomGiftSent should be accepted and skipped: %v", err)
	}
	if consumed != 0 {
		t.Fatalf("zero heat RoomGiftSent should not create growth events, consumed=%d", consumed)
	}
}

func TestListLevelConfigReturnsAdminRulesAndTiers(t *testing.T) {
	svc, _ := newGrowthService(t)
	ctx := appcode.WithContext(context.Background(), "lalu")
	seedGrowthRules(t, ctx, svc, domain.TrackGame)

	config, err := svc.ListLevelConfig(ctx, domain.ConfigQuery{Track: domain.TrackGame})
	if err != nil {
		t.Fatalf("ListLevelConfig failed: %v", err)
	}
	if len(config.Tracks) != 1 || config.Tracks[0].Track != domain.TrackGame {
		t.Fatalf("track config mismatch: %+v", config.Tracks)
	}
	if len(config.Rules) != 2 || config.Rules[0].Level != 1 || config.Rules[1].RequiredValue != 200 {
		t.Fatalf("rule config mismatch: %+v", config.Rules)
	}
	if len(config.Tiers) != 2 || config.Tiers[1].DisplayBadgeResourceID != 602 {
		t.Fatalf("tier config mismatch: %+v", config.Tiers)
	}
	if config.ServerTimeMS != fixedGrowthNow().UnixMilli() {
		t.Fatalf("server time mismatch: %d", config.ServerTimeMS)
	}
}

func TestZeroLevelRuleAndTierAreValid(t *testing.T) {
	svc, _ := newGrowthService(t)
	ctx := appcode.WithContext(context.Background(), "lalu")
	if _, created, err := svc.UpsertLevelRule(ctx, domain.RuleCommand{
		Track:             domain.TrackWealth,
		Level:             0,
		RequiredValue:     0,
		Name:              "wealth 0",
		Status:            domain.StatusActive,
		SortOrder:         0,
		DisplayConfigJSON: `{"long_badge_resource_id":700}`,
		OperatorAdminID:   90001,
	}); err != nil || !created {
		t.Fatalf("seed level rule 0 failed: created=%v err=%v", created, err)
	}
	if _, created, err := svc.UpsertLevelTier(ctx, domain.TierCommand{
		Track:           domain.TrackWealth,
		MinLevel:        0,
		MaxLevel:        9,
		Name:            "wealth 0-9",
		Status:          domain.StatusActive,
		OperatorAdminID: 90001,
	}); err != nil || !created {
		t.Fatalf("seed tier 0 failed: created=%v err=%v", created, err)
	}
	detail, err := svc.GetLevelTrack(ctx, 30001, domain.TrackWealth)
	if err != nil {
		t.Fatalf("GetLevelTrack failed: %v", err)
	}
	if detail.Overview.Level != 0 || detail.Overview.CurrentLevelRequiredValue != 0 || detail.Overview.DisplayBadgeResourceID != 700 || detail.Overview.DisplayBadgeSourceLevel != 0 {
		t.Fatalf("zero level overview mismatch: %+v", detail.Overview)
	}
}

func TestIssueRegistrationLevelBadgesGrantsLevelOneBadges(t *testing.T) {
	svc, wallet := newGrowthService(t)
	ctx := appcode.WithContext(context.Background(), "lalu")
	seedGrowthRules(t, ctx, svc, domain.TrackWealth)
	seedGrowthRules(t, ctx, svc, domain.TrackGame)
	seedGrowthRules(t, ctx, svc, domain.TrackCharm)

	grants, err := svc.IssueRegistrationLevelBadges(ctx, 30001, "registration_level_badges:lalu:30001")
	if err != nil {
		t.Fatalf("IssueRegistrationLevelBadges failed: %v", err)
	}
	if len(grants.Grants) != 3 || len(wallet.resourceGrants) != 3 {
		t.Fatalf("registration level badge grants mismatch: grants=%+v wallet=%d", grants.Grants, len(wallet.resourceGrants))
	}
	for _, req := range wallet.resourceGrants {
		if req.GetTargetUserId() != 30001 || req.GetResourceId() != 701 || req.GetQuantity() != 1 || req.GetGrantSource() != domain.GrantSourceGrowthLevel {
			t.Fatalf("wallet grant request mismatch: %+v", req)
		}
	}
}

func newGrowthService(t *testing.T) (*growthservice.Service, *fakeGrowthWallet) {
	t.Helper()
	repository := mysqltest.NewRepository(t)
	wallet := &fakeGrowthWallet{}
	svc := growthservice.New(repository, wallet)
	svc.SetClock(fixedGrowthNow)
	return svc, wallet
}

func seedGrowthRules(t *testing.T, ctx context.Context, svc *growthservice.Service, track string) {
	t.Helper()
	if _, created, err := svc.UpsertLevelRule(ctx, domain.RuleCommand{
		Track:                 track,
		Level:                 1,
		RequiredValue:         100,
		Name:                  track + " 1",
		Status:                domain.StatusActive,
		RewardResourceGroupID: 0,
		SortOrder:             1,
		DisplayConfigJSON:     `{"long_badge_resource_id":701}`,
		OperatorAdminID:       90001,
	}); err != nil || !created {
		t.Fatalf("seed level rule 1 failed: created=%v err=%v", created, err)
	}
	if _, created, err := svc.UpsertLevelRule(ctx, domain.RuleCommand{
		Track:                 track,
		Level:                 2,
		RequiredValue:         200,
		Name:                  track + " 2",
		Status:                domain.StatusActive,
		RewardResourceGroupID: 9002,
		SortOrder:             2,
		DisplayConfigJSON:     `{"long_badge_resource_id":702}`,
		OperatorAdminID:       90001,
	}); err != nil || !created {
		t.Fatalf("seed level rule 2 failed: created=%v err=%v", created, err)
	}
	if _, created, err := svc.UpsertLevelTier(ctx, domain.TierCommand{
		Track:                        track,
		MinLevel:                     1,
		MaxLevel:                     1,
		Name:                         track + " 1-1",
		DisplayAvatarFrameResourceID: 501,
		DisplayBadgeResourceID:       601,
		RewardResourceGroupID:        9001,
		Status:                       domain.StatusActive,
		OperatorAdminID:              90001,
	}); err != nil || !created {
		t.Fatalf("seed tier 1 failed: created=%v err=%v", created, err)
	}
	if _, created, err := svc.UpsertLevelTier(ctx, domain.TierCommand{
		Track:                        track,
		MinLevel:                     2,
		MaxLevel:                     10,
		Name:                         track + " 2-10",
		DisplayAvatarFrameResourceID: 502,
		DisplayBadgeResourceID:       602,
		RewardResourceGroupID:        9003,
		Status:                       domain.StatusActive,
		OperatorAdminID:              90001,
	}); err != nil || !created {
		t.Fatalf("seed tier 2 failed: created=%v err=%v", created, err)
	}
}

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

func findTrackOverview(items []domain.TrackOverview, track string) domain.TrackOverview {
	for _, item := range items {
		if item.Track == track {
			return item
		}
	}
	return domain.TrackOverview{}
}

type fakeGrowthWallet struct {
	grants         []*walletv1.GrantResourceGroupRequest
	resourceGrants []*walletv1.GrantResourceRequest
}

func (f *fakeGrowthWallet) GrantResource(_ context.Context, req *walletv1.GrantResourceRequest, _ ...grpc.CallOption) (*walletv1.ResourceGrantResponse, error) {
	f.resourceGrants = append(f.resourceGrants, req)
	return &walletv1.ResourceGrantResponse{
		Grant: &walletv1.ResourceGrant{
			GrantId: fmt.Sprintf("resource-grant-%d", len(f.resourceGrants)),
			Status:  "succeeded",
		},
	}, nil
}

func (f *fakeGrowthWallet) 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("grant-%d", len(f.grants)),
			Status:  "succeeded",
		},
	}, nil
}
