package achievements import ( "context" "fmt" "testing" "time" ) // MockLogger implements the Logger interface for testing type MockLogger struct { InfoMessages []string ErrorMessages []string DebugMessages []string WarningMessages []string } func (ml *MockLogger) LogInfo(system, format string, args ...any) { ml.InfoMessages = append(ml.InfoMessages, fmt.Sprintf(format, args...)) } func (ml *MockLogger) LogError(system, format string, args ...any) { ml.ErrorMessages = append(ml.ErrorMessages, fmt.Sprintf(format, args...)) } func (ml *MockLogger) LogDebug(system, format string, args ...any) { ml.DebugMessages = append(ml.DebugMessages, fmt.Sprintf(format, args...)) } func (ml *MockLogger) LogWarning(system, format string, args ...any) { ml.WarningMessages = append(ml.WarningMessages, fmt.Sprintf(format, args...)) } // MockDatabase implements basic database operations for testing type MockDatabase struct { achievements []Achievement playerAchievements map[uint32][]PlayerAchievement requirements map[uint32][]Requirement rewards map[uint32][]Reward } func NewMockDatabase() *MockDatabase { return &MockDatabase{ achievements: []Achievement{}, playerAchievements: make(map[uint32][]PlayerAchievement), requirements: make(map[uint32][]Requirement), rewards: make(map[uint32][]Reward), } } func (db *MockDatabase) Query(query string, args ...any) (*MockRows, error) { // Simulate database queries based on the query string if query == ` SELECT id, achievement_id, title, uncompleted_text, completed_text, category, expansion, icon, point_value, qty_req, hide_achievement, unknown3a, unknown3b, max_version FROM achievements ORDER BY achievement_id ` { return &MockRows{ achievements: db.achievements, position: 0, queryType: "achievements", }, nil } // Handle other query types as needed return &MockRows{queryType: "unknown"}, nil } func (db *MockDatabase) Exec(query string, args ...any) (any, error) { // Mock exec operations return nil, nil } // MockRows simulates database rows for testing type MockRows struct { achievements []Achievement position int queryType string closed bool } func (rows *MockRows) Next() bool { if rows.closed { return false } if rows.queryType == "achievements" { return rows.position < len(rows.achievements) } return false } func (rows *MockRows) Scan(dest ...any) error { if rows.queryType == "achievements" && rows.position < len(rows.achievements) { achievement := &rows.achievements[rows.position] // Scan values in order expected by the query if len(dest) >= 14 { *dest[0].(*uint32) = achievement.ID *dest[1].(*uint32) = achievement.AchievementID *dest[2].(*string) = achievement.Title *dest[3].(*string) = achievement.UncompletedText *dest[4].(*string) = achievement.CompletedText *dest[5].(*string) = achievement.Category *dest[6].(*string) = achievement.Expansion *dest[7].(*uint16) = achievement.Icon *dest[8].(*uint32) = achievement.PointValue *dest[9].(*uint32) = achievement.QtyRequired var hideInt int if achievement.Hide { hideInt = 1 } *dest[10].(*int) = hideInt *dest[11].(*uint32) = achievement.Unknown3A *dest[12].(*uint32) = achievement.Unknown3B *dest[13].(*uint32) = achievement.MaxVersion } rows.position++ } return nil } func (rows *MockRows) Close() error { rows.closed = true return nil } func (rows *MockRows) Err() error { return nil } // Test data setup func createTestAchievements() []Achievement { return []Achievement{ { ID: 1, AchievementID: 100, Title: "First Kill", UncompletedText: "Kill your first enemy", CompletedText: "You have killed your first enemy!", Category: CategoryCombat, Expansion: ExpansionBase, Icon: 1001, PointValue: 10, QtyRequired: 1, Hide: false, Requirements: []Requirement{ {AchievementID: 100, Name: "Kill Enemy", QtyRequired: 1}, }, Rewards: []Reward{ {AchievementID: 100, Reward: "10 Experience Points"}, }, }, { ID: 2, AchievementID: 101, Title: "Explorer", UncompletedText: "Discover 5 new locations", CompletedText: "You have explored many locations!", Category: CategoryExploration, Expansion: ExpansionBase, Icon: 1002, PointValue: 25, QtyRequired: 5, Hide: false, Requirements: []Requirement{ {AchievementID: 101, Name: "Discover Location", QtyRequired: 5}, }, Rewards: []Reward{ {AchievementID: 101, Reward: "Map Fragment"}, }, }, } } func setupTestManager() (*AchievementManager, *MockLogger, *MockDatabase) { logger := &MockLogger{} mockDB := NewMockDatabase() // Add test data mockDB.achievements = createTestAchievements() config := AchievementConfig{ EnablePacketUpdates: true, AutoCompleteOnReached: true, EnableStatistics: true, MaxCachedPlayers: 100, } // Create manager without database initially for isolated testing manager := NewAchievementManager(nil, logger, config) return manager, logger, mockDB } func TestNewAchievementManager(t *testing.T) { logger := &MockLogger{} config := AchievementConfig{ EnablePacketUpdates: true, MaxCachedPlayers: 100, } manager := NewAchievementManager(nil, logger, config) if manager == nil { t.Fatal("NewAchievementManager returned nil") } if manager.logger != logger { t.Error("Logger not set correctly") } if manager.config != config { t.Error("Config not set correctly") } if len(manager.achievements) != 0 { t.Error("Expected empty achievements map") } } func TestAchievementManagerInitializeWithoutDatabase(t *testing.T) { manager, logger, _ := setupTestManager() // Initialize with no database (should handle gracefully) ctx := context.Background() err := manager.Initialize(ctx) if err != nil { t.Fatalf("Initialize failed: %v", err) } // Should have no achievements loaded if len(manager.achievements) != 0 { t.Error("Expected no achievements without database") } // Logger should have recorded the initialization if len(logger.InfoMessages) == 0 { t.Error("Expected initialization log message") } } func TestGetAchievement(t *testing.T) { manager, _, _ := setupTestManager() // Manually add achievements for testing testAchievements := createTestAchievements() for i := range testAchievements { manager.achievements[testAchievements[i].AchievementID] = &testAchievements[i] } // Test existing achievement achievement, exists := manager.GetAchievement(100) if !exists { t.Error("Expected achievement 100 to exist") } if achievement.Title != "First Kill" { t.Errorf("Expected title 'First Kill', got '%s'", achievement.Title) } // Test non-existing achievement _, exists = manager.GetAchievement(999) if exists { t.Error("Expected achievement 999 to not exist") } } func TestGetAllAchievements(t *testing.T) { manager, _, _ := setupTestManager() // Manually add achievements for testing testAchievements := createTestAchievements() for i := range testAchievements { manager.achievements[testAchievements[i].AchievementID] = &testAchievements[i] } // Build indexes for _, achievement := range manager.achievements { manager.categoryIndex[achievement.Category] = append(manager.categoryIndex[achievement.Category], achievement) manager.expansionIndex[achievement.Expansion] = append(manager.expansionIndex[achievement.Expansion], achievement) } achievements := manager.GetAllAchievements() if len(achievements) != 2 { t.Errorf("Expected 2 achievements, got %d", len(achievements)) } } func TestGetAchievementsByCategory(t *testing.T) { manager, _, _ := setupTestManager() // Manually add achievements and build indexes testAchievements := createTestAchievements() for i := range testAchievements { manager.achievements[testAchievements[i].AchievementID] = &testAchievements[i] manager.categoryIndex[testAchievements[i].Category] = append(manager.categoryIndex[testAchievements[i].Category], &testAchievements[i]) } combatAchievements := manager.GetAchievementsByCategory(CategoryCombat) if len(combatAchievements) != 1 { t.Errorf("Expected 1 combat achievement, got %d", len(combatAchievements)) } explorationAchievements := manager.GetAchievementsByCategory(CategoryExploration) if len(explorationAchievements) != 1 { t.Errorf("Expected 1 exploration achievement, got %d", len(explorationAchievements)) } } func TestGetAchievementsByExpansion(t *testing.T) { manager, _, _ := setupTestManager() // Manually add achievements and build indexes testAchievements := createTestAchievements() for i := range testAchievements { manager.achievements[testAchievements[i].AchievementID] = &testAchievements[i] manager.expansionIndex[testAchievements[i].Expansion] = append(manager.expansionIndex[testAchievements[i].Expansion], &testAchievements[i]) } baseAchievements := manager.GetAchievementsByExpansion(ExpansionBase) if len(baseAchievements) != 2 { t.Errorf("Expected 2 base expansion achievements, got %d", len(baseAchievements)) } } func TestGetCategories(t *testing.T) { manager, _, _ := setupTestManager() // Manually build category index manager.categoryIndex[CategoryCombat] = []*Achievement{} manager.categoryIndex[CategoryExploration] = []*Achievement{} categories := manager.GetCategories() if len(categories) != 2 { t.Errorf("Expected 2 categories, got %d", len(categories)) } // Check that both categories exist categoryMap := make(map[string]bool) for _, category := range categories { categoryMap[category] = true } if !categoryMap[CategoryCombat] { t.Error("Expected Combat category") } if !categoryMap[CategoryExploration] { t.Error("Expected Exploration category") } } func TestGetExpansions(t *testing.T) { manager, _, _ := setupTestManager() // Manually build expansion index manager.expansionIndex[ExpansionBase] = []*Achievement{} expansions := manager.GetExpansions() if len(expansions) != 1 { t.Errorf("Expected 1 expansion, got %d", len(expansions)) } if expansions[0] != ExpansionBase { t.Errorf("Expected expansion '%s', got '%s'", ExpansionBase, expansions[0]) } } func TestUpdatePlayerProgress(t *testing.T) { manager, logger, _ := setupTestManager() // Add test achievement testAchievement := &Achievement{ AchievementID: 100, Title: "Test Achievement", QtyRequired: 5, PointValue: 10, } manager.achievements[100] = testAchievement ctx := context.Background() characterID := uint32(12345) achievementID := uint32(100) // Test updating progress err := manager.UpdatePlayerProgress(ctx, characterID, achievementID, 3) if err != nil { t.Fatalf("UpdatePlayerProgress failed: %v", err) } // Verify progress was set progress, err := manager.GetPlayerAchievementProgress(characterID, achievementID) if err != nil { t.Fatalf("GetPlayerAchievementProgress failed: %v", err) } if progress != 3 { t.Errorf("Expected progress 3, got %d", progress) } // Test auto-completion when reaching required quantity err = manager.UpdatePlayerProgress(ctx, characterID, achievementID, 5) if err != nil { t.Fatalf("UpdatePlayerProgress failed: %v", err) } // Should be completed now completed, err := manager.IsPlayerAchievementCompleted(characterID, achievementID) if err != nil { t.Fatalf("IsPlayerAchievementCompleted failed: %v", err) } if !completed { t.Error("Expected achievement to be completed") } // Check that completion was logged found := false for _, msg := range logger.InfoMessages { if msg == fmt.Sprintf("Character %d completed achievement %d", characterID, achievementID) { found = true break } } if !found { t.Error("Expected completion log message") } } func TestCompletePlayerAchievement(t *testing.T) { manager, logger, _ := setupTestManager() // Add test achievement manager.achievements[100] = &Achievement{AchievementID: 100, Title: "Test"} ctx := context.Background() characterID := uint32(12345) achievementID := uint32(100) // Complete the achievement err := manager.CompletePlayerAchievement(ctx, characterID, achievementID) if err != nil { t.Fatalf("CompletePlayerAchievement failed: %v", err) } // Verify completion completed, err := manager.IsPlayerAchievementCompleted(characterID, achievementID) if err != nil { t.Fatalf("IsPlayerAchievementCompleted failed: %v", err) } if !completed { t.Error("Expected achievement to be completed") } // Check that completion was logged found := false for _, msg := range logger.InfoMessages { if msg == fmt.Sprintf("Character %d completed achievement %d", characterID, achievementID) { found = true break } } if !found { t.Error("Expected completion log message") } // Test completing already completed achievement (should not log again) originalLogCount := len(logger.InfoMessages) err = manager.CompletePlayerAchievement(ctx, characterID, achievementID) if err != nil { t.Fatalf("CompletePlayerAchievement failed on already completed: %v", err) } if len(logger.InfoMessages) != originalLogCount { t.Error("Expected no additional log message for already completed achievement") } } func TestGetPlayerAchievements(t *testing.T) { manager, _, _ := setupTestManager() characterID := uint32(12345) // Test with no achievements achievements, err := manager.GetPlayerAchievements(characterID) if err != nil { t.Fatalf("GetPlayerAchievements failed: %v", err) } if len(achievements) != 0 { t.Error("Expected empty achievements map") } // Add an achievement manually manager.playerAchievements[characterID] = map[uint32]*PlayerAchievement{ 100: { CharacterID: characterID, AchievementID: 100, Progress: 3, CompletedDate: time.Now(), }, } // Test with achievements achievements, err = manager.GetPlayerAchievements(characterID) if err != nil { t.Fatalf("GetPlayerAchievements failed: %v", err) } if len(achievements) != 1 { t.Errorf("Expected 1 achievement, got %d", len(achievements)) } achievement, exists := achievements[100] if !exists { t.Error("Expected achievement 100 to exist") } if achievement.Progress != 3 { t.Errorf("Expected progress 3, got %d", achievement.Progress) } } func TestGetPlayerStatistics(t *testing.T) { manager, _, _ := setupTestManager() // Add test achievements manager.achievements[100] = &Achievement{AchievementID: 100, PointValue: 10, Category: CategoryCombat} manager.achievements[101] = &Achievement{AchievementID: 101, PointValue: 25, Category: CategoryExploration} characterID := uint32(12345) // Add player achievements - one completed, one in progress manager.playerAchievements[characterID] = map[uint32]*PlayerAchievement{ 100: { CharacterID: characterID, AchievementID: 100, Progress: 10, CompletedDate: time.Now(), // Completed }, 101: { CharacterID: characterID, AchievementID: 101, Progress: 3, CompletedDate: time.Time{}, // In progress }, } stats, err := manager.GetPlayerStatistics(characterID) if err != nil { t.Fatalf("GetPlayerStatistics failed: %v", err) } if stats.CharacterID != characterID { t.Errorf("Expected character ID %d, got %d", characterID, stats.CharacterID) } if stats.TotalAchievements != 2 { t.Errorf("Expected 2 total achievements, got %d", stats.TotalAchievements) } if stats.CompletedCount != 1 { t.Errorf("Expected 1 completed achievement, got %d", stats.CompletedCount) } if stats.InProgressCount != 1 { t.Errorf("Expected 1 in-progress achievement, got %d", stats.InProgressCount) } if stats.TotalPointsEarned != 10 { t.Errorf("Expected 10 points earned, got %d", stats.TotalPointsEarned) } if stats.TotalPointsAvailable != 35 { t.Errorf("Expected 35 points available, got %d", stats.TotalPointsAvailable) } if stats.CompletedByCategory[CategoryCombat] != 1 { t.Errorf("Expected 1 combat achievement completed, got %d", stats.CompletedByCategory[CategoryCombat]) } } func TestInvalidAchievementOperations(t *testing.T) { manager, _, _ := setupTestManager() ctx := context.Background() characterID := uint32(12345) invalidAchievementID := uint32(999) // Test updating progress for non-existent achievement err := manager.UpdatePlayerProgress(ctx, characterID, invalidAchievementID, 1) if err == nil { t.Error("Expected error for invalid achievement ID") } // Test completing non-existent achievement err = manager.CompletePlayerAchievement(ctx, characterID, invalidAchievementID) if err == nil { t.Error("Expected error for invalid achievement ID") } } func TestThreadSafety(t *testing.T) { manager, _, _ := setupTestManager() // Add test achievement manager.achievements[100] = &Achievement{ AchievementID: 100, QtyRequired: 10, PointValue: 10, } ctx := context.Background() characterID := uint32(12345) achievementID := uint32(100) // Test concurrent access done := make(chan bool, 10) // Start 10 concurrent operations for i := 0; i < 10; i++ { go func(progress uint32) { defer func() { done <- true }() // Update progress err := manager.UpdatePlayerProgress(ctx, characterID, achievementID, progress) if err != nil { t.Errorf("UpdatePlayerProgress failed: %v", err) return } // Read progress _, err = manager.GetPlayerAchievementProgress(characterID, achievementID) if err != nil { t.Errorf("GetPlayerAchievementProgress failed: %v", err) return } // Check completion status _, err = manager.IsPlayerAchievementCompleted(characterID, achievementID) if err != nil { t.Errorf("IsPlayerAchievementCompleted failed: %v", err) return } }(uint32(i + 1)) } // Wait for all operations to complete for i := 0; i < 10; i++ { <-done } } func TestPacketBuilding(t *testing.T) { manager, logger, _ := setupTestManager() // Add test achievement manager.achievements[100] = &Achievement{ AchievementID: 100, Title: "Test Achievement", CompletedText: "Completed!", UncompletedText: "Not completed", Category: CategoryCombat, Expansion: ExpansionBase, Icon: 1001, PointValue: 10, QtyRequired: 1, Hide: false, } characterID := uint32(12345) clientVersion := int32(1096) // Test sending packet with no player achievements (should not error) err := manager.SendPlayerAchievementsPacket(characterID, clientVersion) if err != nil { t.Fatalf("SendPlayerAchievementsPacket failed: %v", err) } // Should have debug message about packet building found := false expectedMsg := fmt.Sprintf("Built achievement list packet for character %d (0 achievements)", characterID) for _, msg := range logger.DebugMessages { if expectedMsg == msg { found = true break } } if !found { t.Errorf("Expected debug message '%s', got messages: %v", expectedMsg, logger.DebugMessages) } } func TestShutdown(t *testing.T) { manager, logger, _ := setupTestManager() ctx := context.Background() err := manager.Shutdown(ctx) if err != nil { t.Fatalf("Shutdown failed: %v", err) } // Should have info message about shutdown found := false for _, msg := range logger.InfoMessages { if msg == "Shutting down achievement manager" { found = true break } } if !found { t.Error("Expected shutdown log message") } } // Benchmark tests func BenchmarkGetAchievement(b *testing.B) { manager, _, _ := setupTestManager() // Add many achievements for i := uint32(0); i < 1000; i++ { manager.achievements[i] = &Achievement{AchievementID: i, Title: fmt.Sprintf("Achievement %d", i)} } b.ResetTimer() for i := 0; i < b.N; i++ { _, _ = manager.GetAchievement(uint32(i % 1000)) } } func BenchmarkUpdatePlayerProgress(b *testing.B) { manager, _, _ := setupTestManager() // Add test achievement manager.achievements[100] = &Achievement{ AchievementID: 100, QtyRequired: 1000000, // High value so it doesn't auto-complete PointValue: 10, } ctx := context.Background() characterID := uint32(12345) achievementID := uint32(100) b.ResetTimer() for i := 0; i < b.N; i++ { _ = manager.UpdatePlayerProgress(ctx, characterID, achievementID, uint32(i)) } }