use rand::seq::SliceRandom; use serde::{Deserialize, Serialize}; use super::media::{MediaItem, MediaSource, QueueContext}; /// Repeat mode for the queue /// /// TRACES: UR-005 | DR-005 #[derive(specta::Type, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] #[serde(rename_all = "lowercase")] pub enum RepeatMode { #[default] Off, All, One, } /// Queue manager for playlist functionality /// /// TRACES: UR-005, UR-015 | DR-005, DR-020 #[derive(specta::Type, Debug, Clone, Serialize, Deserialize)] pub struct QueueManager { /// All items in the queue items: Vec, /// Current item index current_index: Option, /// Whether shuffle is enabled shuffle: bool, /// Current repeat mode repeat: RepeatMode, /// Shuffled order of indices (used when shuffle is on) shuffle_order: Vec, /// History of played indices (for going back with shuffle) history: Vec, /// Context for the queue (album, playlist, or custom) /// Used for remote playback transfer to maintain album/playlist context #[serde(default)] context: QueueContext, } impl Default for QueueManager { fn default() -> Self { Self::new() } } impl QueueManager { pub fn new() -> Self { Self { items: Vec::new(), current_index: None, shuffle: false, repeat: RepeatMode::Off, shuffle_order: Vec::new(), history: Vec::new(), context: QueueContext::Custom, } } /// Get all items in the queue pub fn items(&self) -> &[MediaItem] { &self.items } /// Get the current item index pub fn current_index(&self) -> Option { self.current_index } /// Get the current item pub fn current(&self) -> Option<&MediaItem> { self.current_index.and_then(|i| self.items.get(i)) } /// Check if shuffle is enabled pub fn is_shuffle(&self) -> bool { self.shuffle } /// Get the current repeat mode pub fn repeat_mode(&self) -> RepeatMode { self.repeat } /// Get the current queue context (album, playlist, or custom) pub fn context(&self) -> &QueueContext { &self.context } /// Set the queue context pub fn set_context(&mut self, context: QueueContext) { self.context = context; } /// Set the queue with new items (resets context to Custom) pub fn set_queue(&mut self, items: Vec, start_index: usize) { self.set_queue_with_context(items, start_index, QueueContext::Custom); } /// Set the queue with new items and explicit context pub fn set_queue_with_context( &mut self, items: Vec, start_index: usize, context: QueueContext, ) { let start_index = start_index.min(items.len().saturating_sub(1)); if self.shuffle && !items.is_empty() { self.shuffle_order = self.generate_shuffle_order(items.len(), Some(start_index)); } else { self.shuffle_order.clear(); } self.items = items; self.current_index = if self.items.is_empty() { None } else { Some(start_index) }; self.history.clear(); self.context = context; } /// Add items to the queue pub fn add(&mut self, items: Vec, position: AddPosition) { if items.is_empty() { return; } let insert_index = match position { AddPosition::Next => self.current_index.map(|i| i + 1).unwrap_or(self.items.len()), AddPosition::End => self.items.len(), }; // Insert items for (i, item) in items.into_iter().enumerate() { self.items.insert(insert_index + i, item); } // Update current index if needed if let Some(current) = self.current_index { if insert_index <= current { self.current_index = Some(current + 1); } } // Regenerate shuffle order if shuffle is on if self.shuffle { self.shuffle_order = self.generate_shuffle_order( self.items.len(), self.current_index, ); } } /// Remove an item from the queue pub fn remove(&mut self, index: usize) -> Option { if index >= self.items.len() { return None; } let removed = self.items.remove(index); // Update current index if let Some(current) = self.current_index { if index < current { self.current_index = Some(current - 1); } else if index == current { self.current_index = if self.items.is_empty() { None } else if index >= self.items.len() { Some(self.items.len() - 1) } else { Some(index) }; } } // Update shuffle order if self.shuffle { self.shuffle_order = self.shuffle_order .iter() .filter(|&&i| i != index) .map(|&i| if i > index { i - 1 } else { i }) .collect(); } Some(removed) } /// Move to the next item pub fn next(&mut self) -> Option<&MediaItem> { if self.items.is_empty() { return None; } let current = self.current_index?; let next_index = if self.repeat == RepeatMode::One { // Repeat current track current } else if self.shuffle { // Find current position in shuffle order and get next let pos = self.shuffle_order.iter().position(|&i| i == current)?; if pos + 1 < self.shuffle_order.len() { self.shuffle_order[pos + 1] } else if self.repeat == RepeatMode::All { // Wrap around to beginning of shuffle self.shuffle_order[0] } else { log::debug!("[Queue] next() at end of shuffle order, no next track"); return None; } } else { // Normal sequential order if current + 1 < self.items.len() { current + 1 } else if self.repeat == RepeatMode::All { 0 } else { log::debug!("[Queue] next() at end of queue (index {}), no next track", current); return None; } }; // Only add to history if we're actually changing position if next_index != current { self.history.push(current); log::debug!("[Queue] next() moving: {} -> {}", current, next_index); } else { log::debug!("[Queue] next() repeat one, staying at index {}", current); } self.current_index = Some(next_index); self.items.get(next_index) } /// Move to the previous item pub fn previous(&mut self) -> Option<&MediaItem> { if self.items.is_empty() { return None; } let current = self.current_index?; // If we have history, go back in history // But validate it to prevent wraparound bugs if let Some(prev) = self.history.pop() { // Safety check: ensure the history entry is valid if prev >= self.items.len() { log::warn!("[Queue] Invalid history entry {} (queue has {} items), clearing history", prev, self.items.len()); self.history.clear(); return None; } // In non-shuffle mode, previous track should be before current (or this is from a skip_to) // This prevents going from first track to last track if !self.shuffle && prev >= current { log::warn!("[Queue] Suspicious history: going from index {} to {} (non-shuffle mode), clearing history", current, prev); self.history.clear(); return None; } log::debug!("[Queue] previous() using history: {} -> {}", current, prev); self.current_index = Some(prev); return self.items.get(prev); } log::debug!("[Queue] previous() no history, current={}", current); let prev_index = if self.shuffle { // In shuffle mode without history, go to previous in shuffle order let pos = self.shuffle_order.iter().position(|&i| i == current)?; if pos > 0 { self.shuffle_order[pos - 1] } else { log::debug!("[Queue] previous() at start of shuffle order, staying at current"); return None; } } else { // Normal sequential order if current > 0 { current - 1 } else { log::debug!("[Queue] previous() at index 0, staying at current"); return None; } }; log::debug!("[Queue] previous() moving: {} -> {}", current, prev_index); self.current_index = Some(prev_index); self.items.get(prev_index) } /// Skip to a specific index pub fn skip_to(&mut self, index: usize) -> Option<&MediaItem> { if index >= self.items.len() { return None; } if let Some(current) = self.current_index { self.history.push(current); } self.current_index = Some(index); self.items.get(index) } /// Toggle shuffle mode pub fn toggle_shuffle(&mut self) { self.shuffle = !self.shuffle; if self.shuffle && !self.items.is_empty() { self.shuffle_order = self.generate_shuffle_order( self.items.len(), self.current_index, ); } else { self.shuffle_order.clear(); } } /// Cycle through repeat modes pub fn cycle_repeat(&mut self) { self.repeat = match self.repeat { RepeatMode::Off => RepeatMode::All, RepeatMode::All => RepeatMode::One, RepeatMode::One => RepeatMode::Off, }; } /// Set repeat mode directly (for testing) #[cfg(test)] pub fn set_repeat(&mut self, mode: RepeatMode) { self.repeat = mode; } /// Check if there's a next item available pub fn has_next(&self) -> bool { if self.items.is_empty() { return false; } match self.current_index { None => false, Some(current) => { if self.repeat == RepeatMode::All || self.repeat == RepeatMode::One { true } else if self.shuffle { let pos = self.shuffle_order.iter().position(|&i| i == current); pos.map(|p| p + 1 < self.shuffle_order.len()).unwrap_or(false) } else { current + 1 < self.items.len() } } } } /// Check if there's a previous item available pub fn has_previous(&self) -> bool { !self.history.is_empty() || { match self.current_index { None => false, Some(current) => { if self.shuffle { let pos = self.shuffle_order.iter().position(|&i| i == current); pos.map(|p| p > 0).unwrap_or(false) } else { current > 0 } } } } } /// Get the next N upcoming items (for preloading) /// Returns items that will play after the current item, respecting shuffle order pub fn get_upcoming(&self, count: usize) -> Vec<&MediaItem> { if self.items.is_empty() || count == 0 { return Vec::new(); } let current = match self.current_index { Some(idx) => idx, None => return Vec::new(), }; let mut upcoming = Vec::with_capacity(count); if self.shuffle { // Find current position in shuffle order if let Some(pos) = self.shuffle_order.iter().position(|&i| i == current) { for i in 1..=count { let next_pos = pos + i; if next_pos < self.shuffle_order.len() { if let Some(item) = self.items.get(self.shuffle_order[next_pos]) { upcoming.push(item); } } else if self.repeat == RepeatMode::All { // Wrap around if repeat all is enabled let wrapped_pos = (next_pos) % self.shuffle_order.len(); if let Some(item) = self.items.get(self.shuffle_order[wrapped_pos]) { upcoming.push(item); } } } } } else { // Normal sequential order for i in 1..=count { let next_idx = current + i; if next_idx < self.items.len() { upcoming.push(&self.items[next_idx]); } else if self.repeat == RepeatMode::All { // Wrap around if repeat all is enabled let wrapped_idx = next_idx % self.items.len(); upcoming.push(&self.items[wrapped_idx]); } } } upcoming } /// Move an item from one index to another pub fn move_item(&mut self, from_index: usize, to_index: usize) -> bool { if from_index >= self.items.len() || to_index >= self.items.len() || from_index == to_index { return false; } // Remove the item and insert at new position let item = self.items.remove(from_index); self.items.insert(to_index, item); // Update current_index if affected if let Some(current) = self.current_index { if current == from_index { // The moved item was the current one self.current_index = Some(to_index); } else if from_index < current && to_index >= current { // Item moved from before current to after/at current self.current_index = Some(current - 1); } else if from_index > current && to_index <= current { // Item moved from after current to before/at current self.current_index = Some(current + 1); } } // Update shuffle order if shuffle is on if self.shuffle && !self.shuffle_order.is_empty() { // Regenerate shuffle order to maintain consistency self.shuffle_order = self.generate_shuffle_order(self.items.len(), self.current_index); } true } /// Update the stream URL of the current item (for transcoded seeking) /// Returns true if the update was successful pub fn update_current_stream_url(&mut self, new_url: String) -> bool { if let Some(current_index) = self.current_index { if let Some(item) = self.items.get_mut(current_index) { // Only update if it's a Remote source if let MediaSource::Remote { jellyfin_item_id, .. } = &item.source { item.source = MediaSource::Remote { stream_url: new_url, jellyfin_item_id: jellyfin_item_id.clone(), }; return true; } } } false } /// Generate a shuffle order, optionally starting from a specific index fn generate_shuffle_order(&self, length: usize, start_index: Option) -> Vec { let mut indices: Vec = (0..length).collect(); let mut rng = rand::thread_rng(); indices.shuffle(&mut rng); // Move start index to the front if specified if let Some(start) = start_index { if let Some(pos) = indices.iter().position(|&i| i == start) { indices.remove(pos); indices.insert(0, start); } } indices } } /// Position to add items to the queue #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum AddPosition { /// Add immediately after current item Next, /// Add at the end of the queue End, } // TRACES: UR-005, UR-015 | DR-005 | UT-003, UT-004, UT-005 #[cfg(test)] mod tests { use super::*; use crate::player::media::{MediaSource, MediaType}; fn create_test_items(count: usize) -> Vec { (0..count) .map(|i| MediaItem { id: format!("item_{}", i), title: format!("Track {}", i + 1), name: Some(format!("Track {}", i + 1)), artist: Some("Artist".to_string()), album: None, album_name: None, album_id: None, artist_items: None, artists: Some(vec!["Artist".to_string()]), primary_image_tag: None, item_type: Some("Audio".to_string()), playlist_id: None, duration: Some(180.0), artwork_url: None, media_type: MediaType::Audio, source: MediaSource::DirectUrl { url: format!("http://example.com/track_{}.mp3", i), }, video_codec: None, needs_transcoding: false, video_width: None, video_height: None, subtitles: vec![], series_id: None, server_id: None, }) .collect() } /// Test setting up the queue with items /// /// @req-test: UR-015 - View and manage audio queue (add tracks) /// @req-test: DR-005 - Queue manager with shuffle, repeat, history #[test] fn test_set_queue() { let mut queue = QueueManager::new(); let items = create_test_items(5); queue.set_queue(items.clone(), 0); assert_eq!(queue.items().len(), 5); assert_eq!(queue.current_index(), Some(0)); assert_eq!(queue.current().unwrap().id, "item_0"); } /// Test next track navigation /// /// @req-test: UR-005 - Control media playback (skip to next track) /// @req-test: DR-005 - Queue manager with shuffle, repeat, history #[test] fn test_next() { let mut queue = QueueManager::new(); queue.set_queue(create_test_items(3), 0); assert_eq!(queue.current().unwrap().id, "item_0"); queue.next(); assert_eq!(queue.current().unwrap().id, "item_1"); queue.next(); assert_eq!(queue.current().unwrap().id, "item_2"); // No next without repeat assert!(queue.next().is_none()); } /// Test repeat all mode wraps to beginning /// /// @req-test: UR-005 - Control media playback (repeat all mode) /// @req-test: DR-005 - Queue manager with repeat #[test] fn test_repeat_all() { let mut queue = QueueManager::new(); queue.set_queue(create_test_items(2), 0); queue.set_repeat(RepeatMode::All); queue.next(); // Move to item_1 let next = queue.next(); // Should wrap to item_0 assert!(next.is_some()); assert_eq!(queue.current().unwrap().id, "item_0"); } /// Test previous track navigation /// /// @req-test: UR-005 - Control media playback (previous track) /// @req-test: DR-005 - Queue manager #[test] fn test_previous() { let mut queue = QueueManager::new(); queue.set_queue(create_test_items(3), 2); queue.previous(); assert_eq!(queue.current_index(), Some(1)); } /// Test viewing upcoming tracks in queue /// /// @req-test: UR-015 - View and manage audio queue /// @req-test: DR-020 - Queue management UI (upcoming tracks) #[test] fn test_get_upcoming() { let mut queue = QueueManager::new(); queue.set_queue(create_test_items(5), 0); // Get next 3 items let upcoming = queue.get_upcoming(3); assert_eq!(upcoming.len(), 3); assert_eq!(upcoming[0].id, "item_1"); assert_eq!(upcoming[1].id, "item_2"); assert_eq!(upcoming[2].id, "item_3"); // Move to item 2 and get upcoming queue.next(); queue.next(); let upcoming = queue.get_upcoming(3); assert_eq!(upcoming.len(), 2); // Only 2 items remaining assert_eq!(upcoming[0].id, "item_3"); assert_eq!(upcoming[1].id, "item_4"); } /// Test upcoming tracks with repeat all mode /// /// @req-test: UR-015 - View and manage audio queue /// @req-test: DR-005 - Queue manager with repeat #[test] fn test_get_upcoming_with_repeat() { let mut queue = QueueManager::new(); queue.set_queue(create_test_items(3), 1); queue.set_repeat(RepeatMode::All); // At item_1, get upcoming with repeat let upcoming = queue.get_upcoming(4); assert_eq!(upcoming.len(), 4); assert_eq!(upcoming[0].id, "item_2"); // Next assert_eq!(upcoming[1].id, "item_0"); // Wrapped assert_eq!(upcoming[2].id, "item_1"); // Wrapped (current again) assert_eq!(upcoming[3].id, "item_2"); // Wrapped } /// Test upcoming tracks on empty queue /// /// @req-test: DR-005 - Queue manager (edge case: empty queue) #[test] fn test_get_upcoming_empty() { let queue = QueueManager::new(); let upcoming = queue.get_upcoming(3); assert!(upcoming.is_empty()); } /// Test next stops at end without repeat mode /// /// @req-test: UR-005 - Control media playback (queue end behavior) /// @req-test: DR-005 - Queue manager #[test] fn test_next_no_repeat_reaches_end() { let mut queue = QueueManager::new(); queue.set_queue(create_test_items(3), 0); queue.set_repeat(RepeatMode::Off); // At item 0, should have next assert!(queue.has_next()); assert_eq!(queue.current_index(), Some(0)); // Move to item 1 queue.next(); assert!(queue.has_next()); assert_eq!(queue.current_index(), Some(1)); // Move to item 2 (last) queue.next(); assert!(!queue.has_next()); // No more items assert_eq!(queue.current_index(), Some(2)); // Try to move past end let result = queue.next(); assert!(result.is_none()); assert_eq!(queue.current_index(), Some(2)); // Should stay at last item } /// Test next wraps to beginning with repeat all /// /// @req-test: UR-005 - Control media playback (repeat all wrapping) /// @req-test: DR-005 - Queue manager with repeat #[test] fn test_next_repeat_all_wraps() { let mut queue = QueueManager::new(); queue.set_queue(create_test_items(3), 0); queue.set_repeat(RepeatMode::All); // With repeat all, has_next should always be true assert!(queue.has_next()); // Move through all items queue.next(); // item_1 assert!(queue.has_next()); queue.next(); // item_2 assert!(queue.has_next()); // Wrap to beginning let result = queue.next(); assert!(result.is_some()); assert_eq!(queue.current().unwrap().id, "item_0"); assert!(queue.has_next()); // Still has next (loops forever) } /// Test next repeats same track with repeat one mode /// /// @req-test: UR-005 - Control media playback (repeat one mode) /// @req-test: DR-005 - Queue manager with repeat #[test] fn test_next_repeat_one_stays() { let mut queue = QueueManager::new(); queue.set_queue(create_test_items(3), 1); queue.set_repeat(RepeatMode::One); // Should always have next (repeats current) assert!(queue.has_next()); assert_eq!(queue.current_index(), Some(1)); // Call next multiple times - should stay on same track for _ in 0..5 { let result = queue.next(); assert!(result.is_some()); assert_eq!(queue.current().unwrap().id, "item_1"); assert_eq!(queue.current_index(), Some(1)); assert!(queue.has_next()); } } /// Test shuffle mode follows randomized order /// /// @req-test: UR-005 - Control media playback (shuffle mode) /// @req-test: DR-005 - Queue manager with shuffle #[test] fn test_next_shuffle_follows_order() { let mut queue = QueueManager::new(); queue.set_queue(create_test_items(4), 0); queue.toggle_shuffle(); // Enable shuffle // Get the shuffle order for verification let shuffle_order = queue.shuffle_order.clone(); assert_eq!(shuffle_order.len(), 4); // Current should be first item in shuffle order let first_shuffled_index = shuffle_order[0]; assert_eq!(queue.current_index(), Some(first_shuffled_index)); // Move through shuffle order for i in 1..shuffle_order.len() { assert!(queue.has_next()); let result = queue.next(); assert!(result.is_some()); let expected_index = shuffle_order[i]; assert_eq!(queue.current_index(), Some(expected_index)); } // At end of shuffle without repeat assert!(!queue.has_next()); let result = queue.next(); assert!(result.is_none()); } /// Test shuffle with repeat all wraps shuffle order /// /// @req-test: UR-005 - Control media playback (shuffle + repeat) /// @req-test: DR-005 - Queue manager with shuffle and repeat #[test] fn test_next_shuffle_with_repeat_all() { let mut queue = QueueManager::new(); queue.set_queue(create_test_items(3), 0); queue.toggle_shuffle(); // Enable shuffle queue.set_repeat(RepeatMode::All); let shuffle_order = queue.shuffle_order.clone(); // Move through entire shuffle order for _ in 1..shuffle_order.len() { queue.next(); } // At end, should wrap to beginning of shuffle order assert!(queue.has_next()); let result = queue.next(); assert!(result.is_some()); assert_eq!(queue.current_index(), Some(shuffle_order[0])); } /// Test has_next logic accuracy across different scenarios /// /// @req-test: DR-005 - Queue manager (has_next accuracy) /// @req-test: UR-015 - View and manage audio queue #[test] fn test_has_next_accuracy() { // Test 1: Empty queue let queue = QueueManager::new(); assert!(!queue.has_next()); // Test 2: Last track with repeat off let mut queue = QueueManager::new(); queue.set_queue(create_test_items(2), 1); // Start at last item queue.set_repeat(RepeatMode::Off); assert!(!queue.has_next()); // Test 3: Last track with repeat all let mut queue = QueueManager::new(); queue.set_queue(create_test_items(2), 1); // Start at last item queue.set_repeat(RepeatMode::All); assert!(queue.has_next()); // Should wrap // Test 4: Repeat one mode (always has next) let mut queue = QueueManager::new(); queue.set_queue(create_test_items(1), 0); queue.set_repeat(RepeatMode::One); assert!(queue.has_next()); // Repeats forever // Test 5: Middle of queue with repeat off let mut queue = QueueManager::new(); queue.set_queue(create_test_items(3), 1); // Middle item queue.set_repeat(RepeatMode::Off); assert!(queue.has_next()); // Has item_2 next // Test 6: Shuffle at end without repeat let mut queue = QueueManager::new(); queue.set_queue(create_test_items(3), 0); queue.toggle_shuffle(); // Enable shuffle queue.set_repeat(RepeatMode::Off); // Move to last item in shuffle order let shuffle_len = queue.shuffle_order.len(); for _ in 1..shuffle_len { queue.next(); } assert!(!queue.has_next()); } /// Test has_next on empty queue edge case /// /// @req-test: DR-005 - Queue manager (edge case: empty queue) #[test] fn test_has_next_empty_queue() { let queue = QueueManager::new(); assert!(!queue.has_next()); assert_eq!(queue.current_index(), None); } /// Test that selecting a specific track in an album starts at the correct index /// /// Reproduces the bug where clicking songs 1-5 always played song 13 #[test] fn test_play_specific_track_from_album() { let mut queue = QueueManager::new(); let items = create_test_items(14); // 14-track album // Simulate playing track 0 (first track) queue.set_queue(items.clone(), 0); assert_eq!(queue.current_index(), Some(0)); assert_eq!(queue.current().unwrap().id, "item_0"); // Simulate playing track 3 (fourth track) queue.set_queue(items.clone(), 3); assert_eq!(queue.current_index(), Some(3)); assert_eq!(queue.current().unwrap().id, "item_3"); // Simulate playing track 13 (last track) queue.set_queue(items, 13); assert_eq!(queue.current_index(), Some(13)); assert_eq!(queue.current().unwrap().id, "item_13"); } /// Test that next() then previous() returns to the original track #[test] fn test_next_previous_roundtrip() { let mut queue = QueueManager::new(); queue.set_queue(create_test_items(5), 2); // Start at middle track assert_eq!(queue.current_index(), Some(2)); // Go to next track queue.next(); assert_eq!(queue.current_index(), Some(3)); // Go back - should return to track 2 queue.previous(); assert_eq!(queue.current_index(), Some(2)); assert_eq!(queue.current().unwrap().id, "item_2"); } /// Test that previous() at the first track stays at first track #[test] fn test_previous_at_first_track_stays() { let mut queue = QueueManager::new(); queue.set_queue(create_test_items(5), 0); // Start at first track assert_eq!(queue.current_index(), Some(0)); // Try to go to previous - should stay at 0 let result = queue.previous(); assert!(result.is_none()); assert_eq!(queue.current_index(), Some(0)); assert_eq!(queue.current().unwrap().id, "item_0"); } /// Test that next() at last track (no repeat) stays at last track #[test] fn test_next_at_last_track_stays() { let mut queue = QueueManager::new(); queue.set_queue(create_test_items(5), 4); // Start at last track assert_eq!(queue.current_index(), Some(4)); // Try to go to next - should return None and stay at 4 let result = queue.next(); assert!(result.is_none()); assert_eq!(queue.current_index(), Some(4)); assert_eq!(queue.current().unwrap().id, "item_4"); } /// Test history validation prevents invalid wraparound #[test] fn test_history_validation_prevents_wraparound() { let mut queue = QueueManager::new(); queue.set_queue(create_test_items(5), 0); // Manually corrupt history to simulate the bug // (In the real bug, history would have last track's index) queue.history.push(4); // Try to go previous - should detect invalid history and clear it let result = queue.previous(); assert!(result.is_none()); // Should not wrap to track 4 assert_eq!(queue.current_index(), Some(0)); // Should stay at track 0 assert!(queue.history.is_empty()); // History should be cleared } /// Test multiple next() calls build correct history #[test] fn test_multiple_next_builds_history() { let mut queue = QueueManager::new(); queue.set_queue(create_test_items(5), 0); // Navigate: 0 -> 1 -> 2 -> 3 queue.next(); // Now at 1, history=[0] queue.next(); // Now at 2, history=[0, 1] queue.next(); // Now at 3, history=[0, 1, 2] assert_eq!(queue.current_index(), Some(3)); // Go back through history: 3 -> 2 -> 1 -> 0 queue.previous(); assert_eq!(queue.current_index(), Some(2)); queue.previous(); assert_eq!(queue.current_index(), Some(1)); queue.previous(); assert_eq!(queue.current_index(), Some(0)); } }