Skip to main content

jellytau_lib/player/
queue.rs

1use rand::seq::SliceRandom;
2use serde::{Deserialize, Serialize};
3
4use super::media::{MediaItem, MediaSource, QueueContext};
5
6/// Repeat mode for the queue
7///
8/// TRACES: UR-005 | DR-005
9#[derive(specta::Type, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
10#[serde(rename_all = "lowercase")]
11pub enum RepeatMode {
12    #[default]
13    Off,
14    All,
15    One,
16}
17
18/// Queue manager for playlist functionality
19///
20/// TRACES: UR-005, UR-015 | DR-005, DR-020
21#[derive(specta::Type, Debug, Clone, Serialize, Deserialize)]
22pub struct QueueManager {
23    /// All items in the queue
24    items: Vec<MediaItem>,
25    /// Current item index
26    current_index: Option<usize>,
27    /// Whether shuffle is enabled
28    shuffle: bool,
29    /// Current repeat mode
30    repeat: RepeatMode,
31    /// Shuffled order of indices (used when shuffle is on)
32    shuffle_order: Vec<usize>,
33    /// History of played indices (for going back with shuffle)
34    history: Vec<usize>,
35    /// Context for the queue (album, playlist, or custom)
36    /// Used for remote playback transfer to maintain album/playlist context
37    #[serde(default)]
38    context: QueueContext,
39}
40
41impl Default for QueueManager {
42    fn default() -> Self {
43        Self::new()
44    }
45}
46
47impl QueueManager {
48    pub fn new() -> Self {
49        Self {
50            items: Vec::new(),
51            current_index: None,
52            shuffle: false,
53            repeat: RepeatMode::Off,
54            shuffle_order: Vec::new(),
55            history: Vec::new(),
56            context: QueueContext::Custom,
57        }
58    }
59
60    /// Get all items in the queue
61    pub fn items(&self) -> &[MediaItem] {
62        &self.items
63    }
64
65    /// Mutable access to queue items.
66    ///
67    /// Used to re-point streaming entries at a completed local download
68    /// without disturbing queue order, shuffle state, or the current index.
69    pub fn items_mut(&mut self) -> &mut [MediaItem] {
70        &mut self.items
71    }
72
73    /// Get the current item index
74    pub fn current_index(&self) -> Option<usize> {
75        self.current_index
76    }
77
78    /// Get the current item
79    pub fn current(&self) -> Option<&MediaItem> {
80        self.current_index.and_then(|i| self.items.get(i))
81    }
82
83    /// Check if shuffle is enabled
84    pub fn is_shuffle(&self) -> bool {
85        self.shuffle
86    }
87
88    /// Get the current repeat mode
89    pub fn repeat_mode(&self) -> RepeatMode {
90        self.repeat
91    }
92
93    /// Get the current queue context (album, playlist, or custom)
94    pub fn context(&self) -> &QueueContext {
95        &self.context
96    }
97
98    /// Set the queue context
99    pub fn set_context(&mut self, context: QueueContext) {
100        self.context = context;
101    }
102
103    /// Set the queue with new items (resets context to Custom)
104    pub fn set_queue(&mut self, items: Vec<MediaItem>, start_index: usize) {
105        self.set_queue_with_context(items, start_index, QueueContext::Custom);
106    }
107
108    /// Set the queue with new items and explicit context
109    pub fn set_queue_with_context(
110        &mut self,
111        items: Vec<MediaItem>,
112        start_index: usize,
113        context: QueueContext,
114    ) {
115        let start_index = start_index.min(items.len().saturating_sub(1));
116
117        if self.shuffle && !items.is_empty() {
118            self.shuffle_order = self.generate_shuffle_order(items.len(), Some(start_index));
119        } else {
120            self.shuffle_order.clear();
121        }
122
123        self.items = items;
124        self.current_index = if self.items.is_empty() {
125            None
126        } else {
127            Some(start_index)
128        };
129        self.history.clear();
130        self.context = context;
131    }
132
133    /// Clear the queue entirely, returning it to the empty state.
134    ///
135    /// Used when playback genuinely stops (sleep timer fires, or the queue ends
136    /// with repeat off) so the frontend's `currentQueueItem` becomes null and
137    /// the mini player hides. History and shuffle order are reset too.
138    pub fn clear(&mut self) {
139        self.items.clear();
140        self.current_index = None;
141        self.history.clear();
142        self.shuffle_order.clear();
143    }
144
145    /// Add items to the queue
146    pub fn add(&mut self, items: Vec<MediaItem>, position: AddPosition) {
147        if items.is_empty() {
148            return;
149        }
150
151        let insert_index = match position {
152            AddPosition::Next => self
153                .current_index
154                .map(|i| i + 1)
155                .unwrap_or(self.items.len()),
156            AddPosition::End => self.items.len(),
157        };
158
159        // Insert items
160        for (i, item) in items.into_iter().enumerate() {
161            self.items.insert(insert_index + i, item);
162        }
163
164        // Update current index if needed
165        if let Some(current) = self.current_index {
166            if insert_index <= current {
167                self.current_index = Some(current + 1);
168            }
169        }
170
171        // Regenerate shuffle order if shuffle is on
172        if self.shuffle {
173            self.shuffle_order = self.generate_shuffle_order(self.items.len(), self.current_index);
174        }
175    }
176
177    /// Remove an item from the queue
178    pub fn remove(&mut self, index: usize) -> Option<MediaItem> {
179        if index >= self.items.len() {
180            return None;
181        }
182
183        let removed = self.items.remove(index);
184
185        // Update current index
186        if let Some(current) = self.current_index {
187            if index < current {
188                self.current_index = Some(current - 1);
189            } else if index == current {
190                self.current_index = if self.items.is_empty() {
191                    None
192                } else if index >= self.items.len() {
193                    Some(self.items.len() - 1)
194                } else {
195                    Some(index)
196                };
197            }
198        }
199
200        // Update shuffle order
201        if self.shuffle {
202            self.shuffle_order = self
203                .shuffle_order
204                .iter()
205                .filter(|&&i| i != index)
206                .map(|&i| if i > index { i - 1 } else { i })
207                .collect();
208        }
209
210        Some(removed)
211    }
212
213    /// Move to the next item
214    pub fn next(&mut self) -> Option<&MediaItem> {
215        if self.items.is_empty() {
216            return None;
217        }
218
219        let current = self.current_index?;
220
221        let next_index = if self.repeat == RepeatMode::One {
222            // Repeat current track
223            current
224        } else if self.shuffle {
225            // Find current position in shuffle order and get next
226            let pos = self.shuffle_order.iter().position(|&i| i == current)?;
227            if pos + 1 < self.shuffle_order.len() {
228                self.shuffle_order[pos + 1]
229            } else if self.repeat == RepeatMode::All {
230                // Wrap around to beginning of shuffle
231                self.shuffle_order[0]
232            } else {
233                log::debug!("[Queue] next() at end of shuffle order, no next track");
234                return None;
235            }
236        } else {
237            // Normal sequential order
238            if current + 1 < self.items.len() {
239                current + 1
240            } else if self.repeat == RepeatMode::All {
241                0
242            } else {
243                log::debug!(
244                    "[Queue] next() at end of queue (index {}), no next track",
245                    current
246                );
247                return None;
248            }
249        };
250
251        // Only add to history if we're actually changing position
252        if next_index != current {
253            self.history.push(current);
254            log::debug!("[Queue] next() moving: {} -> {}", current, next_index);
255        } else {
256            log::debug!("[Queue] next() repeat one, staying at index {}", current);
257        }
258
259        self.current_index = Some(next_index);
260        self.items.get(next_index)
261    }
262
263    /// Move to the previous item
264    pub fn previous(&mut self) -> Option<&MediaItem> {
265        if self.items.is_empty() {
266            return None;
267        }
268
269        let current = self.current_index?;
270
271        // If we have history, go back in history
272        // But validate it to prevent wraparound bugs
273        if let Some(prev) = self.history.pop() {
274            // Safety check: ensure the history entry is valid
275            if prev >= self.items.len() {
276                log::warn!(
277                    "[Queue] Invalid history entry {} (queue has {} items), clearing history",
278                    prev,
279                    self.items.len()
280                );
281                self.history.clear();
282                return None;
283            }
284
285            // In non-shuffle mode, previous track should be before current (or this is from a skip_to)
286            // This prevents going from first track to last track
287            if !self.shuffle && prev >= current {
288                log::warn!("[Queue] Suspicious history: going from index {} to {} (non-shuffle mode), clearing history",
289                    current, prev);
290                self.history.clear();
291                return None;
292            }
293
294            log::debug!("[Queue] previous() using history: {} -> {}", current, prev);
295            self.current_index = Some(prev);
296            return self.items.get(prev);
297        }
298
299        log::debug!("[Queue] previous() no history, current={}", current);
300
301        let prev_index = if self.shuffle {
302            // In shuffle mode without history, go to previous in shuffle order
303            let pos = self.shuffle_order.iter().position(|&i| i == current)?;
304            if pos > 0 {
305                self.shuffle_order[pos - 1]
306            } else {
307                log::debug!("[Queue] previous() at start of shuffle order, staying at current");
308                return None;
309            }
310        } else {
311            // Normal sequential order
312            if current > 0 {
313                current - 1
314            } else {
315                log::debug!("[Queue] previous() at index 0, staying at current");
316                return None;
317            }
318        };
319
320        log::debug!("[Queue] previous() moving: {} -> {}", current, prev_index);
321        self.current_index = Some(prev_index);
322        self.items.get(prev_index)
323    }
324
325    /// Skip to a specific index
326    pub fn skip_to(&mut self, index: usize) -> Option<&MediaItem> {
327        if index >= self.items.len() {
328            return None;
329        }
330
331        if let Some(current) = self.current_index {
332            self.history.push(current);
333        }
334
335        self.current_index = Some(index);
336        self.items.get(index)
337    }
338
339    /// Toggle shuffle mode
340    pub fn toggle_shuffle(&mut self) {
341        self.shuffle = !self.shuffle;
342
343        if self.shuffle && !self.items.is_empty() {
344            self.shuffle_order = self.generate_shuffle_order(self.items.len(), self.current_index);
345        } else {
346            self.shuffle_order.clear();
347        }
348    }
349
350    /// Cycle through repeat modes
351    pub fn cycle_repeat(&mut self) {
352        self.repeat = match self.repeat {
353            RepeatMode::Off => RepeatMode::All,
354            RepeatMode::All => RepeatMode::One,
355            RepeatMode::One => RepeatMode::Off,
356        };
357    }
358
359    /// Set repeat mode directly (for testing)
360    #[cfg(test)]
361    pub fn set_repeat(&mut self, mode: RepeatMode) {
362        self.repeat = mode;
363    }
364
365    /// Check if there's a next item available
366    pub fn has_next(&self) -> bool {
367        if self.items.is_empty() {
368            return false;
369        }
370
371        match self.current_index {
372            None => false,
373            Some(current) => {
374                if self.repeat == RepeatMode::All || self.repeat == RepeatMode::One {
375                    true
376                } else if self.shuffle {
377                    let pos = self.shuffle_order.iter().position(|&i| i == current);
378                    pos.map(|p| p + 1 < self.shuffle_order.len())
379                        .unwrap_or(false)
380                } else {
381                    current + 1 < self.items.len()
382                }
383            }
384        }
385    }
386
387    /// Check if there's a previous item available
388    pub fn has_previous(&self) -> bool {
389        !self.history.is_empty() || {
390            match self.current_index {
391                None => false,
392                Some(current) => {
393                    if self.shuffle {
394                        let pos = self.shuffle_order.iter().position(|&i| i == current);
395                        pos.map(|p| p > 0).unwrap_or(false)
396                    } else {
397                        current > 0
398                    }
399                }
400            }
401        }
402    }
403
404    /// Get the next N upcoming items (for preloading)
405    /// Returns items that will play after the current item, respecting shuffle order
406    pub fn get_upcoming(&self, count: usize) -> Vec<&MediaItem> {
407        if self.items.is_empty() || count == 0 {
408            return Vec::new();
409        }
410
411        let current = match self.current_index {
412            Some(idx) => idx,
413            None => return Vec::new(),
414        };
415
416        let mut upcoming = Vec::with_capacity(count);
417
418        if self.shuffle {
419            // Find current position in shuffle order
420            if let Some(pos) = self.shuffle_order.iter().position(|&i| i == current) {
421                for i in 1..=count {
422                    let next_pos = pos + i;
423                    if next_pos < self.shuffle_order.len() {
424                        if let Some(item) = self.items.get(self.shuffle_order[next_pos]) {
425                            upcoming.push(item);
426                        }
427                    } else if self.repeat == RepeatMode::All {
428                        // Wrap around if repeat all is enabled
429                        let wrapped_pos = (next_pos) % self.shuffle_order.len();
430                        if let Some(item) = self.items.get(self.shuffle_order[wrapped_pos]) {
431                            upcoming.push(item);
432                        }
433                    }
434                }
435            }
436        } else {
437            // Normal sequential order
438            for i in 1..=count {
439                let next_idx = current + i;
440                if next_idx < self.items.len() {
441                    upcoming.push(&self.items[next_idx]);
442                } else if self.repeat == RepeatMode::All {
443                    // Wrap around if repeat all is enabled
444                    let wrapped_idx = next_idx % self.items.len();
445                    upcoming.push(&self.items[wrapped_idx]);
446                }
447            }
448        }
449
450        upcoming
451    }
452
453    /// Move an item from one index to another
454    pub fn move_item(&mut self, from_index: usize, to_index: usize) -> bool {
455        if from_index >= self.items.len() || to_index >= self.items.len() || from_index == to_index
456        {
457            return false;
458        }
459
460        // Remove the item and insert at new position
461        let item = self.items.remove(from_index);
462        self.items.insert(to_index, item);
463
464        // Update current_index if affected
465        if let Some(current) = self.current_index {
466            if current == from_index {
467                // The moved item was the current one
468                self.current_index = Some(to_index);
469            } else if from_index < current && to_index >= current {
470                // Item moved from before current to after/at current
471                self.current_index = Some(current - 1);
472            } else if from_index > current && to_index <= current {
473                // Item moved from after current to before/at current
474                self.current_index = Some(current + 1);
475            }
476        }
477
478        // Update shuffle order if shuffle is on
479        if self.shuffle && !self.shuffle_order.is_empty() {
480            // Regenerate shuffle order to maintain consistency
481            self.shuffle_order = self.generate_shuffle_order(self.items.len(), self.current_index);
482        }
483
484        true
485    }
486
487    /// Update the stream URL of the current item (for transcoded seeking)
488    /// Returns true if the update was successful
489    pub fn update_current_stream_url(&mut self, new_url: String) -> bool {
490        if let Some(current_index) = self.current_index {
491            if let Some(item) = self.items.get_mut(current_index) {
492                // Only update if it's a Remote source
493                if let MediaSource::Remote {
494                    jellyfin_item_id, ..
495                } = &item.source
496                {
497                    item.source = MediaSource::Remote {
498                        stream_url: new_url,
499                        jellyfin_item_id: jellyfin_item_id.clone(),
500                    };
501                    return true;
502                }
503            }
504        }
505        false
506    }
507
508    /// Generate a shuffle order, optionally starting from a specific index
509    fn generate_shuffle_order(&self, length: usize, start_index: Option<usize>) -> Vec<usize> {
510        let mut indices: Vec<usize> = (0..length).collect();
511        let mut rng = rand::thread_rng();
512        indices.shuffle(&mut rng);
513
514        // Move start index to the front if specified
515        if let Some(start) = start_index {
516            if let Some(pos) = indices.iter().position(|&i| i == start) {
517                indices.remove(pos);
518                indices.insert(0, start);
519            }
520        }
521
522        indices
523    }
524}
525
526/// Position to add items to the queue
527#[derive(Debug, Clone, Copy, PartialEq, Eq)]
528pub enum AddPosition {
529    /// Add immediately after current item
530    Next,
531    /// Add at the end of the queue
532    End,
533}
534
535// TRACES: UR-005, UR-015 | DR-005 | UT-003, UT-004, UT-005
536#[cfg(test)]
537mod tests {
538    use super::*;
539    use crate::player::media::{MediaSource, MediaType};
540
541    fn create_test_items(count: usize) -> Vec<MediaItem> {
542        (0..count)
543            .map(|i| MediaItem {
544                // Audio and direct-URL items never negotiate a transport.
545                transport: None,
546                id: format!("item_{}", i),
547                title: format!("Track {}", i + 1),
548                name: Some(format!("Track {}", i + 1)),
549                artist: Some("Artist".to_string()),
550                album: None,
551                album_name: None,
552                album_id: None,
553                artist_items: None,
554                artists: Some(vec!["Artist".to_string()]),
555                primary_image_tag: None,
556                image_id: None,
557                item_type: Some("Audio".to_string()),
558                playlist_id: None,
559                duration: Some(180.0),
560                artwork_url: None,
561                media_type: MediaType::Audio,
562                source: MediaSource::DirectUrl {
563                    url: format!("http://example.com/track_{}.mp3", i),
564                },
565                video_codec: None,
566                needs_transcoding: false,
567                video_width: None,
568                video_height: None,
569                subtitles: vec![],
570                series_id: None,
571                server_id: None,
572            })
573            .collect()
574    }
575
576    /// Test setting up the queue with items
577    ///
578    /// @req-test: UR-015 - View and manage audio queue (add tracks)
579    /// @req-test: DR-005 - Queue manager with shuffle, repeat, history
580    #[test]
581    fn test_set_queue() {
582        let mut queue = QueueManager::new();
583        let items = create_test_items(5);
584
585        queue.set_queue(items.clone(), 0);
586
587        assert_eq!(queue.items().len(), 5);
588        assert_eq!(queue.current_index(), Some(0));
589        assert_eq!(queue.current().unwrap().id, "item_0");
590    }
591
592    /// Test clearing the queue returns it to the empty state so the frontend
593    /// hides the mini player on a genuine stop.
594    #[test]
595    fn test_clear() {
596        let mut queue = QueueManager::new();
597        queue.set_queue(create_test_items(3), 1);
598        assert_eq!(queue.current_index(), Some(1));
599
600        queue.clear();
601
602        assert_eq!(queue.items().len(), 0);
603        assert_eq!(queue.current_index(), None);
604        assert!(queue.current().is_none());
605    }
606
607    /// Test next track navigation
608    ///
609    /// @req-test: UR-005 - Control media playback (skip to next track)
610    /// @req-test: DR-005 - Queue manager with shuffle, repeat, history
611    #[test]
612    fn test_next() {
613        let mut queue = QueueManager::new();
614        queue.set_queue(create_test_items(3), 0);
615
616        assert_eq!(queue.current().unwrap().id, "item_0");
617
618        queue.next();
619        assert_eq!(queue.current().unwrap().id, "item_1");
620
621        queue.next();
622        assert_eq!(queue.current().unwrap().id, "item_2");
623
624        // No next without repeat
625        assert!(queue.next().is_none());
626    }
627
628    /// Test repeat all mode wraps to beginning
629    ///
630    /// @req-test: UR-005 - Control media playback (repeat all mode)
631    /// @req-test: DR-005 - Queue manager with repeat
632    #[test]
633    fn test_repeat_all() {
634        let mut queue = QueueManager::new();
635        queue.set_queue(create_test_items(2), 0);
636        queue.set_repeat(RepeatMode::All);
637
638        queue.next(); // Move to item_1
639        let next = queue.next(); // Should wrap to item_0
640
641        assert!(next.is_some());
642        assert_eq!(queue.current().unwrap().id, "item_0");
643    }
644
645    /// Test previous track navigation
646    ///
647    /// @req-test: UR-005 - Control media playback (previous track)
648    /// @req-test: DR-005 - Queue manager
649    #[test]
650    fn test_previous() {
651        let mut queue = QueueManager::new();
652        queue.set_queue(create_test_items(3), 2);
653
654        queue.previous();
655        assert_eq!(queue.current_index(), Some(1));
656    }
657
658    /// Test viewing upcoming tracks in queue
659    ///
660    /// @req-test: UR-015 - View and manage audio queue
661    /// @req-test: DR-020 - Queue management UI (upcoming tracks)
662    #[test]
663    fn test_get_upcoming() {
664        let mut queue = QueueManager::new();
665        queue.set_queue(create_test_items(5), 0);
666
667        // Get next 3 items
668        let upcoming = queue.get_upcoming(3);
669        assert_eq!(upcoming.len(), 3);
670        assert_eq!(upcoming[0].id, "item_1");
671        assert_eq!(upcoming[1].id, "item_2");
672        assert_eq!(upcoming[2].id, "item_3");
673
674        // Move to item 2 and get upcoming
675        queue.next();
676        queue.next();
677        let upcoming = queue.get_upcoming(3);
678        assert_eq!(upcoming.len(), 2); // Only 2 items remaining
679        assert_eq!(upcoming[0].id, "item_3");
680        assert_eq!(upcoming[1].id, "item_4");
681    }
682
683    /// Test upcoming tracks with repeat all mode
684    ///
685    /// @req-test: UR-015 - View and manage audio queue
686    /// @req-test: DR-005 - Queue manager with repeat
687    #[test]
688    fn test_get_upcoming_with_repeat() {
689        let mut queue = QueueManager::new();
690        queue.set_queue(create_test_items(3), 1);
691        queue.set_repeat(RepeatMode::All);
692
693        // At item_1, get upcoming with repeat
694        let upcoming = queue.get_upcoming(4);
695        assert_eq!(upcoming.len(), 4);
696        assert_eq!(upcoming[0].id, "item_2"); // Next
697        assert_eq!(upcoming[1].id, "item_0"); // Wrapped
698        assert_eq!(upcoming[2].id, "item_1"); // Wrapped (current again)
699        assert_eq!(upcoming[3].id, "item_2"); // Wrapped
700    }
701
702    /// Test upcoming tracks on empty queue
703    ///
704    /// @req-test: DR-005 - Queue manager (edge case: empty queue)
705    #[test]
706    fn test_get_upcoming_empty() {
707        let queue = QueueManager::new();
708        let upcoming = queue.get_upcoming(3);
709        assert!(upcoming.is_empty());
710    }
711
712    /// Test next stops at end without repeat mode
713    ///
714    /// @req-test: UR-005 - Control media playback (queue end behavior)
715    /// @req-test: DR-005 - Queue manager
716    #[test]
717    fn test_next_no_repeat_reaches_end() {
718        let mut queue = QueueManager::new();
719        queue.set_queue(create_test_items(3), 0);
720        queue.set_repeat(RepeatMode::Off);
721
722        // At item 0, should have next
723        assert!(queue.has_next());
724        assert_eq!(queue.current_index(), Some(0));
725
726        // Move to item 1
727        queue.next();
728        assert!(queue.has_next());
729        assert_eq!(queue.current_index(), Some(1));
730
731        // Move to item 2 (last)
732        queue.next();
733        assert!(!queue.has_next()); // No more items
734        assert_eq!(queue.current_index(), Some(2));
735
736        // Try to move past end
737        let result = queue.next();
738        assert!(result.is_none());
739        assert_eq!(queue.current_index(), Some(2)); // Should stay at last item
740    }
741
742    /// Test next wraps to beginning with repeat all
743    ///
744    /// @req-test: UR-005 - Control media playback (repeat all wrapping)
745    /// @req-test: DR-005 - Queue manager with repeat
746    #[test]
747    fn test_next_repeat_all_wraps() {
748        let mut queue = QueueManager::new();
749        queue.set_queue(create_test_items(3), 0);
750        queue.set_repeat(RepeatMode::All);
751
752        // With repeat all, has_next should always be true
753        assert!(queue.has_next());
754
755        // Move through all items
756        queue.next(); // item_1
757        assert!(queue.has_next());
758        queue.next(); // item_2
759        assert!(queue.has_next());
760
761        // Wrap to beginning
762        let result = queue.next();
763        assert!(result.is_some());
764        assert_eq!(queue.current().unwrap().id, "item_0");
765        assert!(queue.has_next()); // Still has next (loops forever)
766    }
767
768    /// Test next repeats same track with repeat one mode
769    ///
770    /// @req-test: UR-005 - Control media playback (repeat one mode)
771    /// @req-test: DR-005 - Queue manager with repeat
772    #[test]
773    fn test_next_repeat_one_stays() {
774        let mut queue = QueueManager::new();
775        queue.set_queue(create_test_items(3), 1);
776        queue.set_repeat(RepeatMode::One);
777
778        // Should always have next (repeats current)
779        assert!(queue.has_next());
780        assert_eq!(queue.current_index(), Some(1));
781
782        // Call next multiple times - should stay on same track
783        for _ in 0..5 {
784            let result = queue.next();
785            assert!(result.is_some());
786            assert_eq!(queue.current().unwrap().id, "item_1");
787            assert_eq!(queue.current_index(), Some(1));
788            assert!(queue.has_next());
789        }
790    }
791
792    /// Test shuffle mode follows randomized order
793    ///
794    /// @req-test: UR-005 - Control media playback (shuffle mode)
795    /// @req-test: DR-005 - Queue manager with shuffle
796    #[test]
797    fn test_next_shuffle_follows_order() {
798        let mut queue = QueueManager::new();
799        queue.set_queue(create_test_items(4), 0);
800        queue.toggle_shuffle(); // Enable shuffle
801
802        // Get the shuffle order for verification
803        let shuffle_order = queue.shuffle_order.clone();
804        assert_eq!(shuffle_order.len(), 4);
805
806        // Current should be first item in shuffle order
807        let first_shuffled_index = shuffle_order[0];
808        assert_eq!(queue.current_index(), Some(first_shuffled_index));
809
810        // Move through shuffle order
811        for &expected_index in &shuffle_order[1..] {
812            assert!(queue.has_next());
813            let result = queue.next();
814            assert!(result.is_some());
815            assert_eq!(queue.current_index(), Some(expected_index));
816        }
817
818        // At end of shuffle without repeat
819        assert!(!queue.has_next());
820        let result = queue.next();
821        assert!(result.is_none());
822    }
823
824    /// Test shuffle with repeat all wraps shuffle order
825    ///
826    /// @req-test: UR-005 - Control media playback (shuffle + repeat)
827    /// @req-test: DR-005 - Queue manager with shuffle and repeat
828    #[test]
829    fn test_next_shuffle_with_repeat_all() {
830        let mut queue = QueueManager::new();
831        queue.set_queue(create_test_items(3), 0);
832        queue.toggle_shuffle(); // Enable shuffle
833        queue.set_repeat(RepeatMode::All);
834
835        let shuffle_order = queue.shuffle_order.clone();
836
837        // Move through entire shuffle order
838        for _ in 1..shuffle_order.len() {
839            queue.next();
840        }
841
842        // At end, should wrap to beginning of shuffle order
843        assert!(queue.has_next());
844        let result = queue.next();
845        assert!(result.is_some());
846        assert_eq!(queue.current_index(), Some(shuffle_order[0]));
847    }
848
849    /// Test has_next logic accuracy across different scenarios
850    ///
851    /// @req-test: DR-005 - Queue manager (has_next accuracy)
852    /// @req-test: UR-015 - View and manage audio queue
853    #[test]
854    fn test_has_next_accuracy() {
855        // Test 1: Empty queue
856        let queue = QueueManager::new();
857        assert!(!queue.has_next());
858
859        // Test 2: Last track with repeat off
860        let mut queue = QueueManager::new();
861        queue.set_queue(create_test_items(2), 1); // Start at last item
862        queue.set_repeat(RepeatMode::Off);
863        assert!(!queue.has_next());
864
865        // Test 3: Last track with repeat all
866        let mut queue = QueueManager::new();
867        queue.set_queue(create_test_items(2), 1); // Start at last item
868        queue.set_repeat(RepeatMode::All);
869        assert!(queue.has_next()); // Should wrap
870
871        // Test 4: Repeat one mode (always has next)
872        let mut queue = QueueManager::new();
873        queue.set_queue(create_test_items(1), 0);
874        queue.set_repeat(RepeatMode::One);
875        assert!(queue.has_next()); // Repeats forever
876
877        // Test 5: Middle of queue with repeat off
878        let mut queue = QueueManager::new();
879        queue.set_queue(create_test_items(3), 1); // Middle item
880        queue.set_repeat(RepeatMode::Off);
881        assert!(queue.has_next()); // Has item_2 next
882
883        // Test 6: Shuffle at end without repeat
884        let mut queue = QueueManager::new();
885        queue.set_queue(create_test_items(3), 0);
886        queue.toggle_shuffle(); // Enable shuffle
887        queue.set_repeat(RepeatMode::Off);
888        // Move to last item in shuffle order
889        let shuffle_len = queue.shuffle_order.len();
890        for _ in 1..shuffle_len {
891            queue.next();
892        }
893        assert!(!queue.has_next());
894    }
895
896    /// Test has_next on empty queue edge case
897    ///
898    /// @req-test: DR-005 - Queue manager (edge case: empty queue)
899    #[test]
900    fn test_has_next_empty_queue() {
901        let queue = QueueManager::new();
902        assert!(!queue.has_next());
903        assert_eq!(queue.current_index(), None);
904    }
905
906    /// Test that selecting a specific track in an album starts at the correct index
907    ///
908    /// Reproduces the bug where clicking songs 1-5 always played song 13
909    #[test]
910    fn test_play_specific_track_from_album() {
911        let mut queue = QueueManager::new();
912        let items = create_test_items(14); // 14-track album
913
914        // Simulate playing track 0 (first track)
915        queue.set_queue(items.clone(), 0);
916        assert_eq!(queue.current_index(), Some(0));
917        assert_eq!(queue.current().unwrap().id, "item_0");
918
919        // Simulate playing track 3 (fourth track)
920        queue.set_queue(items.clone(), 3);
921        assert_eq!(queue.current_index(), Some(3));
922        assert_eq!(queue.current().unwrap().id, "item_3");
923
924        // Simulate playing track 13 (last track)
925        queue.set_queue(items, 13);
926        assert_eq!(queue.current_index(), Some(13));
927        assert_eq!(queue.current().unwrap().id, "item_13");
928    }
929
930    /// Test that next() then previous() returns to the original track
931    #[test]
932    fn test_next_previous_roundtrip() {
933        let mut queue = QueueManager::new();
934        queue.set_queue(create_test_items(5), 2); // Start at middle track
935
936        assert_eq!(queue.current_index(), Some(2));
937
938        // Go to next track
939        queue.next();
940        assert_eq!(queue.current_index(), Some(3));
941
942        // Go back - should return to track 2
943        queue.previous();
944        assert_eq!(queue.current_index(), Some(2));
945        assert_eq!(queue.current().unwrap().id, "item_2");
946    }
947
948    /// Test that previous() at the first track stays at first track
949    #[test]
950    fn test_previous_at_first_track_stays() {
951        let mut queue = QueueManager::new();
952        queue.set_queue(create_test_items(5), 0); // Start at first track
953
954        assert_eq!(queue.current_index(), Some(0));
955
956        // Try to go to previous - should stay at 0
957        let result = queue.previous();
958        assert!(result.is_none());
959        assert_eq!(queue.current_index(), Some(0));
960        assert_eq!(queue.current().unwrap().id, "item_0");
961    }
962
963    /// Test that next() at last track (no repeat) stays at last track
964    #[test]
965    fn test_next_at_last_track_stays() {
966        let mut queue = QueueManager::new();
967        queue.set_queue(create_test_items(5), 4); // Start at last track
968
969        assert_eq!(queue.current_index(), Some(4));
970
971        // Try to go to next - should return None and stay at 4
972        let result = queue.next();
973        assert!(result.is_none());
974        assert_eq!(queue.current_index(), Some(4));
975        assert_eq!(queue.current().unwrap().id, "item_4");
976    }
977
978    /// Test history validation prevents invalid wraparound
979    #[test]
980    fn test_history_validation_prevents_wraparound() {
981        let mut queue = QueueManager::new();
982        queue.set_queue(create_test_items(5), 0);
983
984        // Manually corrupt history to simulate the bug
985        // (In the real bug, history would have last track's index)
986        queue.history.push(4);
987
988        // Try to go previous - should detect invalid history and clear it
989        let result = queue.previous();
990        assert!(result.is_none()); // Should not wrap to track 4
991        assert_eq!(queue.current_index(), Some(0)); // Should stay at track 0
992        assert!(queue.history.is_empty()); // History should be cleared
993    }
994
995    /// Test multiple next() calls build correct history
996    #[test]
997    fn test_multiple_next_builds_history() {
998        let mut queue = QueueManager::new();
999        queue.set_queue(create_test_items(5), 0);
1000
1001        // Navigate: 0 -> 1 -> 2 -> 3
1002        queue.next(); // Now at 1, history=[0]
1003        queue.next(); // Now at 2, history=[0, 1]
1004        queue.next(); // Now at 3, history=[0, 1, 2]
1005
1006        assert_eq!(queue.current_index(), Some(3));
1007
1008        // Go back through history: 3 -> 2 -> 1 -> 0
1009        queue.previous();
1010        assert_eq!(queue.current_index(), Some(2));
1011
1012        queue.previous();
1013        assert_eq!(queue.current_index(), Some(1));
1014
1015        queue.previous();
1016        assert_eq!(queue.current_index(), Some(0));
1017    }
1018}