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 setupTestPlaybackHeatmapServiceDB(t *testing.T) (*gorm.DB, *PlaybackHeatmapService) { 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 := NewPlaybackHeatmapService(db, logger) return db, service } func TestNewPlaybackHeatmapService(t *testing.T) { db, _ := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) logger := zaptest.NewLogger(t) service := NewPlaybackHeatmapService(db, logger) assert.NotNil(t, service) assert.Equal(t, db, service.db) assert.NotNil(t, service.logger) } func TestNewPlaybackHeatmapService_NilLogger(t *testing.T) { db, _ := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) service := NewPlaybackHeatmapService(db, nil) assert.NotNil(t, service) assert.NotNil(t, service.logger) } func TestPlaybackHeatmapService_GenerateHeatmap_NoSessions(t *testing.T) { db, service := setupTestPlaybackHeatmapServiceDB(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) result, err := service.GenerateHeatmap(ctx, trackID, 5) require.NoError(t, err) assert.NotNil(t, result) assert.Equal(t, trackID, result.TrackID) assert.Equal(t, 180, result.TrackDuration) assert.Equal(t, 5, result.SegmentSize) assert.Equal(t, int64(0), result.TotalSessions) assert.NotNil(t, result.Segments) } func TestPlaybackHeatmapService_GenerateHeatmap_InvalidTrackID(t *testing.T) { _, service := setupTestPlaybackHeatmapServiceDB(t) ctx := context.Background() result, err := service.GenerateHeatmap(ctx, uuid.Nil, 5) assert.Error(t, err) assert.Contains(t, err.Error(), "invalid track ID") assert.Nil(t, result) } func TestPlaybackHeatmapService_GenerateHeatmap_TrackNotFound(t *testing.T) { _, service := setupTestPlaybackHeatmapServiceDB(t) ctx := context.Background() result, err := service.GenerateHeatmap(ctx, uuid.New(), 5) assert.Error(t, err) assert.Contains(t, err.Error(), "track not found") assert.Nil(t, result) } func TestPlaybackHeatmapService_GenerateHeatmap_WithSessions(t *testing.T) { db, service := setupTestPlaybackHeatmapServiceDB(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 temps de lecture 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: 180, // 100% de 180 PauseCount: 0, SeekCount: 0, CompletionRate: 100.0, StartedAt: now, CreatedAt: now, } db.Create(analytics1) db.Create(analytics2) result, err := service.GenerateHeatmap(ctx, trackID, 10) // Segments de 10 secondes require.NoError(t, err) assert.NotNil(t, result) assert.Equal(t, trackID, result.TrackID) assert.Equal(t, 180, result.TrackDuration) assert.Equal(t, 10, result.SegmentSize) assert.Equal(t, int64(2), result.TotalSessions) assert.Greater(t, len(result.Segments), 0) // Vérifier que les premiers segments ont été écoutés if len(result.Segments) > 0 { assert.Greater(t, result.Segments[0].ListenCount, int64(0)) } } func TestPlaybackHeatmapService_GenerateHeatmap_DefaultSegmentSize(t *testing.T) { db, service := setupTestPlaybackHeatmapServiceDB(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 segmentSize (devrait utiliser la valeur par défaut de 5) result, err := service.GenerateHeatmap(ctx, trackID, 0) require.NoError(t, err) assert.NotNil(t, result) assert.Equal(t, 5, result.SegmentSize) // Valeur par défaut } func TestPlaybackHeatmapService_GenerateHeatmap_MaxSegmentSize(t *testing.T) { db, service := setupTestPlaybackHeatmapServiceDB(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é à 60) result, err := service.GenerateHeatmap(ctx, trackID, 200) require.NoError(t, err) assert.NotNil(t, result) assert.Equal(t, 60, result.SegmentSize) // Maximum } func TestPlaybackHeatmapService_GenerateHeatmap_InvalidDuration(t *testing.T) { db, service := setupTestPlaybackHeatmapServiceDB(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.GenerateHeatmap(ctx, trackID, 5) assert.Error(t, err) assert.Contains(t, err.Error(), "invalid duration") assert.Nil(t, result) } func TestPlaybackHeatmapService_CalculateListenedZones(t *testing.T) { _, service := setupTestPlaybackHeatmapServiceDB(t) // Créer des analytics avec différents temps de lecture analytics := []models.PlaybackAnalytics{ {PlayTime: 30, CompletionRate: 16.67}, // 30 secondes sur 180 {PlayTime: 60, CompletionRate: 33.33}, // 60 secondes sur 180 {PlayTime: 180, CompletionRate: 100.0}, // 180 secondes (complet) } zones := service.calculateListenedZones(analytics, 180, 10) // Segments de 10 secondes assert.NotNil(t, zones) assert.Greater(t, len(zones), 0) // Vérifier que les premiers segments ont été écoutés if zones[0] != nil { assert.Greater(t, zones[0].ListenCount, int64(0)) } // Vérifier que le segment 0-10 a été écouté par toutes les sessions if zones[0] != nil { assert.Equal(t, int64(3), zones[0].ListenCount) // 3 sessions ont atteint le premier segment } } func TestPlaybackHeatmapService_CalculateSkipZones(t *testing.T) { _, service := setupTestPlaybackHeatmapServiceDB(t) // Créer des analytics avec des seeks (indiquant des skips) analytics := []models.PlaybackAnalytics{ {PlayTime: 30, SeekCount: 2, CompletionRate: 16.67}, // 2 seeks, lecture courte {PlayTime: 60, SeekCount: 1, CompletionRate: 33.33}, // 1 seek {PlayTime: 180, SeekCount: 0, CompletionRate: 100.0}, // Pas de seeks } zones := service.calculateSkipZones(analytics, 180, 10) assert.NotNil(t, zones) assert.Greater(t, len(zones), 0) } func TestPlaybackHeatmapService_GenerateHeatmapSegments(t *testing.T) { _, service := setupTestPlaybackHeatmapServiceDB(t) // Créer des zones écoutées et skip listenedZones := make(map[int]*ListenedZone) listenedZones[0] = &ListenedZone{ StartTime: 0.0, EndTime: 10.0, ListenCount: 3, TotalPlayTime: 30.0, SessionCount: 3, } listenedZones[1] = &ListenedZone{ StartTime: 10.0, EndTime: 20.0, ListenCount: 2, TotalPlayTime: 20.0, SessionCount: 2, } skipZones := make(map[int]*SkipZone) skipZones[0] = &SkipZone{ StartTime: 0.0, EndTime: 10.0, SkipCount: 0, } skipZones[1] = &SkipZone{ StartTime: 10.0, EndTime: 20.0, SkipCount: 1, } segments := service.generateHeatmapSegments(listenedZones, skipZones, 180, 10) assert.NotNil(t, segments) assert.Greater(t, len(segments), 0) // Vérifier que les segments ont des intensités calculées if len(segments) > 0 { assert.GreaterOrEqual(t, segments[0].Intensity, 0.0) assert.Greater(t, segments[0].ListenCount, int64(0)) } } func TestPlaybackHeatmapService_GetHeatmapIntensityArray(t *testing.T) { db, service := setupTestPlaybackHeatmapServiceDB(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) // Créer des analytics now := time.Now() analytics := &models.PlaybackAnalytics{ TrackID: trackID, UserID: userID, PlayTime: 90, PauseCount: 1, SeekCount: 0, CompletionRate: 50.0, StartedAt: now, CreatedAt: now, } db.Create(analytics) intensities, err := service.GetHeatmapIntensityArray(ctx, trackID, 10) require.NoError(t, err) assert.NotNil(t, intensities) assert.Greater(t, len(intensities), 0) // Vérifier que les intensités sont normalisées (0-1) for _, intensity := range intensities { assert.GreaterOrEqual(t, intensity, 0.0) assert.LessOrEqual(t, intensity, 1.0) } } func TestPlaybackHeatmapService_GenerateHeatmap_WithSkips(t *testing.T) { db, service := setupTestPlaybackHeatmapServiceDB(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) // Créer des analytics avec des seeks (skips) now := time.Now() analytics := &models.PlaybackAnalytics{ TrackID: trackID, UserID: userID, PlayTime: 60, PauseCount: 0, SeekCount: 3, // 3 seeks = skips CompletionRate: 33.33, StartedAt: now, CreatedAt: now, } db.Create(analytics) result, err := service.GenerateHeatmap(ctx, trackID, 10) require.NoError(t, err) assert.NotNil(t, result) assert.Greater(t, len(result.Segments), 0) // Vérifier qu'il y a des skips détectés (ou pas, selon le seuil) // Note: Les skips peuvent ne pas être détectés si le ratio est trop faible // C'est un comportement attendu basé sur le seuil de 0.1 _ = result.Segments // Utilisé pour vérifier la structure } func TestPlaybackHeatmapService_GenerateHeatmap_IntensityNormalization(t *testing.T) { db, service := setupTestPlaybackHeatmapServiceDB(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) // Créer plusieurs analytics pour avoir des intensités variées now := time.Now() for i := 0; i < 5; i++ { analytics := &models.PlaybackAnalytics{ TrackID: trackID, UserID: userID, PlayTime: 90 + (i * 10), PauseCount: 0, SeekCount: 0, CompletionRate: float64(50 + i*5), StartedAt: now, CreatedAt: now, } db.Create(analytics) } result, err := service.GenerateHeatmap(ctx, trackID, 10) require.NoError(t, err) assert.NotNil(t, result) // Vérifier que les intensités sont normalisées (0-1) for _, seg := range result.Segments { assert.GreaterOrEqual(t, seg.Intensity, 0.0) assert.LessOrEqual(t, seg.Intensity, 1.0) } }