package model import ( "strings" "testing" "github.com/bjarneo/cliamp/playlist" ) func TestFormatTrackTime(t *testing.T) { tests := []struct { secs int want string }{ {0, ""}, {-5, ""}, {1, "0:01"}, {59, "0:59"}, {60, "1:00"}, {222, "3:42"}, {3599, "59:59"}, {3600, "1:00:00"}, {3661, "1:01:01"}, {36000, "10:00:00"}, } for _, tt := range tests { if got := formatTrackTime(tt.secs); got != tt.want { t.Errorf("formatTrackTime(%d) = %q, want %q", tt.secs, got, tt.want) } } } func TestFormatPlaylistDuration(t *testing.T) { tests := []struct { secs int want string }{ {0, ""}, {-1, ""}, {45, "45s"}, {59, "59s"}, {60, "1m"}, {600, "10m"}, {3540, "59m"}, {3600, "1h"}, {3660, "1h 1m"}, {7200, "2h"}, {7320, "2h 2m"}, } for _, tt := range tests { if got := formatPlaylistDuration(tt.secs); got != tt.want { t.Errorf("formatPlaylistDuration(%d) = %q, want %q", tt.secs, got, tt.want) } } } func TestPlaylistLabel(t *testing.T) { tests := []struct { name string prefix string info playlist.PlaylistInfo want string }{ { "name only when both unknown", " ", playlist.PlaylistInfo{Name: "Mix"}, " Mix", }, { "track count only", "> ", playlist.PlaylistInfo{Name: "Mix", TrackCount: 12}, "> Mix · 12 tracks", }, { "duration only", " ", playlist.PlaylistInfo{Name: "Mix", DurationSecs: 3660}, " Mix · 1h 1m", }, { "both", " ", playlist.PlaylistInfo{Name: "Mix", TrackCount: 12, DurationSecs: 2700}, " Mix · 12 tracks · 45m", }, } for _, tt := range tests { got := playlistLabel(tt.prefix, tt.info) if got != tt.want { t.Errorf("%s: playlistLabel = %q, want %q", tt.name, got, tt.want) } } } func TestFormatTrackRow(t *testing.T) { // No duration: returns just "N. title". row := formatTrackRow(3, "Song", 0) if row != "3. Song" { t.Errorf("no-duration row = %q, want %q", row, "3. Song") } // With duration: ends with the time string. row = formatTrackRow(3, "Song", 222) if !strings.HasSuffix(row, "3:42") { t.Errorf("with-duration row %q does not end with %q", row, "3:42") } if !strings.HasPrefix(row, "3. Song") { t.Errorf("with-duration row %q does not start with %q", row, "3. Song") } } func TestHeaderStateIncremental(t *testing.T) { mk := func(album string) playlist.Track { return playlist.Track{Album: album} } tests := []struct { name string batches [][]playlist.Track wantHeaders bool wantTracks int wantSegs int }{ { name: "empty", batches: nil, wantHeaders: false, }, { name: "single track is below cohesion threshold", batches: [][]playlist.Track{ {mk("Aja")}, }, wantHeaders: false, wantTracks: 1, wantSegs: 1, }, { name: "full album in one shot is cohesive", batches: [][]playlist.Track{ {mk("Aja"), mk("Aja"), mk("Aja"), mk("Aja")}, }, wantHeaders: true, wantTracks: 4, wantSegs: 1, }, { name: "full album split across batches stays cohesive", batches: [][]playlist.Track{ {mk("Aja"), mk("Aja")}, {mk("Aja"), mk("Aja")}, }, wantHeaders: true, wantTracks: 4, wantSegs: 1, }, { name: "mixtape across batches is not cohesive", batches: [][]playlist.Track{ {mk("A"), mk("B")}, {mk("C"), mk("D")}, }, wantHeaders: false, wantTracks: 4, wantSegs: 4, }, { name: "two albums of 3 tracks each meet threshold", batches: [][]playlist.Track{ {mk("X"), mk("X"), mk("X")}, {mk("Y"), mk("Y"), mk("Y")}, }, wantHeaders: true, wantTracks: 6, wantSegs: 2, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { m := &Model{} m.setHeaderStateFromTracks(nil) // reset counters for _, batch := range tt.batches { m.addToHeaderState(batch) } if m.showAlbumHeaders != tt.wantHeaders { t.Errorf("showAlbumHeaders = %v, want %v", m.showAlbumHeaders, tt.wantHeaders) } if m.headerTracks != tt.wantTracks { t.Errorf("headerTracks = %d, want %d", m.headerTracks, tt.wantTracks) } if m.headerSegments != tt.wantSegs { t.Errorf("headerSegments = %d, want %d", m.headerSegments, tt.wantSegs) } }) } } func TestHeaderStateManualOverride(t *testing.T) { mk := func(album string) playlist.Track { return playlist.Track{Album: album} } m := &Model{} // Start with a cohesive album so the heuristic would prefer headers. m.setHeaderStateFromTracks([]playlist.Track{mk("A"), mk("A"), mk("A"), mk("A")}) if !m.showAlbumHeaders { t.Fatalf("baseline cohesive album should default to showing headers") } // User manually toggles off. m.toggleAlbumHeadersManual() if m.showAlbumHeaders { t.Fatalf("after manual toggle showAlbumHeaders should be false") } // Adding more cohesive tracks must NOT flip back on. m.addToHeaderState([]playlist.Track{mk("A"), mk("A"), mk("A")}) if m.showAlbumHeaders { t.Fatalf("manual override should suppress heuristic after Add") } // A fresh load via setHeaderStateFromTracks clears the manual flag. m.setHeaderStateFromTracks([]playlist.Track{mk("B"), mk("B"), mk("B"), mk("B")}) if !m.showAlbumHeaders { t.Fatalf("setHeaderStateFromTracks should clear manual flag and re-run heuristic") } } func TestProviderKeyForShortcut(t *testing.T) { tests := map[string]string{ "S": "spotify", "N": "navidrome", "P": "plex", "J": "jellyfin", "Y": "yt", "L": "local", "R": "radio", "x": "", "": "", } for in, want := range tests { if got := providerKeyForShortcut(in); got != want { t.Errorf("providerKeyForShortcut(%q) = %q, want %q", in, got, want) } } }