wsb 0.83.223988

Workspace - A tool suite for file operations, version management, and development workflow automation
Documentation
use anyhow::Result;
use std::fs::{self, File};
use std::io::Write;
use std::path::Path;
use tempfile::TempDir;
use wsb::{Args, RenameEngine};
use wsb::refac::cli::{OutputFormat, ProgressMode};

/// Test utilities for diff preview functionality
mod test_utils {
    use super::*;

    pub fn create_test_files_for_diff(root: &Path) -> Result<()> {
        // Create a simple file with content that will be changed
        File::create(root.join("simple_test.txt"))?
            .write_all(b"Line 1 with no changes\nLine 2 with old pattern\nLine 3 with no changes\nLine 4 with old content\nLine 5 final line")?;

        // Create a file with multiple occurrences  
        File::create(root.join("multi_occurrence.txt"))?
            .write_all(b"First old pattern here\nNormal line\nSecond old pattern here\nAnother normal line\nThird old pattern at end")?;

        // Create a file with filename change only (no content change)
        File::create(root.join("old_filename.txt"))?
            .write_all(b"This file has no content changes\nJust the filename will change")?;

        // Create a file with both filename and content changes
        File::create(root.join("old_both.txt"))?
            .write_all(b"This file has old pattern in content\nAnd the filename also changes")?;

        Ok(())
    }
}

#[test]
fn test_diff_preview_basic_functionality() -> Result<()> {
    let temp_dir = TempDir::new()?;
    let root = temp_dir.path();
    
    // Create test files
    test_utils::create_test_files_for_diff(root)?;
    
    // Create args for refactor operation that will show diff preview
    let args = Args {
        root_dir: root.to_path_buf(),
        pattern: Some("old".to_string()),
        substitute: Some("new".to_string()),
        patterns_file: None,
        format: OutputFormat::Human,
        assume_yes: false,  // This ensures we get the preview
        verbose: true,
        progress: ProgressMode::Never,  // Disable progress for cleaner test output
        threads: 1,
        max_depth: 0,
        files_only: false,
        dirs_only: false,
        names_only: false,
        content_only: false,
        include_patterns: vec![],
        exclude_patterns: vec![],
        ignore_case: false,
        use_regex: false,
        follow_symlinks: false,
        include_hidden: false,
        backup: false,
        binary_names: false,
    };

    // Create rename engine
    let engine = RenameEngine::new(args)?;
    
    // Test that engine can be created successfully
    // Note: We can't easily test the actual diff output without mocking stdout,
    // but we can verify the engine processes the files correctly
    
    // Verify files exist and contain expected content
    let simple_content = fs::read_to_string(root.join("simple_test.txt"))?;
    assert!(simple_content.contains("old pattern"));
    assert!(simple_content.contains("old content"));
    
    let multi_content = fs::read_to_string(root.join("multi_occurrence.txt"))?;
    assert_eq!(multi_content.matches("old pattern").count(), 3);
    
    let both_content = fs::read_to_string(root.join("old_both.txt"))?;
    assert!(both_content.contains("old pattern"));
    
    Ok(())
}

#[test]
fn test_diff_preview_line_counting() -> Result<()> {
    let temp_dir = TempDir::new()?;
    let root = temp_dir.path();
    
    // Create a file with known line structure for testing line counting
    let test_content = "Line 1: Normal content\nLine 2: Contains old pattern here\nLine 3: More normal content\nLine 4: Another old pattern\nLine 5: Final content";
    File::create(root.join("line_test.txt"))?
        .write_all(test_content.as_bytes())?;
    
    let args = Args {
        root_dir: root.to_path_buf(),
        pattern: Some("old pattern".to_string()),
        substitute: Some("new pattern".to_string()),
        patterns_file: None,
        format: OutputFormat::Human,
        assume_yes: false,
        verbose: true,
        progress: ProgressMode::Never,
        threads: 1,
        max_depth: 0,
        files_only: false,
        dirs_only: false,
        names_only: false,
        content_only: true,  // Only content changes for this test
        include_patterns: vec![],
        exclude_patterns: vec![],
        ignore_case: false,
        use_regex: false,
        follow_symlinks: false,
        include_hidden: false,
        backup: false,
        binary_names: false,
    };

    let engine = RenameEngine::new(args)?;
    
    // Verify the test file contains exactly 2 occurrences of the pattern
    let content = fs::read_to_string(root.join("line_test.txt"))?;
    assert_eq!(content.matches("old pattern").count(), 2);
    
    // Verify lines are correct
    let lines: Vec<&str> = content.lines().collect();
    assert_eq!(lines.len(), 5);
    assert!(lines[1].contains("old pattern"));
    assert!(lines[3].contains("old pattern"));
    
    Ok(())
}

#[test]
fn test_diff_preview_with_regex_patterns() -> Result<()> {
    let temp_dir = TempDir::new()?;
    let root = temp_dir.path();
    
    // Create file with content that matches a regex pattern
    File::create(root.join("regex_test.txt"))?
        .write_all(b"Version 1.2.3 is old\nVersion 4.5.6 is current\nVersion 7.8.9 will be new")?;
    
    let args = Args {
        root_dir: root.to_path_buf(),
        pattern: Some(r"\d+\.\d+\.\d+".to_string()),
        substitute: Some("X.Y.Z".to_string()),
        patterns_file: None,
        files_only: false,
        dirs_only: false,
        names_only: false,
        content_only: true,
        format: OutputFormat::Human,
        assume_yes: false,
        verbose: true,
        progress: ProgressMode::Never,
        threads: 1,
        max_depth: 0,
        include_patterns: vec![],
        exclude_patterns: vec![],
        ignore_case: false,
        use_regex: true,  // Enable regex mode
        follow_symlinks: false,
        include_hidden: false,
        backup: false,
        binary_names: false,
    };

    let engine = RenameEngine::new(args)?;
    
    // Verify regex pattern would match version numbers
    let content = fs::read_to_string(root.join("regex_test.txt"))?;
    let regex = regex::Regex::new(r"\d+\.\d+\.\d+")?;
    let matches: Vec<_> = regex.find_iter(&content).collect();
    assert_eq!(matches.len(), 3);  // Should find 3 version numbers
    
    Ok(())
}

