package inviteactivity

import (
	"context"
	"testing"
	"time"

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

func TestClaimRechargeOnlyHighestTierDelta(t *testing.T) {
	repo := newFakeInviteActivityRepo()
	repo.config = domain.Config{
		AppCode: "lalu",
		Enabled: true,
		Tiers: []domain.Tier{
			{TierID: 1, RewardType: domain.RewardTypeRecharge, TierCode: "r1", TierName: "80k", ThresholdCoinAmount: 80000, RewardCoinAmount: 1000, Status: domain.TierStatusActive, SortOrder: 1},
			{TierID: 2, RewardType: domain.RewardTypeRecharge, TierCode: "r2", TierName: "160k", ThresholdCoinAmount: 160000, RewardCoinAmount: 2500, Status: domain.TierStatusActive, SortOrder: 2},
		},
	}
	repo.progress = domain.Progress{AppCode: "lalu", CycleKey: "2026-06", UserID: 42, TotalRechargeCoinAmount: 200000}
	repo.claims = []domain.Claim{
		{ClaimID: "IAR_42_2026-06_recharge_1", AppCode: "lalu", CycleKey: "2026-06", UserID: 42, RewardType: domain.RewardTypeRecharge, TierID: 1, RewardCoinAmount: 1000, Status: domain.ClaimStatusGranted},
	}
	wallet := &fakeInviteActivityWallet{}
	service := New(repo, wallet, nil)
	service.SetClock(func() time.Time { return time.Date(2026, 6, 11, 12, 0, 0, 0, time.UTC) })
	ctx := appcode.WithContext(context.Background(), "lalu")

	if _, _, err := service.Claim(ctx, 42, domain.RewardTypeRecharge, 1, "cmd-low"); err == nil {
		t.Fatalf("expected lower recharge tier claim to be rejected")
	}

	claim, status, err := service.Claim(ctx, 42, domain.RewardTypeRecharge, 2, "cmd-high")
	if err != nil {
		t.Fatalf("claim highest recharge tier: %v", err)
	}
	if claim.RewardCoinAmount != 1500 {
		t.Fatalf("expected delta reward 1500, got %d", claim.RewardCoinAmount)
	}
	if len(wallet.requests) != 1 || wallet.requests[0].Amount != 1500 {
		t.Fatalf("wallet amount mismatch: %#v", wallet.requests)
	}
	if len(status.Claims) != 2 {
		t.Fatalf("expected refreshed status with 2 claims, got %d", len(status.Claims))
	}
}

func TestClaimValidInviteEachReachedTier(t *testing.T) {
	repo := newFakeInviteActivityRepo()
	repo.config = domain.Config{
		AppCode: "lalu",
		Enabled: true,
		Tiers: []domain.Tier{
			{TierID: 11, RewardType: domain.RewardTypeValidInvite, TierCode: "v1", TierName: "one", ThresholdValidInviteCount: 1, RewardCoinAmount: 100, Status: domain.TierStatusActive, SortOrder: 1},
			{TierID: 12, RewardType: domain.RewardTypeValidInvite, TierCode: "v3", TierName: "three", ThresholdValidInviteCount: 3, RewardCoinAmount: 500, Status: domain.TierStatusActive, SortOrder: 2},
		},
	}
	repo.progress = domain.Progress{AppCode: "lalu", CycleKey: "2026-06", UserID: 77, ValidInviteCount: 3}
	wallet := &fakeInviteActivityWallet{}
	service := New(repo, wallet, nil)
	service.SetClock(func() time.Time { return time.Date(2026, 6, 11, 12, 0, 0, 0, time.UTC) })
	ctx := appcode.WithContext(context.Background(), "lalu")

	first, _, err := service.Claim(ctx, 77, domain.RewardTypeValidInvite, 11, "cmd-valid-1")
	if err != nil {
		t.Fatalf("claim first valid invite tier: %v", err)
	}
	second, _, err := service.Claim(ctx, 77, domain.RewardTypeValidInvite, 12, "cmd-valid-3")
	if err != nil {
		t.Fatalf("claim second valid invite tier: %v", err)
	}
	if first.RewardCoinAmount != 100 || second.RewardCoinAmount != 500 {
		t.Fatalf("unexpected rewards: first=%d second=%d", first.RewardCoinAmount, second.RewardCoinAmount)
	}
	if len(wallet.requests) != 2 {
		t.Fatalf("expected 2 wallet credits, got %d", len(wallet.requests))
	}
	if _, _, err := service.Claim(ctx, 77, domain.RewardTypeValidInvite, 11, "cmd-valid-repeat"); err == nil {
		t.Fatalf("expected duplicate valid invite tier claim to be rejected")
	}
}

func TestConsumeInviteEventGrantsPerInviteRewardsIdempotently(t *testing.T) {
	repo := newFakeInviteActivityRepo()
	repo.config = domain.Config{
		AppCode:                          "lalu",
		Enabled:                          true,
		PerInviteInviterRewardCoinAmount: 700,
		PerInviteInviteeRewardCoinAmount: 200,
	}
	wallet := &fakeInviteActivityWallet{}
	service := New(repo, wallet, nil)
	service.SetClock(func() time.Time { return time.Date(2026, 6, 11, 12, 0, 0, 0, time.UTC) })
	ctx := appcode.WithContext(context.Background(), "lalu")
	event := domain.InviteEvent{
		AppCode:       "lalu",
		EventID:       "evt-user-invited-1",
		EventType:     domain.EventTypeUserInvited,
		InviterUserID: 42,
		InvitedUserID: 99,
		InviteCode:    "ABC123",
		Source:        "invite_h5",
		OccurredAtMS:  time.Date(2026, 6, 11, 11, 0, 0, 0, time.UTC).UnixMilli(),
	}

	if err := service.ConsumeInviteEvent(ctx, event); err != nil {
		t.Fatalf("consume invite event: %v", err)
	}
	if len(wallet.requests) != 2 {
		t.Fatalf("expected inviter and invitee wallet credits, got %d", len(wallet.requests))
	}
	if wallet.requests[0].GetTargetUserId() != 42 || wallet.requests[0].GetAmount() != 700 || wallet.requests[0].GetRewardType() != domain.RewardTypeInviteInviter {
		t.Fatalf("inviter reward mismatch: %#v", wallet.requests[0])
	}
	if wallet.requests[1].GetTargetUserId() != 99 || wallet.requests[1].GetAmount() != 200 || wallet.requests[1].GetRewardType() != domain.RewardTypeInvitee {
		t.Fatalf("invitee reward mismatch: %#v", wallet.requests[1])
	}
	if len(repo.claims) != 2 || repo.claims[0].Status != domain.ClaimStatusGranted || repo.claims[1].Status != domain.ClaimStatusGranted {
		t.Fatalf("expected 2 granted claims, got %#v", repo.claims)
	}

	if err := service.ConsumeInviteEvent(ctx, event); err != nil {
		t.Fatalf("consume duplicate invite event: %v", err)
	}
	if len(wallet.requests) != 2 || len(repo.claims) != 2 {
		t.Fatalf("duplicate event should reuse existing claims: wallet=%d claims=%d", len(wallet.requests), len(repo.claims))
	}
}

func TestListLeaderboardDefaultsCurrentCycleAndCapsPageSize(t *testing.T) {
	repo := newFakeInviteActivityRepo()
	repo.leaderboardEntries = []domain.LeaderboardEntry{
		{RankNo: 1, UserID: 1001, ValidInviteCount: 3, TotalRechargeCoinAmount: 240000},
	}
	service := New(repo, nil, nil)
	service.SetClock(func() time.Time { return time.Date(2026, 6, 11, 12, 0, 0, 0, time.UTC) })
	ctx := appcode.WithContext(context.Background(), "lalu")

	result, err := service.ListLeaderboard(ctx, domain.LeaderboardQuery{PageSize: 1000})
	if err != nil {
		t.Fatalf("list leaderboard: %v", err)
	}
	if result.CycleKey != "2026-06" {
		t.Fatalf("expected current cycle, got %q", result.CycleKey)
	}
	if repo.lastLeaderboardQuery.Page != 1 || repo.lastLeaderboardQuery.PageSize != 100 {
		t.Fatalf("unexpected normalized query: %#v", repo.lastLeaderboardQuery)
	}
	if result.Total != 1 || len(result.Entries) != 1 || result.Entries[0].UserID != 1001 {
		t.Fatalf("unexpected leaderboard result: %#v", result)
	}
}

func TestListSummariesTrimsCycleAndCapsPageSize(t *testing.T) {
	repo := newFakeInviteActivityRepo()
	repo.summaries = []domain.Summary{
		{AppCode: "lalu", CycleKey: "2026-06", UserID: 1001, InviteCount: 2, ValidInviteCount: 1, TotalRechargeCoinAmount: 9000, TotalRewardCoinAmount: 700},
	}
	service := New(repo, nil, nil)
	ctx := appcode.WithContext(context.Background(), "lalu")

	summaries, total, err := service.ListSummaries(ctx, domain.SummaryQuery{CycleKey: " 2026-06 ", PageSize: 1000})
	if err != nil {
		t.Fatalf("list summaries: %v", err)
	}
	if repo.lastSummaryQuery.CycleKey != "2026-06" || repo.lastSummaryQuery.Page != 1 || repo.lastSummaryQuery.PageSize != 100 {
		t.Fatalf("unexpected normalized summary query: %#v", repo.lastSummaryQuery)
	}
	if total != 1 || len(summaries) != 1 || summaries[0].UserID != 1001 {
		t.Fatalf("unexpected summary result: total=%d summaries=%#v", total, summaries)
	}
}

type fakeInviteActivityRepo struct {
	config               domain.Config
	progress             domain.Progress
	claims               []domain.Claim
	summaries            []domain.Summary
	leaderboardEntries   []domain.LeaderboardEntry
	lastSummaryQuery     domain.SummaryQuery
	lastLeaderboardQuery domain.LeaderboardQuery
}

func newFakeInviteActivityRepo() *fakeInviteActivityRepo {
	return &fakeInviteActivityRepo{}
}

func (r *fakeInviteActivityRepo) GetInviteActivityRewardConfig(context.Context) (domain.Config, bool, error) {
	return r.config, true, nil
}

func (r *fakeInviteActivityRepo) UpdateInviteActivityRewardConfig(context.Context, domain.Config, int64) (domain.Config, error) {
	return domain.Config{}, nil
}

func (r *fakeInviteActivityRepo) GetInviteActivityRewardProgress(context.Context, string, int64) (domain.Progress, bool, error) {
	return r.progress, true, nil
}

func (r *fakeInviteActivityRepo) ListInviteActivityRewardClaimsByUserCycle(context.Context, string, int64) ([]domain.Claim, error) {
	return append([]domain.Claim(nil), r.claims...), nil
}

func (r *fakeInviteActivityRepo) ConsumeInviteActivityRechargeEvent(context.Context, domain.RechargeEvent, domain.Cycle, int64) (domain.ConsumeResult, error) {
	return domain.ConsumeResult{}, nil
}

func (r *fakeInviteActivityRepo) RecordInviteActivityInviteEvent(context.Context, domain.InviteEvent, domain.Cycle, string, string, int64) (domain.ConsumeResult, error) {
	return domain.ConsumeResult{Consumed: true, Reason: domain.ReasonEligible}, nil
}

func (r *fakeInviteActivityRepo) ConsumeInviteActivityValidInviteEvent(context.Context, domain.ValidInviteEvent, domain.Cycle, int64) (domain.ConsumeResult, error) {
	return domain.ConsumeResult{}, nil
}

func (r *fakeInviteActivityRepo) CreateInviteActivityRewardClaim(_ context.Context, claim domain.Claim, nowMS int64) (domain.Claim, error) {
	for _, existing := range r.claims {
		if existing.ClaimID == claim.ClaimID {
			return existing, nil
		}
	}
	claim.Status = domain.ClaimStatusPending
	claim.CreatedAtMS = nowMS
	claim.UpdatedAtMS = nowMS
	r.claims = append(r.claims, claim)
	return claim, nil
}

func (r *fakeInviteActivityRepo) MarkInviteActivityRewardClaimGranted(_ context.Context, claimID string, walletTransactionID string, grantedAtMS int64) (domain.Claim, error) {
	for index := range r.claims {
		if r.claims[index].ClaimID != claimID {
			continue
		}
		r.claims[index].Status = domain.ClaimStatusGranted
		r.claims[index].WalletTransactionID = walletTransactionID
		r.claims[index].GrantedAtMS = grantedAtMS
		r.claims[index].UpdatedAtMS = grantedAtMS
		r.progress.ClaimedRewardCoinAmount += r.claims[index].RewardCoinAmount
		return r.claims[index], nil
	}
	return domain.Claim{}, nil
}

func (r *fakeInviteActivityRepo) MarkInviteActivityRewardClaimFailed(context.Context, string, string, int64) error {
	return nil
}

func (r *fakeInviteActivityRepo) ListInviteActivityRewardClaims(context.Context, domain.ClaimQuery) ([]domain.Claim, int64, error) {
	return append([]domain.Claim(nil), r.claims...), int64(len(r.claims)), nil
}

func (r *fakeInviteActivityRepo) ListInviteActivityRewardSummaries(_ context.Context, query domain.SummaryQuery) ([]domain.Summary, int64, error) {
	r.lastSummaryQuery = query
	return append([]domain.Summary(nil), r.summaries...), int64(len(r.summaries)), nil
}

func (r *fakeInviteActivityRepo) ListInviteActivityLeaderboard(_ context.Context, query domain.LeaderboardQuery) ([]domain.LeaderboardEntry, int64, error) {
	r.lastLeaderboardQuery = query
	return append([]domain.LeaderboardEntry(nil), r.leaderboardEntries...), int64(len(r.leaderboardEntries)), nil
}

type fakeInviteActivityWallet struct {
	requests []*walletv1.CreditInviteActivityRewardRequest
}

func (w *fakeInviteActivityWallet) CreditInviteActivityReward(_ context.Context, req *walletv1.CreditInviteActivityRewardRequest, _ ...grpc.CallOption) (*walletv1.CreditInviteActivityRewardResponse, error) {
	w.requests = append(w.requests, req)
	return &walletv1.CreditInviteActivityRewardResponse{
		TransactionId: "wallet-" + req.GetCommandId(),
		GrantedAtMs:   1781179200000,
	}, nil
}
