package services import ( "context" "testing" "time" "veza-backend-api/internal/models" "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gorm.io/driver/sqlite" "gorm.io/gorm" ) // TestSearch_ArtistZeroPlays_Discoverable verifies that an artist with 0 plays // appears in search results when searched by name. // TASK-ETH-002: ORIGIN_FEATURES_REGISTRY.md F375 — search must not require play count. func TestSearch_ArtistZeroPlays_Discoverable(t *testing.T) { db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) require.NoError(t, err) err = db.AutoMigrate(&models.Track{}, &models.User{}) require.NoError(t, err) service := NewTrackSearchService(db) ctx := context.Background() // Artist with 0 plays — emerging artist emergingID := uuid.New() err = db.Create(&models.User{ID: emergingID, Username: "emerging_talent", Email: "emerging@test.com", IsActive: true}).Error require.NoError(t, err) // Create track with 0 plays track := &models.Track{ UserID: emergingID, Title: "My First Song", Artist: "emerging_talent", FilePath: "/test/first.mp3", FileSize: 4 * 1024 * 1024, Format: "MP3", Duration: 200, IsPublic: true, Status: models.TrackStatusCompleted, PlayCount: 0, LikeCount: 0, } err = db.Create(track).Error require.NoError(t, err) // Search by artist name results, total, err := service.SearchTracks(ctx, TrackSearchParams{ Query: "emerging_talent", Page: 1, Limit: 10, }) require.NoError(t, err) assert.Equal(t, int64(1), total, "artist with 0 plays must appear in search results") require.Len(t, results, 1) assert.Equal(t, "My First Song", results[0].Title) } // TestSearch_ZeroPlaysTrack_NotFilteredOut verifies that tracks with 0 play count // are not filtered from search results by any implicit popularity filter. // TASK-ETH-002: no play-count minimum for searchability. func TestSearch_ZeroPlaysTrack_NotFilteredOut(t *testing.T) { db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) require.NoError(t, err) err = db.AutoMigrate(&models.Track{}, &models.User{}) require.NoError(t, err) service := NewTrackSearchService(db) ctx := context.Background() userID := uuid.New() err = db.Create(&models.User{ID: userID, Username: "testuser", Email: "t@test.com", IsActive: true}).Error require.NoError(t, err) now := time.Now() // Create tracks with varying play counts tracks := []struct { title string playCount int64 createdAt time.Time }{ {"Zero Plays Song", 0, now}, {"One Play Song", 1, now.Add(-1 * time.Hour)}, {"Popular Song", 100_000, now.Add(-2 * time.Hour)}, } for _, tc := range tracks { err = db.Create(&models.Track{ UserID: userID, Title: tc.title, Artist: "Test Artist", FilePath: "/test/" + tc.title + ".mp3", FileSize: 5 * 1024 * 1024, Format: "MP3", Duration: 180, IsPublic: true, Status: models.TrackStatusCompleted, PlayCount: tc.playCount, CreatedAt: tc.createdAt, }).Error require.NoError(t, err) } // Search for "Song" — all 3 must appear regardless of play count results, total, err := service.SearchTracks(ctx, TrackSearchParams{ Query: "Song", Page: 1, Limit: 10, }) require.NoError(t, err) assert.Equal(t, int64(3), total, "all tracks must appear regardless of play count") assert.Len(t, results, 3) // Verify all titles are present titles := make([]string, len(results)) for i, r := range results { titles[i] = r.Title } assert.Contains(t, titles, "Zero Plays Song", "0-play track must be in results") assert.Contains(t, titles, "One Play Song", "1-play track must be in results") assert.Contains(t, titles, "Popular Song", "popular track must be in results") } // TestSearch_DefaultSortIsChronological verifies that the default sort order // is created_at DESC (chronological), not by popularity or engagement. // TASK-ETH-002: ORIGIN_BUSINESS_LOGIC.md — no engagement optimization. func TestSearch_DefaultSortIsChronological(t *testing.T) { db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) require.NoError(t, err) err = db.AutoMigrate(&models.Track{}, &models.User{}) require.NoError(t, err) service := NewTrackSearchService(db) ctx := context.Background() userID := uuid.New() err = db.Create(&models.User{ID: userID, Username: "testuser", Email: "t@test.com", IsActive: true}).Error require.NoError(t, err) now := time.Now() // Create tracks: popular one is oldest, unpopular one is newest oldPopular := &models.Track{ UserID: userID, Title: "Old Popular", Artist: "Artist", FilePath: "/test/old.mp3", FileSize: 5 * 1024 * 1024, Format: "MP3", Duration: 180, IsPublic: true, Status: models.TrackStatusCompleted, PlayCount: 1_000_000, CreatedAt: now.Add(-24 * time.Hour), } err = db.Create(oldPopular).Error require.NoError(t, err) newUnpopular := &models.Track{ UserID: userID, Title: "New Unpopular", Artist: "Artist", FilePath: "/test/new.mp3", FileSize: 5 * 1024 * 1024, Format: "MP3", Duration: 180, IsPublic: true, Status: models.TrackStatusCompleted, PlayCount: 0, CreatedAt: now, } err = db.Create(newUnpopular).Error require.NoError(t, err) // Default search (no SortBy specified) — should be created_at DESC results, _, err := service.SearchTracks(ctx, TrackSearchParams{ Page: 1, Limit: 10, }) require.NoError(t, err) require.Len(t, results, 2) assert.Equal(t, "New Unpopular", results[0].Title, "default sort must be chronological (newest first), not popularity") assert.Equal(t, "Old Popular", results[1].Title) } // TestSearch_NoPopularityBias_InDefaultRanking verifies that when no sort // is specified, results are not biased by play_count or like_count. // TASK-ETH-002: ethical search — equal visibility for all artists. func TestSearch_NoPopularityBias_InDefaultRanking(t *testing.T) { db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) require.NoError(t, err) err = db.AutoMigrate(&models.Track{}, &models.User{}) require.NoError(t, err) service := NewTrackSearchService(db) ctx := context.Background() // Two different artists emergingID := uuid.New() err = db.Create(&models.User{ID: emergingID, Username: "new_artist", Email: "new@test.com", IsActive: true}).Error require.NoError(t, err) starID := uuid.New() err = db.Create(&models.User{ID: starID, Username: "mega_star", Email: "star@test.com", IsActive: true}).Error require.NoError(t, err) now := time.Now() // Emerging artist's track (newest, genre=jazz) err = db.Create(&models.Track{ UserID: emergingID, Title: "Jazz Improvisation", Artist: "new_artist", FilePath: "/test/jazz1.mp3", FileSize: 5 * 1024 * 1024, Format: "MP3", Duration: 300, Genre: "jazz", IsPublic: true, Status: models.TrackStatusCompleted, PlayCount: 0, LikeCount: 0, CreatedAt: now, }).Error require.NoError(t, err) // Star's track (older, genre=jazz) err = db.Create(&models.Track{ UserID: starID, Title: "Jazz Standards", Artist: "mega_star", FilePath: "/test/jazz2.mp3", FileSize: 5 * 1024 * 1024, Format: "MP3", Duration: 250, Genre: "jazz", IsPublic: true, Status: models.TrackStatusCompleted, PlayCount: 5_000_000, LikeCount: 200_000, CreatedAt: now.Add(-1 * time.Hour), }).Error require.NoError(t, err) // Search for "Jazz" — default sort genre := "jazz" results, _, err := service.SearchTracks(ctx, TrackSearchParams{ Genre: &genre, Page: 1, Limit: 10, }) require.NoError(t, err) require.Len(t, results, 2) // ETHICAL ASSERTION: emerging artist's newer track must come first // when using default sort (chronological), despite having 0 plays vs 5M assert.Equal(t, "Jazz Improvisation", results[0].Title, "emerging artist's track must rank first (chronological default), not star's track with 5M plays") }