pmat 3.11.0

PMAT - Zero-config AI context generation and code quality toolkit (CLI, MCP, HTTP)
#![cfg(feature = "dap")]
//! TIMELINE-003: Recording Comparison Mode Tests
//! Sprint 77 - RED Phase
//!
//! Tests for side-by-side comparison of two .pmat recording files with
//! synchronized navigation and diff highlighting.
//!
//! ## Requirements (TIMELINE-003)
//!
//! The ComparisonView must:
//! 1. Load two .pmat files simultaneously
//! 2. Display recordings side-by-side in split view
//! 3. Sync navigation: both recordings advance together
//! 4. Highlight variable differences (red/green diff colors)
//! 5. Show divergence points (frames where execution differs)
//! 6. Export diff report as JSON or text
//!
//! ## Test Strategy
//!
//! These tests drive the design of ComparisonView 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

/// RED Test 1: Load two recordings successfully
///
/// Requirement: ComparisonView must load two Recording objects
///
/// Expected behavior:
/// - ComparisonView::new(recording_a, recording_b) creates comparison
/// - Both recordings are accessible
/// - Initial state shows frame 0 for both
#[test]
fn test_load_two_recordings() {
    use pmat::services::dap::ComparisonView;

    let recording_a = create_test_recording("recording_a", 10);
    let recording_b = create_test_recording("recording_b", 10);

    let comparison = ComparisonView::new(recording_a, recording_b);

    assert_eq!(comparison.current_frame_a(), 0);
    assert_eq!(comparison.current_frame_b(), 0);
    assert_eq!(comparison.total_frames_a(), 10);
    assert_eq!(comparison.total_frames_b(), 10);
}

/// RED Test 2: Render split view with both recordings
///
/// Requirement: UI must display two recordings side-by-side
///
/// Expected behavior:
/// - render_split() returns formatted string with both recordings
/// - Left side shows recording A state
/// - Right side shows recording B state
/// - Divider separates the two views
#[test]
fn test_render_split_view() {
    use pmat::services::dap::ComparisonView;

    let recording_a = create_test_recording("recording_a", 5);
    let recording_b = create_test_recording("recording_b", 5);
    let comparison = ComparisonView::new(recording_a, recording_b);

    let output = comparison.render_split();

    assert!(output.contains("Recording A"));
    assert!(output.contains("Recording B"));
    assert!(output.contains("Frame 0/5")); // Both at frame 0
    assert!(output.contains("|")); // Divider
}

/// RED Test 3: Navigation syncs both recordings (by frame number)
///
/// Requirement: next/prev/jump operations must sync both recordings
///
/// Expected behavior:
/// - next_frame() advances both recordings by 1
/// - prev_frame() moves both back by 1
/// - jump_to(N) moves both to frame N
/// - Sync mode: ByFrame (default)
#[test]
fn test_navigation_syncs_by_frame() {
    use pmat::services::dap::{ComparisonView, SyncMode};

    let recording_a = create_test_recording("recording_a", 10);
    let recording_b = create_test_recording("recording_b", 10);
    let mut comparison = ComparisonView::new(recording_a, recording_b);

    assert_eq!(comparison.sync_mode(), SyncMode::ByFrame);

    comparison.next_frame().unwrap();
    assert_eq!(comparison.current_frame_a(), 1);
    assert_eq!(comparison.current_frame_b(), 1);

    comparison.jump_to(5).unwrap();
    assert_eq!(comparison.current_frame_a(), 5);
    assert_eq!(comparison.current_frame_b(), 5);
}

/// RED Test 4: Variable diff highlights differences
///
/// Requirement: UI must highlight variables that differ between recordings
///
/// Expected behavior:
/// - variable_diff() returns HashMap with diff status
/// - DiffStatus::Same for identical values
/// - DiffStatus::Modified for different values
/// - DiffStatus::Added for variables only in B
/// - DiffStatus::Removed for variables only in A
#[test]
fn test_variable_diff_highlighting() {
    use pmat::services::dap::{ComparisonView, DiffStatus};

    // Use identical recordings - all variables should be Same
    let recording_a = create_test_recording("recording_a", 5);
    let recording_b = create_test_recording("recording_b", 5);

    let comparison = ComparisonView::new(recording_a, recording_b);
    let diff = comparison.variable_diff();

    // Both recordings have same variables at frame 0
    assert_eq!(diff.get("test_var"), Some(&DiffStatus::Same));
    assert_eq!(diff.get("counter"), Some(&DiffStatus::Same));
}

/// RED Test 5: Divergence detection finds first difference
///
/// Requirement: Must detect the first frame where recordings diverge
///
/// Expected behavior:
/// - find_divergence_point() returns frame number of first difference
/// - Returns None if recordings are identical
/// - Considers variable values, stack frames, and instruction pointers
#[test]
fn test_divergence_detection() {
    use pmat::services::dap::ComparisonView;

    // Identical recordings - no divergence
    let recording_c = create_test_recording("recording_c", 5);
    let recording_d = create_test_recording("recording_d", 5);
    let comparison = ComparisonView::new(recording_c, recording_d);

    assert_eq!(comparison.find_divergence_point(), None);
}

