package service_test import ( "context" "testing" "financeiro-carvalho/internal/model" "financeiro-carvalho/internal/service" ) type mockGameRepo struct { profile model.PlayerProfile achievements map[string]bool ensuredCats map[int]string removedCats []int catBudgets []model.CategoryBudgetInfo catSpend map[int]float64 } func newMockGameRepo() *mockGameRepo { return &mockGameRepo{ profile: model.PlayerProfile{ID: 1, Level: 1, XP: 0, XPToNext: 100}, achievements: map[string]bool{}, ensuredCats: map[int]string{}, catSpend: map[int]float64{}, } } func (m *mockGameRepo) GetProfile(_ context.Context) (*model.PlayerProfile, error) { cp := m.profile return &cp, nil } func (m *mockGameRepo) AddXP(_ context.Context, _ string, amount int, _ string) (*model.PlayerProfile, error) { m.profile.XP += amount for m.profile.XP >= m.profile.XPToNext { m.profile.XP -= m.profile.XPToNext m.profile.Level++ m.profile.XPToNext = m.profile.Level * m.profile.Level * 100 } cp := m.profile return &cp, nil } func (m *mockGameRepo) ListActiveQuests(_ context.Context) ([]model.PlayerQuest, error) { return nil, nil } func (m *mockGameRepo) EnsurePlayerQuest(_ context.Context, _ int, _ string) (int, error) { return 1, nil } func (m *mockGameRepo) IncrementQuestCount(_ context.Context, _, _ string, _ int) error { return nil } func (m *mockGameRepo) UpdateQuestPct(_ context.Context, _ string, _ float64) error { return nil } func (m *mockGameRepo) ClaimQuest(_ context.Context, _ int) (int, error) { return 50, nil } func (m *mockGameRepo) ListAchievements(_ context.Context) ([]model.Achievement, error) { return nil, nil } func (m *mockGameRepo) IsAchievementUnlocked(_ context.Context, c string) (bool, error) { return m.achievements[c], nil } func (m *mockGameRepo) UnlockAchievement(_ context.Context, c string) (int, error) { m.achievements[c] = true return 100, nil } func (m *mockGameRepo) ListCosmetics(_ context.Context) ([]model.Cosmetic, error) { return nil, nil } func (m *mockGameRepo) EquipCosmetic(_ context.Context, _ int) error { return nil } func (m *mockGameRepo) UnlockCosmeticsForLevel(_ context.Context, _ int) error { return nil } func (m *mockGameRepo) EnsureCategoryAchievement(_ context.Context, catID int, name string) error { m.ensuredCats[catID] = name return nil } func (m *mockGameRepo) RemoveCategoryAchievementIfNotEarned(_ context.Context, catID int) error { m.removedCats = append(m.removedCats, catID) return nil } func (m *mockGameRepo) ListCategoriesWithBudget(_ context.Context) ([]model.CategoryBudgetInfo, error) { return m.catBudgets, nil } func (m *mockGameRepo) GetMonthlySpendByCategory(_ context.Context, _ string, catID int) (float64, error) { return m.catSpend[catID], nil } func TestGame_XPAccumulates(t *testing.T) { repo := newMockGameRepo() svc := service.NewGameService(repo) p, err := svc.AwardXP(context.Background(), "test", 50, "test") if err != nil { t.Fatal(err) } if p.XP != 50 { t.Errorf("expected xp=50, got %d", p.XP) } } func TestGame_LevelUpAtThreshold(t *testing.T) { repo := newMockGameRepo() svc := service.NewGameService(repo) // level 1 threshold is 100 XP p, err := svc.AwardXP(context.Background(), "test", 100, "test") if err != nil { t.Fatal(err) } if p.Level != 2 { t.Errorf("expected level=2 after 100 XP, got %d", p.Level) } if p.XP != 0 { t.Errorf("expected remaining xp=0, got %d", p.XP) } } func TestGame_AchievementNotDuplicated(t *testing.T) { repo := newMockGameRepo() svc := service.NewGameService(repo) svc.CheckAndUnlockAchievement(context.Background(), "first_transaction") svc.CheckAndUnlockAchievement(context.Background(), "first_transaction") // second call should be a no-op — XP only awarded once // repo tracks it as unlocked after first call if !repo.achievements["first_transaction"] { t.Error("achievement should be marked unlocked") } } func TestGame_EnsureCategoryAchievementOnBudgetSet(t *testing.T) { repo := newMockGameRepo() svc := service.NewGameService(repo) budget := 800.0 svc.OnCategoryBudgetSet(context.Background(), 5, "Alimentação", &budget) if repo.ensuredCats[5] != "Alimentação" { t.Errorf("expected EnsureCategoryAchievement called for cat 5, got %v", repo.ensuredCats) } if len(repo.removedCats) != 0 { t.Error("expected no removal when budget is set") } } func TestGame_RemoveCategoryAchievementOnBudgetNull(t *testing.T) { repo := newMockGameRepo() svc := service.NewGameService(repo) svc.OnCategoryBudgetSet(context.Background(), 5, "Alimentação", nil) if len(repo.removedCats) == 0 || repo.removedCats[0] != 5 { t.Errorf("expected RemoveCategoryAchievementIfNotEarned for cat 5, got %v", repo.removedCats) } if len(repo.ensuredCats) != 0 { t.Error("expected no ensure when budget is nil") } } func TestGame_CategoryBudgetUnlock(t *testing.T) { repo := newMockGameRepo() repo.catBudgets = []model.CategoryBudgetInfo{{CategoryID: 5, MonthlyBudget: 800}} repo.catSpend[5] = 750 svc := service.NewGameService(repo) svc.CheckCategoryBudgets(context.Background(), "2026-05") if !repo.achievements["budget_cat_5"] { t.Error("expected budget_cat_5 unlocked when spend 750 <= budget 800") } } func TestGame_CategoryBudgetNoUnlockWhenOverBudget(t *testing.T) { repo := newMockGameRepo() repo.catBudgets = []model.CategoryBudgetInfo{{CategoryID: 5, MonthlyBudget: 800}} repo.catSpend[5] = 850 svc := service.NewGameService(repo) svc.CheckCategoryBudgets(context.Background(), "2026-05") if repo.achievements["budget_cat_5"] { t.Error("expected budget_cat_5 NOT unlocked when spend 850 > budget 800") } }