package services import ( "context" "github.com/google/uuid" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/zap" "gorm.io/driver/sqlite" "gorm.io/gorm" "veza-backend-api/internal/models" ) func setupTestAnalyticsService(t *testing.T) (*AnalyticsService, *gorm.DB, func()) { // Setup in-memory SQLite database db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) require.NoError(t, err) // Enable foreign keys for SQLite db.Exec("PRAGMA foreign_keys = ON") // Auto-migrate err = db.AutoMigrate(&models.User{}, &models.Track{}, &models.TrackPlay{}) require.NoError(t, err) // Create test user user := &models.User{ Username: "testuser", Email: "test@example.com", PasswordHash: "hash", Slug: "testuser", IsActive: true, } err = db.Create(user).Error require.NoError(t, err) // Create test track track := &models.Track{ UserID: user.ID, Title: "Test Track", FilePath: "/test/track.mp3", FileSize: 5 * 1024 * 1024, Format: "MP3", Duration: 180, // 3 minutes IsPublic: true, Status: models.TrackStatusCompleted, } err = db.Create(track).Error require.NoError(t, err) // Setup logger logger := zap.NewNop() // Setup test service service := NewAnalyticsService(db, logger) // Cleanup function cleanup := func() { // Database will be closed automatically } return service, db, cleanup } func TestAnalyticsService_RecordPlay(t *testing.T) { service, db, cleanup := setupTestAnalyticsService(t) defer cleanup() ctx := context.Background() // Get track ID var track models.Track err := db.First(&track).Error require.NoError(t, err) // Get user ID var user models.User err = db.First(&user).Error require.NoError(t, err) t.Run("Record play with user", func(t *testing.T) { userID := user.ID err := service.RecordPlay(ctx, track.ID, &userID, 120, "Chrome", "192.168.1.1") assert.NoError(t, err) // Verify play was recorded var count int64 db.Model(&models.TrackPlay{}).Where("track_id = ? AND user_id = ?", track.ID, userID).Count(&count) assert.Equal(t, int64(1), count) }) t.Run("Record play without user (anonymous)", func(t *testing.T) { err := service.RecordPlay(ctx, track.ID, nil, 60, "Firefox", "10.0.0.1") assert.NoError(t, err) // Verify play was recorded var count int64 db.Model(&models.TrackPlay{}).Where("track_id = ? AND user_id IS NULL", track.ID).Count(&count) assert.Equal(t, int64(1), count) }) t.Run("Record play with invalid track ID", func(t *testing.T) { userID := user.ID err := service.RecordPlay(ctx, uuid.New(), &userID, 120, "Chrome", "192.168.1.1") assert.Error(t, err) assert.Contains(t, err.Error(), "track not found") }) } func TestAnalyticsService_GetTrackStats(t *testing.T) { service, db, cleanup := setupTestAnalyticsService(t) defer cleanup() ctx := context.Background() // Get track ID var track models.Track err := db.First(&track).Error require.NoError(t, err) // Get user ID var user models.User err = db.First(&user).Error require.NoError(t, err) // Create multiple plays userID := user.ID plays := []models.TrackPlay{ {TrackID: track.ID, UserID: &userID, Duration: 120, PlayedAt: time.Now()}, {TrackID: track.ID, UserID: &userID, Duration: 150, PlayedAt: time.Now()}, {TrackID: track.ID, UserID: nil, Duration: 100, PlayedAt: time.Now()}, {TrackID: track.ID, UserID: nil, Duration: 180, PlayedAt: time.Now()}, // Completed } for _, play := range plays { err = db.Create(&play).Error require.NoError(t, err) } t.Run("Get track stats", func(t *testing.T) { stats, err := service.GetTrackStats(ctx, track.ID) assert.NoError(t, err) assert.NotNil(t, stats) assert.Equal(t, int64(4), stats.TotalPlays) assert.Equal(t, int64(1), stats.UniqueListeners) // Only one user (anonymous plays don't count) assert.Greater(t, stats.AverageDuration, 0.0) assert.Greater(t, stats.CompletionRate, 0.0) // At least one play completed 90%+ }) t.Run("Get track stats with invalid track ID", func(t *testing.T) { stats, err := service.GetTrackStats(ctx, uuid.New()) assert.Error(t, err) assert.Nil(t, stats) assert.Contains(t, err.Error(), "track not found") }) t.Run("Get track stats with no plays", func(t *testing.T) { // Create a new track without plays newTrack := &models.Track{ UserID: user.ID, Title: "New Track", FilePath: "/test/new.mp3", FileSize: 5 * 1024 * 1024, Format: "MP3", Duration: 180, IsPublic: true, Status: models.TrackStatusCompleted, } err = db.Create(newTrack).Error require.NoError(t, err) stats, err := service.GetTrackStats(ctx, newTrack.ID) assert.NoError(t, err) assert.NotNil(t, stats) assert.Equal(t, int64(0), stats.TotalPlays) assert.Equal(t, int64(0), stats.UniqueListeners) assert.Equal(t, 0.0, stats.AverageDuration) assert.Equal(t, 0.0, stats.CompletionRate) }) } func TestAnalyticsService_GetPlaysOverTime(t *testing.T) { service, db, cleanup := setupTestAnalyticsService(t) defer cleanup() ctx := context.Background() // Get track ID var track models.Track err := db.First(&track).Error require.NoError(t, err) // Create plays at different times now := time.Now() plays := []models.TrackPlay{ {TrackID: track.ID, Duration: 120, PlayedAt: now.Add(-24 * time.Hour)}, {TrackID: track.ID, Duration: 150, PlayedAt: now.Add(-12 * time.Hour)}, {TrackID: track.ID, Duration: 100, PlayedAt: now}, } for _, play := range plays { err = db.Create(&play).Error require.NoError(t, err) } t.Run("Get plays over time", func(t *testing.T) { startDate := now.Add(-48 * time.Hour) endDate := now.Add(1 * time.Hour) points, err := service.GetPlaysOverTime(ctx, track.ID, startDate, endDate, "day") assert.NoError(t, err) assert.NotNil(t, points) assert.Greater(t, len(points), 0) }) t.Run("Get plays over time with invalid track ID", func(t *testing.T) { startDate := time.Now().Add(-48 * time.Hour) endDate := time.Now() points, err := service.GetPlaysOverTime(ctx, uuid.New(), startDate, endDate, "day") assert.Error(t, err) assert.Nil(t, points) assert.Contains(t, err.Error(), "track not found") }) } func TestAnalyticsService_GetTopTracks(t *testing.T) { service, db, cleanup := setupTestAnalyticsService(t) defer cleanup() ctx := context.Background() // Get user ID var user models.User err := db.First(&user).Error require.NoError(t, err) // Create multiple tracks tracks := []models.Track{ {UserID: user.ID, Title: "Track 1", FilePath: "/test/1.mp3", FileSize: 5 * 1024 * 1024, Format: "MP3", Duration: 180, IsPublic: true, Status: models.TrackStatusCompleted}, {UserID: user.ID, Title: "Track 2", FilePath: "/test/2.mp3", FileSize: 5 * 1024 * 1024, Format: "MP3", Duration: 180, IsPublic: true, Status: models.TrackStatusCompleted}, {UserID: user.ID, Title: "Track 3", FilePath: "/test/3.mp3", FileSize: 5 * 1024 * 1024, Format: "MP3", Duration: 180, IsPublic: true, Status: models.TrackStatusCompleted}, } for i := range tracks { err = db.Create(&tracks[i]).Error require.NoError(t, err) } // Create plays for tracks (Track 1: 5 plays, Track 2: 3 plays, Track 3: 1 play) for i := 0; i < 5; i++ { play := models.TrackPlay{TrackID: tracks[0].ID, Duration: 120, PlayedAt: time.Now()} db.Create(&play) } for i := 0; i < 3; i++ { play := models.TrackPlay{TrackID: tracks[1].ID, Duration: 150, PlayedAt: time.Now()} db.Create(&play) } play := models.TrackPlay{TrackID: tracks[2].ID, Duration: 100, PlayedAt: time.Now()} db.Create(&play) t.Run("Get top tracks", func(t *testing.T) { topTracks, err := service.GetTopTracks(ctx, 10, nil, nil) assert.NoError(t, err) assert.NotNil(t, topTracks) assert.GreaterOrEqual(t, len(topTracks), 3) // Verify ordering (most plays first) if len(topTracks) >= 3 { assert.Equal(t, int64(5), topTracks[0].TotalPlays) // Track 1 assert.Equal(t, int64(3), topTracks[1].TotalPlays) // Track 2 assert.Equal(t, int64(1), topTracks[2].TotalPlays) // Track 3 } }) t.Run("Get top tracks with limit", func(t *testing.T) { topTracks, err := service.GetTopTracks(ctx, 2, nil, nil) assert.NoError(t, err) assert.NotNil(t, topTracks) assert.LessOrEqual(t, len(topTracks), 2) }) t.Run("Get top tracks with date filter", func(t *testing.T) { startDate := time.Now().Add(-24 * time.Hour) endDate := time.Now().Add(1 * time.Hour) topTracks, err := service.GetTopTracks(ctx, 10, &startDate, &endDate) assert.NoError(t, err) assert.NotNil(t, topTracks) }) } func TestAnalyticsService_GetUserStats(t *testing.T) { service, db, cleanup := setupTestAnalyticsService(t) defer cleanup() ctx := context.Background() // Get user ID var user models.User err := db.First(&user).Error require.NoError(t, err) // Get track ID var track models.Track err = db.First(&track).Error require.NoError(t, err) // Create another track anotherTrack := &models.Track{ UserID: user.ID, Title: "Another Track", FilePath: "/test/another.mp3", FileSize: 5 * 1024 * 1024, Format: "MP3", Duration: 180, IsPublic: true, Status: models.TrackStatusCompleted, } err = db.Create(anotherTrack).Error require.NoError(t, err) // Create plays for the user userID := user.ID plays := []models.TrackPlay{ {TrackID: track.ID, UserID: &userID, Duration: 120, PlayedAt: time.Now()}, {TrackID: track.ID, UserID: &userID, Duration: 150, PlayedAt: time.Now()}, {TrackID: anotherTrack.ID, UserID: &userID, Duration: 100, PlayedAt: time.Now()}, } for _, play := range plays { err = db.Create(&play).Error require.NoError(t, err) } t.Run("Get user stats", func(t *testing.T) { stats, err := service.GetUserStats(ctx, user.ID) assert.NoError(t, err) assert.NotNil(t, stats) assert.Equal(t, int64(3), stats.TotalPlays) assert.Equal(t, int64(2), stats.UniqueTracks) assert.Greater(t, stats.TotalDuration, int64(0)) assert.Greater(t, stats.AverageDuration, 0.0) }) t.Run("Get user stats with invalid user ID", func(t *testing.T) { stats, err := service.GetUserStats(ctx, uuid.New()) assert.Error(t, err) assert.Nil(t, stats) assert.Contains(t, err.Error(), "user not found") }) t.Run("Get user stats with no plays", func(t *testing.T) { // Create a new user without plays newUser := &models.User{ Username: "newuser", Email: "new@example.com", PasswordHash: "hash", Slug: "newuser", IsActive: true, } err = db.Create(newUser).Error require.NoError(t, err) stats, err := service.GetUserStats(ctx, newUser.ID) assert.NoError(t, err) assert.NotNil(t, stats) assert.Equal(t, int64(0), stats.TotalPlays) assert.Equal(t, int64(0), stats.UniqueTracks) assert.Equal(t, int64(0), stats.TotalDuration) assert.Equal(t, 0.0, stats.AverageDuration) }) }