package services import ( "context" "testing" "time" "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/zap/zaptest" "gorm.io/driver/sqlite" "gorm.io/gorm" "veza-backend-api/internal/models" ) func setupTestPlaybackRetentionServiceDB(t *testing.T) (*gorm.DB, *PlaybackRetentionService) { db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) require.NoError(t, err) db.Exec("PRAGMA foreign_keys = ON") err = db.AutoMigrate(&models.User{}, &models.Track{}, &models.PlaybackAnalytics{}) require.NoError(t, err) logger := zaptest.NewLogger(t) service := NewPlaybackRetentionService(db, logger) return db, service } func TestNewPlaybackRetentionService(t *testing.T) { db, _ := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) logger := zaptest.NewLogger(t) service := NewPlaybackRetentionService(db, logger) assert.NotNil(t, service) assert.Equal(t, db, service.db) assert.NotNil(t, service.logger) } func TestNewPlaybackRetentionService_NilLogger(t *testing.T) { db, _ := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) service := NewPlaybackRetentionService(db, nil) assert.NotNil(t, service) assert.NotNil(t, service.logger) } func TestPlaybackRetentionService_AnalyzeRetention_NoSessions(t *testing.T) { db, service := setupTestPlaybackRetentionServiceDB(t) ctx := context.Background() // Créer user et track userID := uuid.New() trackID := uuid.New() user := &models.User{ID: userID, Username: "testuser", Email: "test@example.com", IsActive: true} db.Create(user) track := &models.Track{ ID: trackID, UserID: userID, Title: "Test Track", FilePath: "/test.mp3", FileSize: 1024, Format: "MP3", Duration: 180, IsPublic: true, Status: models.TrackStatusCompleted, } db.Create(track) result, err := service.AnalyzeRetention(ctx, trackID, 10) require.NoError(t, err) assert.NotNil(t, result) assert.Equal(t, trackID, result.TrackID) assert.Equal(t, 180, result.TrackDuration) assert.Equal(t, int64(0), result.TotalSessions) assert.Len(t, result.SegmentRetentions, 10) assert.Len(t, result.ExitPoints, 0) } func TestPlaybackRetentionService_AnalyzeRetention_InvalidTrackID(t *testing.T) { _, service := setupTestPlaybackRetentionServiceDB(t) ctx := context.Background() result, err := service.AnalyzeRetention(ctx, uuid.Nil, 10) assert.Error(t, err) assert.Contains(t, err.Error(), "invalid track ID") assert.Nil(t, result) } func TestPlaybackRetentionService_AnalyzeRetention_TrackNotFound(t *testing.T) { _, service := setupTestPlaybackRetentionServiceDB(t) ctx := context.Background() result, err := service.AnalyzeRetention(ctx, uuid.New(), 10) assert.Error(t, err) assert.Contains(t, err.Error(), "track not found") assert.Nil(t, result) } func TestPlaybackRetentionService_AnalyzeRetention_WithSessions(t *testing.T) { db, service := setupTestPlaybackRetentionServiceDB(t) ctx := context.Background() // Créer user et track userID := uuid.New() trackID := uuid.New() user := &models.User{ID: userID, Username: "testuser", Email: "test@example.com", IsActive: true} db.Create(user) track := &models.Track{ ID: trackID, UserID: userID, Title: "Test Track", FilePath: "/test.mp3", FileSize: 1024, Format: "MP3", Duration: 180, // 3 minutes IsPublic: true, Status: models.TrackStatusCompleted, } db.Create(track) // Créer des analytics avec différents taux de complétion now := time.Now() analytics1 := &models.PlaybackAnalytics{ TrackID: trackID, UserID: userID, PlayTime: 90, // 50% de 180 PauseCount: 2, SeekCount: 1, CompletionRate: 50.0, StartedAt: now, CreatedAt: now, } analytics2 := &models.PlaybackAnalytics{ TrackID: trackID, UserID: userID, PlayTime: 135, // 75% de 180 PauseCount: 1, SeekCount: 0, CompletionRate: 75.0, StartedAt: now, CreatedAt: now, } analytics3 := &models.PlaybackAnalytics{ TrackID: trackID, UserID: userID, PlayTime: 180, // 100% de 180 PauseCount: 0, SeekCount: 0, CompletionRate: 100.0, StartedAt: now, CreatedAt: now, } db.Create(analytics1) db.Create(analytics2) db.Create(analytics3) result, err := service.AnalyzeRetention(ctx, trackID, 10) require.NoError(t, err) assert.NotNil(t, result) assert.Equal(t, trackID, result.TrackID) assert.Equal(t, 180, result.TrackDuration) assert.Equal(t, int64(3), result.TotalSessions) assert.Len(t, result.SegmentRetentions, 10) assert.Greater(t, len(result.ExitPoints), 0) assert.NotZero(t, result.EngagementMetrics.EngagementScore) } func TestPlaybackRetentionService_CalculateSegmentRetention(t *testing.T) { _, service := setupTestPlaybackRetentionServiceDB(t) // Créer des analytics avec différents taux de complétion analytics := []models.PlaybackAnalytics{ {PlayTime: 90, CompletionRate: 50.0}, // 50% de 180 {PlayTime: 135, CompletionRate: 75.0}, // 75% de 180 {PlayTime: 180, CompletionRate: 100.0}, // 100% de 180 } retentions := service.calculateSegmentRetention(analytics, 180, 10) assert.Len(t, retentions, 10) // Vérifier que le premier segment (0-10%) a 100% de rétention (toutes les sessions commencent) assert.Equal(t, 100.0, retentions[0].RetentionRate) // Vérifier que le segment 5 (50-60%) a 100% de rétention (toutes les sessions atteignent 50%) assert.Equal(t, 100.0, retentions[5].RetentionRate) // Vérifier que le segment 8 (80-90%) a moins de rétention (seulement 2 sessions atteignent 80%) assert.Less(t, retentions[8].RetentionRate, 100.0) } func TestPlaybackRetentionService_IdentifyExitPoints(t *testing.T) { _, service := setupTestPlaybackRetentionServiceDB(t) // Créer des analytics avec différents points de sortie analytics := []models.PlaybackAnalytics{ {PlayTime: 45, CompletionRate: 25.0}, // Sortie à 25% {PlayTime: 45, CompletionRate: 25.0}, // Sortie à 25% {PlayTime: 90, CompletionRate: 50.0}, // Sortie à 50% {PlayTime: 135, CompletionRate: 75.0}, // Sortie à 75% {PlayTime: 180, CompletionRate: 100.0}, // Complétion à 100% } exitPoints := service.identifyExitPoints(analytics, 10) assert.NotNil(t, exitPoints) assert.Greater(t, len(exitPoints), 0) assert.LessOrEqual(t, len(exitPoints), 5) // Maximum 5 points de sortie // Vérifier que les points de sortie sont triés par taux de sortie décroissant for i := 0; i < len(exitPoints)-1; i++ { assert.GreaterOrEqual(t, exitPoints[i].ExitRate, exitPoints[i+1].ExitRate) } } func TestPlaybackRetentionService_AnalyzeEngagement(t *testing.T) { _, service := setupTestPlaybackRetentionServiceDB(t) // Créer des analytics avec différents niveaux d'engagement analytics := []models.PlaybackAnalytics{ {PlayTime: 18, CompletionRate: 10.0, PauseCount: 5, SeekCount: 3}, // Faible engagement (<25%) {PlayTime: 90, CompletionRate: 50.0, PauseCount: 2, SeekCount: 1}, // Engagement moyen {PlayTime: 135, CompletionRate: 75.0, PauseCount: 1, SeekCount: 0}, // Engagement élevé (>=75%) {PlayTime: 180, CompletionRate: 100.0, PauseCount: 0, SeekCount: 0}, // Engagement très élevé (>=75%) } metrics := service.analyzeEngagement(analytics) assert.NotNil(t, metrics) assert.InDelta(t, 58.75, metrics.AverageCompletion, 0.1) // (10 + 50 + 75 + 100) / 4 = 58.75 assert.InDelta(t, 58.75, metrics.OverallRetentionRate, 0.1) // Même valeur que AverageCompletion assert.Equal(t, 50.0, metrics.HighEngagementRate) // 2 sessions sur 4 avec >=75% assert.Equal(t, 25.0, metrics.LowEngagementRate) // 1 session sur 4 avec <25% (10% < 25%) assert.Equal(t, 2.0, metrics.AveragePauses) // (5 + 2 + 1 + 0) / 4 assert.Equal(t, 1.0, metrics.AverageSeeks) // (3 + 1 + 0 + 0) / 4 assert.Greater(t, metrics.EngagementScore, 0.0) assert.LessOrEqual(t, metrics.EngagementScore, 100.0) } func TestPlaybackRetentionService_AnalyzeRetention_DefaultSegmentCount(t *testing.T) { db, service := setupTestPlaybackRetentionServiceDB(t) ctx := context.Background() // Créer user et track userID := uuid.New() trackID := uuid.New() user := &models.User{ID: userID, Username: "testuser", Email: "test@example.com", IsActive: true} db.Create(user) track := &models.Track{ ID: trackID, UserID: userID, Title: "Test Track", FilePath: "/test.mp3", FileSize: 1024, Format: "MP3", Duration: 180, IsPublic: true, Status: models.TrackStatusCompleted, } db.Create(track) // Utiliser 0 pour le segmentCount (devrait utiliser la valeur par défaut de 10) result, err := service.AnalyzeRetention(ctx, trackID, 0) require.NoError(t, err) assert.NotNil(t, result) assert.Len(t, result.SegmentRetentions, 10) // Valeur par défaut } func TestPlaybackRetentionService_AnalyzeRetention_MaxSegmentCount(t *testing.T) { db, service := setupTestPlaybackRetentionServiceDB(t) ctx := context.Background() // Créer user et track userID := uuid.New() trackID := uuid.New() user := &models.User{ID: userID, Username: "testuser", Email: "test@example.com", IsActive: true} db.Create(user) track := &models.Track{ ID: trackID, UserID: userID, Title: "Test Track", FilePath: "/test.mp3", FileSize: 1024, Format: "MP3", Duration: 180, IsPublic: true, Status: models.TrackStatusCompleted, } db.Create(track) // Utiliser un nombre très élevé (devrait être limité à 100) result, err := service.AnalyzeRetention(ctx, trackID, 200) require.NoError(t, err) assert.NotNil(t, result) assert.Len(t, result.SegmentRetentions, 100) // Maximum } func TestPlaybackRetentionService_AnalyzeRetention_InvalidDuration(t *testing.T) { db, service := setupTestPlaybackRetentionServiceDB(t) ctx := context.Background() // Créer user et track avec durée invalide userID := uuid.New() trackID := uuid.New() user := &models.User{ID: userID, Username: "testuser", Email: "test@example.com", IsActive: true} db.Create(user) track := &models.Track{ ID: trackID, UserID: userID, Title: "Test Track", FilePath: "/test.mp3", FileSize: 1024, Format: "MP3", Duration: 0, // Durée invalide IsPublic: true, Status: models.TrackStatusCompleted, } db.Create(track) result, err := service.AnalyzeRetention(ctx, trackID, 10) assert.Error(t, err) assert.Contains(t, err.Error(), "invalid duration") assert.Nil(t, result) } func TestPlaybackRetentionService_AnalyzeEngagement_Empty(t *testing.T) { _, service := setupTestPlaybackRetentionServiceDB(t) analytics := []models.PlaybackAnalytics{} metrics := service.analyzeEngagement(analytics) assert.Equal(t, EngagementMetrics{}, metrics) } func TestPlaybackRetentionService_CalculateSegmentRetention_AllComplete(t *testing.T) { _, service := setupTestPlaybackRetentionServiceDB(t) // Toutes les sessions complètent le track analytics := []models.PlaybackAnalytics{ {PlayTime: 180, CompletionRate: 100.0}, {PlayTime: 180, CompletionRate: 100.0}, {PlayTime: 180, CompletionRate: 100.0}, } retentions := service.calculateSegmentRetention(analytics, 180, 10) // Tous les segments devraient avoir 100% de rétention for _, retention := range retentions { assert.Equal(t, 100.0, retention.RetentionRate) } } func TestPlaybackRetentionService_CalculateSegmentRetention_EarlyExits(t *testing.T) { _, service := setupTestPlaybackRetentionServiceDB(t) // Toutes les sessions sortent tôt analytics := []models.PlaybackAnalytics{ {PlayTime: 18, CompletionRate: 10.0}, // 10% de 180 {PlayTime: 18, CompletionRate: 10.0}, {PlayTime: 18, CompletionRate: 10.0}, } retentions := service.calculateSegmentRetention(analytics, 180, 10) // Le premier segment devrait avoir 100% de rétention assert.Equal(t, 100.0, retentions[0].RetentionRate) // Les segments suivants devraient avoir 0% de rétention for i := 2; i < len(retentions); i++ { assert.Equal(t, 0.0, retentions[i].RetentionRate) } } func TestPlaybackRetentionService_IdentifyExitPoints_MultipleExits(t *testing.T) { _, service := setupTestPlaybackRetentionServiceDB(t) // Créer des analytics avec plusieurs sorties au même point analytics := []models.PlaybackAnalytics{ {PlayTime: 45, CompletionRate: 25.0}, // 3 sorties à 25% {PlayTime: 45, CompletionRate: 25.0}, {PlayTime: 45, CompletionRate: 25.0}, {PlayTime: 90, CompletionRate: 50.0}, // 1 sortie à 50% {PlayTime: 180, CompletionRate: 100.0}, // 1 complétion } exitPoints := service.identifyExitPoints(analytics, 10) assert.NotNil(t, exitPoints) // Le point de sortie à 25% devrait être le premier (plus de sorties) if len(exitPoints) > 0 { assert.GreaterOrEqual(t, exitPoints[0].ExitCount, int64(3)) } } func TestPlaybackRetentionService_AnalyzeEngagement_HighEngagement(t *testing.T) { _, service := setupTestPlaybackRetentionServiceDB(t) // Toutes les sessions ont un engagement élevé analytics := []models.PlaybackAnalytics{ {PlayTime: 180, CompletionRate: 100.0, PauseCount: 0, SeekCount: 0}, {PlayTime: 180, CompletionRate: 100.0, PauseCount: 0, SeekCount: 0}, {PlayTime: 180, CompletionRate: 100.0, PauseCount: 0, SeekCount: 0}, } metrics := service.analyzeEngagement(analytics) assert.Equal(t, 100.0, metrics.AverageCompletion) assert.Equal(t, 100.0, metrics.OverallRetentionRate) assert.Equal(t, 100.0, metrics.HighEngagementRate) // Toutes >= 75% assert.Equal(t, 0.0, metrics.LowEngagementRate) // Aucune < 25% assert.Equal(t, 0.0, metrics.AveragePauses) assert.Equal(t, 0.0, metrics.AverageSeeks) assert.Greater(t, metrics.EngagementScore, 90.0) // Score élevé } func TestPlaybackRetentionService_AnalyzeEngagement_LowEngagement(t *testing.T) { _, service := setupTestPlaybackRetentionServiceDB(t) // Toutes les sessions ont un engagement faible analytics := []models.PlaybackAnalytics{ {PlayTime: 18, CompletionRate: 10.0, PauseCount: 10, SeekCount: 5}, {PlayTime: 18, CompletionRate: 10.0, PauseCount: 10, SeekCount: 5}, {PlayTime: 18, CompletionRate: 10.0, PauseCount: 10, SeekCount: 5}, } metrics := service.analyzeEngagement(analytics) assert.Equal(t, 10.0, metrics.AverageCompletion) assert.Equal(t, 0.0, metrics.HighEngagementRate) // Aucune >= 75% assert.Equal(t, 100.0, metrics.LowEngagementRate) // Toutes < 25% assert.Equal(t, 10.0, metrics.AveragePauses) assert.Equal(t, 5.0, metrics.AverageSeeks) assert.Less(t, metrics.EngagementScore, 50.0) // Score faible }