pmat 3.18.2

PMAT - Zero-config AI context generation and code quality toolkit (CLI, MCP, HTTP)
#![cfg(feature = "dap")]
//! TIMELINE-001: TimelinePlayer State Management Tests
//! Sprint 77 - RED Phase
//!
//! Tests for the TimelinePlayer struct that manages recording playback state
//! and navigation through execution snapshots.
//!
//! ## Requirements (TIMELINE-001)
//!
//! The TimelinePlayer must:
//! 1. Load Recording from .pmat file
//! 2. Track current frame position (0..snapshot_count)
//! 3. Navigate: next(), prev(), jump_to(frame)
//! 4. Playback control: play(), pause(), set_speed()
//! 5. Expose current snapshot for UI rendering
//!
//! ## Test Strategy
//!
//! These tests drive the design of TimelinePlayer through EXTREME TDD:
//! - RED Phase: All tests fail with assert!(false)
//! - GREEN Phase: Minimal implementation to pass tests
//! - REFACTOR Phase: Improve design while keeping tests green
//!
//! ## Architecture
//!
//! ```rust
//! pub struct TimelinePlayer {
//!     recording: Recording,
//!     current_frame: usize,
//!     total_frames: usize,
//!     playback_speed: f64,
//!     is_playing: bool,
//! }
//! ```

/// GREEN Test 1: Load recording from valid .pmat file
///
/// Requirement: TimelinePlayer must load a Recording from a .pmat file path
/// and initialize successfully.
///
/// Expected behavior:
/// - TimelinePlayer::new(recording) creates player
/// - Player stores the recording internally
/// - Player is ready for navigation
#[test]
fn test_load_recording_from_file() {
    use pmat::services::dap::timeline_player::TimelinePlayer;

    // Create test recording
    let recording = create_test_recording(5); // 5 snapshots

    // Create TimelinePlayer
    let player = TimelinePlayer::new(recording);
    assert_eq!(player.total_frames(), 5);
    assert_eq!(player.current_frame(), 0);
}

/// GREEN Test 2: Initialize at frame 0
///
/// Requirement: Newly created TimelinePlayer must start at frame 0
///
/// Expected behavior:
/// - current_frame() returns 0 immediately after creation
/// - current_snapshot() returns first snapshot
#[test]
fn test_initialize_at_frame_zero() {
    use pmat::services::dap::timeline_player::TimelinePlayer;

    let recording = create_test_recording(10);
    let player = TimelinePlayer::new(recording);

    assert_eq!(player.current_frame(), 0);
    assert_eq!(player.current_snapshot().frame_id, 0);
}

/// GREEN Test 3: next_frame() advances to frame 1
///
/// Requirement: next_frame() must advance current position by 1
///
/// Expected behavior:
/// - next_frame() returns Some(&Snapshot) with next frame
/// - current_frame() increments by 1
/// - Multiple calls continue advancing
#[test]
fn test_next_frame_advances() {
    use pmat::services::dap::timeline_player::TimelinePlayer;

    let recording = create_test_recording(10);
    let mut player = TimelinePlayer::new(recording);

    assert_eq!(player.current_frame(), 0);

    let snapshot = player.next_frame().unwrap();
    assert_eq!(snapshot.frame_id, 1);
    assert_eq!(player.current_frame(), 1);

    let snapshot = player.next_frame().unwrap();
    assert_eq!(snapshot.frame_id, 2);
    assert_eq!(player.current_frame(), 2);
}

/// GREEN Test 4: prev_frame() moves back to frame 0
///
/// Requirement: prev_frame() must decrement current position by 1
///
/// Expected behavior:
/// - prev_frame() returns Some(&Snapshot) with previous frame
/// - current_frame() decrements by 1
/// - At frame 0, prev_frame() returns None
#[test]
fn test_prev_frame_moves_back() {
    use pmat::services::dap::timeline_player::TimelinePlayer;

    let recording = create_test_recording(10);
    let mut player = TimelinePlayer::new(recording);

    // Advance to frame 2
    player.next_frame();
    player.next_frame();
    assert_eq!(player.current_frame(), 2);

    // Move back
    let snapshot = player.prev_frame().unwrap();
    assert_eq!(snapshot.frame_id, 1);
    assert_eq!(player.current_frame(), 1);

    // Move back to 0
    let snapshot = player.prev_frame().unwrap();
    assert_eq!(snapshot.frame_id, 0);
    assert_eq!(player.current_frame(), 0);

    // Already at 0, returns None
    assert!(player.prev_frame().is_none());
}

/// GREEN Test 5: jump_to(N) sets current frame to N
///
/// Requirement: jump_to() must allow random access to any valid frame
///
/// Expected behavior:
/// - jump_to(N) returns Ok(&Snapshot) for valid N
/// - current_frame() updates to N
/// - Works for any N in [0, total_frames)
#[test]
fn test_jump_to_valid_frame() {
    use pmat::services::dap::timeline_player::TimelinePlayer;

    let recording = create_test_recording(100);
    let mut player = TimelinePlayer::new(recording);

    // Jump to middle
    let snapshot = player.jump_to(50).unwrap();
    assert_eq!(snapshot.frame_id, 50);
    assert_eq!(player.current_frame(), 50);

    // Jump to end
    let snapshot = player.jump_to(99).unwrap();
    assert_eq!(snapshot.frame_id, 99);
    assert_eq!(player.current_frame(), 99);

    // Jump back to start
    let snapshot = player.jump_to(0).unwrap();
    assert_eq!(snapshot.frame_id, 0);
    assert_eq!(player.current_frame(), 0);
}

