package services import ( "testing" "time" "github.com/google/uuid" "go.uber.org/zap" "gorm.io/driver/sqlite" "gorm.io/gorm" "veza-backend-api/internal/models" ) func TestNewTrackRecommendationService(t *testing.T) { db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) if err != nil { t.Fatalf("Failed to open database: %v", err) } logger := zap.NewNop() service := NewTrackRecommendationService(db, logger) if service == nil { t.Error("NewTrackRecommendationService() returned nil") } if service.db == nil { t.Error("NewTrackRecommendationService() returned service with nil db") } if service.logger == nil { t.Error("NewTrackRecommendationService() returned service with nil logger") } } func TestTrackRecommendationService_calculatePopularityScore(t *testing.T) { db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) if err != nil { t.Fatalf("Failed to open database: %v", err) } service := NewTrackRecommendationService(db, zap.NewNop()) tests := []struct { name string track *models.Track wantMin float64 wantMax float64 }{ { name: "high popularity track", track: &models.Track{ PlayCount: 10000, LikeCount: 1000, }, wantMin: 0.8, wantMax: 1.0, }, { name: "medium popularity track", track: &models.Track{ PlayCount: 1000, LikeCount: 100, }, wantMin: 0.3, wantMax: 0.8, }, { name: "low popularity track", track: &models.Track{ PlayCount: 10, LikeCount: 1, }, wantMin: 0.0, wantMax: 0.3, }, { name: "zero popularity track", track: &models.Track{ PlayCount: 0, LikeCount: 0, }, wantMin: 0.0, wantMax: 0.1, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { score := service.calculatePopularityScore(tt.track) if score < tt.wantMin || score > tt.wantMax { t.Errorf("calculatePopularityScore() = %v, want between %v and %v", score, tt.wantMin, tt.wantMax) } if score < 0 || score > 1 { t.Errorf("calculatePopularityScore() = %v, want score between 0 and 1", score) } }) } } func TestTrackRecommendationService_calculateRecencyScore(t *testing.T) { db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) if err != nil { t.Fatalf("Failed to open database: %v", err) } service := NewTrackRecommendationService(db, zap.NewNop()) now := time.Now() tests := []struct { name string track *models.Track wantMin float64 wantMax float64 }{ { name: "very recent track", track: &models.Track{ CreatedAt: now.Add(-1 * time.Hour), }, wantMin: 0.9, wantMax: 1.0, }, { name: "recent track (15 days old)", track: &models.Track{ CreatedAt: now.Add(-15 * 24 * time.Hour), }, wantMin: 0.4, wantMax: 0.6, }, { name: "old track (30+ days)", track: &models.Track{ CreatedAt: now.Add(-35 * 24 * time.Hour), }, wantMin: 0.0, wantMax: 0.2, }, { name: "zero time track", track: &models.Track{ CreatedAt: time.Time{}, }, wantMin: 0.0, wantMax: 0.0, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { score := service.calculateRecencyScore(tt.track) if score < tt.wantMin || score > tt.wantMax { t.Errorf("calculateRecencyScore() = %v, want between %v and %v", score, tt.wantMin, tt.wantMax) } if score < 0 || score > 1 { t.Errorf("calculateRecencyScore() = %v, want score between 0 and 1", score) } }) } } func TestGetKeys(t *testing.T) { tests := []struct { name string m map[uuid.UUID]bool want int }{ { name: "empty map", m: map[uuid.UUID]bool{}, want: 0, }, { name: "map with one key", m: map[uuid.UUID]bool{ uuid.New(): true, }, want: 1, }, { name: "map with multiple keys", m: map[uuid.UUID]bool{ uuid.New(): true, uuid.New(): true, uuid.New(): true, }, want: 3, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := getKeys(tt.m) if len(got) != tt.want { t.Errorf("getKeys() returned %d keys, want %d", len(got), tt.want) } }) } } func TestTrackRecommendationParams_Defaults(t *testing.T) { params := TrackRecommendationParams{} // Test that defaults are applied in GetRecommendations method if params.Limit <= 0 { params.Limit = 20 } if params.Limit > 100 { params.Limit = 100 } if params.MinScore <= 0 { params.MinScore = 0.1 } if params.Limit != 20 { t.Errorf("Expected default limit 20, got %d", params.Limit) } if params.MinScore != 0.1 { t.Errorf("Expected default min score 0.1, got %f", params.MinScore) } } // Note: Full integration tests would require: // 1. A real database with test data (tracks, users, likes, plays) // 2. Verification of collaborative filtering algorithm // 3. Verification of content-based filtering // 4. Performance testing with large datasets // // Example integration test structure: // func TestTrackRecommendationService_GetRecommendations_Integration(t *testing.T) { // // Setup test database // db := setupTestDB(t) // defer cleanupTestDB(t, db) // // // Create test data // userID := createTestUser(t, db) // tracks := createTestTracks(t, db, 50) // createTestLikes(t, db, userID, tracks[:10]) // createTestPlays(t, db, userID, tracks[10:20]) // // service := NewTrackRecommendationService(db, zap.NewNop()) // // ctx := context.Background() // params := TrackRecommendationParams{ // UserID: userID, // Limit: 10, // } // // recommendations, err := service.GetRecommendations(ctx, params) // if err != nil { // t.Fatalf("GetRecommendations() error = %v", err) // } // // if len(recommendations) == 0 { // t.Error("GetRecommendations() returned no recommendations") // } // // // Verify recommendations are sorted by score // for i := 1; i < len(recommendations); i++ { // if recommendations[i].Score > recommendations[i-1].Score { // t.Error("Recommendations are not sorted by score") // } // } // }