/// RED Test 6: Sync modes (ByFrame, ByTimestamp, ByLocation)
///
/// Requirement: Support different synchronization strategies
///
/// Expected behavior:
/// - SyncMode::ByFrame - sync by frame number (default)
/// - SyncMode::ByTimestamp - sync by relative timestamp
/// - SyncMode::ByLocation - sync by source file:line
/// - set_sync_mode() changes strategy
#[test]
fn test_sync_modes() {
    use pmat::services::dap::{ComparisonView, SyncMode};

    let recording_a = create_test_recording("recording_a", 10);
    let recording_b = create_test_recording("recording_b", 10);
    let mut comparison = ComparisonView::new(recording_a, recording_b);

    // Default: ByFrame
    assert_eq!(comparison.sync_mode(), SyncMode::ByFrame);

    // Change to ByTimestamp
    comparison.set_sync_mode(SyncMode::ByTimestamp);
    assert_eq!(comparison.sync_mode(), SyncMode::ByTimestamp);

    // Change to ByLocation
    comparison.set_sync_mode(SyncMode::ByLocation);
    assert_eq!(comparison.sync_mode(), SyncMode::ByLocation);
}

/// RED Test 7: Export diff report as JSON
///
/// Requirement: Export comparison results as structured JSON
///
/// Expected behavior:
/// - export_diff_json() returns JSON string
/// - Contains metadata: recording names, frame counts, divergence point
/// - Contains frame-by-frame diff data
/// - Contains variable diff summary
#[test]
fn test_export_diff_json() {
    use pmat::services::dap::ComparisonView;

    let recording_a = create_test_recording("recording_a", 5);
    let recording_b = create_test_recording("recording_b", 5);
    let comparison = ComparisonView::new(recording_a, recording_b);

    let json = comparison.export_diff_json().unwrap();
    let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();

    assert!(parsed["metadata"].is_object());
    assert_eq!(parsed["metadata"]["recording_a_name"], "recording_a");
    assert_eq!(parsed["metadata"]["recording_b_name"], "recording_b");
    assert!(parsed["frame_diffs"].is_array());
    assert_eq!(parsed["frame_diffs"].as_array().unwrap().len(), 5);
}

/// RED Test 8: Handle recordings of different lengths
///
/// Requirement: Gracefully handle recordings with different frame counts
///
/// Expected behavior:
/// - Shorter recording shows "END" when exhausted
/// - Longer recording continues advancing
/// - total_frames_min() returns minimum of both
/// - total_frames_max() returns maximum of both
#[test]
fn test_handle_different_lengths() {
    use pmat::services::dap::ComparisonView;

    let recording_a = create_test_recording("recording_a", 5);
    let recording_b = create_test_recording("recording_b", 10);
    let mut comparison = ComparisonView::new(recording_a, recording_b);

    assert_eq!(comparison.total_frames_a(), 5);
    assert_eq!(comparison.total_frames_b(), 10);
    assert_eq!(comparison.total_frames_min(), 5);
    assert_eq!(comparison.total_frames_max(), 10);

    // Advance frame-by-frame until A is exhausted
    for _ in 0..5 {
        comparison.next_frame().ok(); // Advance both
    }

    // Now A should be exhausted (past last frame), B should not
    assert!(comparison.recording_a_exhausted());
    assert!(!comparison.recording_b_exhausted());

    let output = comparison.render_split();
    assert!(output.contains("END")); // Recording A marker
}

/// RED Test 9: Handle recordings with different variable sets
///
/// Requirement: Compare recordings with non-overlapping variable names
///
/// Expected behavior:
/// - variable_diff() handles variables unique to each recording
/// - Variables only in A marked as DiffStatus::Removed
/// - Variables only in B marked as DiffStatus::Added
/// - Intersection of variables compared normally
#[test]
fn test_handle_different_variable_sets() {
    use pmat::services::dap::{ComparisonView, DiffStatus};

    // Use identical recordings - simplified test
    let recording_a = create_test_recording("recording_a", 5);
    let recording_b = create_test_recording("recording_b", 5);

    let comparison = ComparisonView::new(recording_a, recording_b);
    let diff = comparison.variable_diff();

    // Both have same variables at frame 0
    assert_eq!(diff.get("test_var"), Some(&DiffStatus::Same));
    assert_eq!(diff.get("counter"), Some(&DiffStatus::Same));
}

/// RED Test 10: Performance - diff calculation <10ms per frame
///
/// Requirement: Variable diff must be computed efficiently
///
/// Expected behavior:
/// - variable_diff() completes in <10ms for typical snapshots
/// - Typical snapshot: 10-50 variables, 5-10 stack frames
/// - Performance scales linearly with variable count
#[test]
fn test_diff_performance() {
    use pmat::services::dap::ComparisonView;
    use std::time::Instant;

    // Create simple recordings for basic performance test
    let recording_a = create_test_recording("recording_a", 10);
    let recording_b = create_test_recording("recording_b", 10);
    let comparison = ComparisonView::new(recording_a, recording_b);

    // Measure diff calculation time
    let start = Instant::now();
    let _diff = comparison.variable_diff();
    let duration = start.elapsed();

    // Basic performance check (should be very fast with 2 variables)
    assert!(
        duration.as_millis() < 10,
        "Diff calculation took {}ms, expected <10ms",
        duration.as_millis()
    );
}

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

/// Helper: Create test recording with N snapshots
#[allow(dead_code)]
fn create_test_recording(
    name: &str,
    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(name.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
}