/// GREEN Test 6: jump_to(out_of_bounds) returns error
///
/// Requirement: jump_to() must validate frame bounds
///
/// Expected behavior:
/// - jump_to(N >= total_frames) returns Err
/// - current_frame() remains unchanged on error
/// - Error message indicates valid range
#[test]
fn test_jump_to_out_of_bounds() {
    use pmat::services::dap::timeline_player::TimelinePlayer;

    let recording = create_test_recording(10);
    let mut player = TimelinePlayer::new(recording);

    // Try to jump beyond bounds
    let result = player.jump_to(10);
    assert!(result.is_err());
    assert_eq!(player.current_frame(), 0); // Unchanged

    let result = player.jump_to(100);
    assert!(result.is_err());
    assert_eq!(player.current_frame(), 0); // Unchanged

    // Error message should be helpful
    let err = player.jump_to(15).unwrap_err();
    let msg = err.to_string();
    assert!(msg.contains("15") && msg.contains("10"));
}

/// GREEN Test 7: play() starts auto-advance
///
/// Requirement: play() must enable auto-advance mode
///
/// Expected behavior:
/// - play() sets is_playing to true
/// - is_playing() returns true after play()
/// - Player ready for timer-based advancement
#[test]
fn test_play_starts_auto_advance() {
    use pmat::services::dap::timeline_player::TimelinePlayer;

    let recording = create_test_recording(100);
    let mut player = TimelinePlayer::new(recording);

    assert!(!player.is_playing());

    player.play();
    assert!(player.is_playing());

    // Note: Actual timer-based advancement will be handled by UI layer
    // TimelinePlayer just tracks the play/pause state
}

/// GREEN Test 8: pause() stops auto-advance
///
/// Requirement: pause() must disable auto-advance mode
///
/// Expected behavior:
/// - pause() sets is_playing to false
/// - is_playing() returns false after pause()
/// - Can toggle between play and pause
#[test]
fn test_pause_stops_auto_advance() {
    use pmat::services::dap::timeline_player::TimelinePlayer;

    let recording = create_test_recording(100);
    let mut player = TimelinePlayer::new(recording);

    player.play();
    assert!(player.is_playing());

    player.pause();
    assert!(!player.is_playing());

    // Can toggle multiple times
    player.play();
    assert!(player.is_playing());
    player.pause();
    assert!(!player.is_playing());
}

/// GREEN Test 9: set_speed() changes playback rate
///
/// Requirement: set_speed() must allow adjustable playback speed
///
/// Expected behavior:
/// - set_speed(0.5) sets speed to 0.5x
/// - set_speed(2.0) sets speed to 2.0x
/// - playback_speed() returns current speed
/// - Speed affects timer interval in UI layer
#[test]
fn test_set_speed_changes_playback_rate() {
    use pmat::services::dap::timeline_player::TimelinePlayer;

    let recording = create_test_recording(100);
    let mut player = TimelinePlayer::new(recording);

    assert_eq!(player.playback_speed(), 1.0); // Default

    player.set_speed(0.5);
    assert_eq!(player.playback_speed(), 0.5);

    player.set_speed(2.0);
    assert_eq!(player.playback_speed(), 2.0);

    player.set_speed(10.0);
    assert_eq!(player.playback_speed(), 10.0);
}

/// GREEN Test 10: current_snapshot() returns correct snapshot
///
/// Requirement: current_snapshot() must expose current frame data for UI rendering
///
/// Expected behavior:
/// - current_snapshot() returns reference to current snapshot
/// - Snapshot data matches current_frame position
/// - Updates when navigation occurs
#[test]
fn test_current_snapshot_returns_correct_data() {
    use pmat::services::dap::timeline_player::TimelinePlayer;

    let recording = create_test_recording(10);
    let mut player = TimelinePlayer::new(recording);

    // Initial snapshot
    let snapshot = player.current_snapshot();
    assert_eq!(snapshot.frame_id, 0);

    // After next_frame()
    player.next_frame();
    let snapshot = player.current_snapshot();
    assert_eq!(snapshot.frame_id, 1);

    // After jump_to()
    player.jump_to(5).unwrap();
    let snapshot = player.current_snapshot();
    assert_eq!(snapshot.frame_id, 5);

    // Verify snapshot has expected data
    assert!(snapshot.variables.contains_key("test_var"));
    assert!(!snapshot.stack_frames.is_empty());
}

// ============================================================================
// Test Helpers
// ============================================================================

/// Helper: Create test recording with N snapshots
#[allow(dead_code)]
fn create_test_recording(snapshot_count: usize) -> pmat::services::dap::recording::Recording {
    use pmat::services::dap::recording::{Recording, Snapshot, StackFrame};
    use std::collections::HashMap;

    let mut recording = Recording::new("test_program".to_string(), vec!["--test".to_string()]);

    for i in 0..snapshot_count {
        let mut variables = HashMap::new();
        variables.insert("test_var".to_string(), serde_json::json!(i));
        variables.insert("counter".to_string(), serde_json::json!(i * 10));

        let stack_frames = vec![StackFrame {
            name: format!("test_function_{}", i),
            file: Some("test.rs".to_string()),
            line: Some(10 + i as u32),
            locals: HashMap::new(),
        }];

        let snapshot = Snapshot {
            frame_id: i as u64,
            timestamp_relative_ms: (i * 100) as u32,
            variables,
            stack_frames,
            instruction_pointer: 0x401000 + (i as u64 * 0x10),
            memory_snapshot: None,
        };

        recording.add_snapshot(snapshot);
    }

    recording
}