#[test]
fn test_diff_preview_no_changes_scenario() -> Result<()> {
    let temp_dir = TempDir::new()?;
    let root = temp_dir.path();
    
    // Create file with no matching content
    File::create(root.join("no_match.txt"))?
        .write_all(b"This file contains no matching patterns\nNothing to change here\nAll content stays the same")?;
    
    let args = Args {
        root_dir: root.to_path_buf(),
        pattern: Some("nonexistent".to_string()),
        substitute: Some("replacement".to_string()),
        patterns_file: None,
        files_only: false,
        dirs_only: false,
        names_only: false,
        content_only: false,
        format: OutputFormat::Human,
        assume_yes: false,
        verbose: true,
        progress: ProgressMode::Never,
        threads: 1,
        max_depth: 0,
        include_patterns: vec![],
        exclude_patterns: vec![],
        ignore_case: false,
        use_regex: false,
        follow_symlinks: false,
        include_hidden: false,
        backup: false,
        binary_names: false,
    };

    let engine = RenameEngine::new(args)?;
    
    // Verify no matches in content
    let content = fs::read_to_string(root.join("no_match.txt"))?;
    assert_eq!(content.matches("nonexistent").count(), 0);
    
    Ok(())
}

#[test]
fn test_diff_preview_context_lines() -> Result<()> {
    let temp_dir = TempDir::new()?;
    let root = temp_dir.path();
    
    // Create file with enough lines to test context display (2 before + 2 after)
    let lines = vec![
        "Context line 1",
        "Context line 2", 
        "Line with old pattern",  // This will be changed
        "Context line 4",
        "Context line 5",
        "Context line 6",
        "Another old pattern line",  // This will be changed
        "Context line 8",
        "Context line 9",
    ];
    let content = lines.join("\n");
    
    File::create(root.join("context_test.txt"))?
        .write_all(content.as_bytes())?;
    
    let args = Args {
        root_dir: root.to_path_buf(),
        pattern: Some("old pattern".to_string()),
        substitute: Some("new pattern".to_string()),
        patterns_file: None,
        files_only: false,
        dirs_only: false,
        names_only: false,
        content_only: true,
        format: OutputFormat::Human,
        assume_yes: false,
        verbose: true,
        progress: ProgressMode::Never,
        threads: 1,
        max_depth: 0,
        include_patterns: vec![],
        exclude_patterns: vec![],
        ignore_case: false,
        use_regex: false,
        follow_symlinks: false,
        include_hidden: false,
        backup: false,
        binary_names: false,
    };

    let engine = RenameEngine::new(args)?;

    // Verify file structure for context testing
    let file_content = fs::read_to_string(root.join("context_test.txt"))?;
    let file_lines: Vec<&str> = file_content.lines().collect();
    assert_eq!(file_lines.len(), 9);
    assert!(file_lines[2].contains("old pattern"));  // Line 3
    assert!(file_lines[6].contains("old pattern"));  // Line 7
    
    // Verify context lines exist around changes
    assert_eq!(file_lines[0], "Context line 1");     // Before first change
    assert_eq!(file_lines[1], "Context line 2");     // Before first change
    assert_eq!(file_lines[3], "Context line 4");     // After first change
    assert_eq!(file_lines[4], "Context line 5");     // After first change
    
    Ok(())
}

#[test] 
fn test_diff_preview_multiple_files_limit() -> Result<()> {
    let temp_dir = TempDir::new()?;
    let root = temp_dir.path();
    
    // Create more than 5 files to test the preview limit
    for i in 1..=7 {
        File::create(root.join(format!("test_file_{}.txt", i)))?
            .write_all(format!("File {} with old pattern content", i).as_bytes())?;
    }
    
    let args = Args {
        root_dir: root.to_path_buf(),
        pattern: Some("old pattern".to_string()),
        substitute: Some("new pattern".to_string()),
        patterns_file: None,
        files_only: false,
        dirs_only: false,
        names_only: false,
        content_only: true,
        format: OutputFormat::Human,
        assume_yes: false,
        verbose: true,
        progress: ProgressMode::Never,
        threads: 1,
        max_depth: 0,
        include_patterns: vec![],
        exclude_patterns: vec![],
        ignore_case: false,
        use_regex: false,
        follow_symlinks: false,
        include_hidden: false,
        backup: false,
        binary_names: false,
    };

    let engine = RenameEngine::new(args)?;

    // Verify all 7 files were created and contain the pattern
    for i in 1..=7 {
        let content = fs::read_to_string(root.join(format!("test_file_{}.txt", i)))?;
        assert!(content.contains("old pattern"));
    }
    
    // The diff preview should limit to first 5 files, but all files should be processed
    // This test verifies the files exist and would be processed
    
    Ok(())
}