use std::path::{Path, PathBuf};
use std::process::Command;

fn get_binary_path() -> PathBuf {
    let workspace_root = Path::new(env!("CARGO_MANIFEST_DIR"));
    // The target directory is at the workspace root when building a member
    let debug_path = workspace_root.join("../target/debug/fren");
    debug_path
}

fn run_fren(pattern: &str, rename: Option<&str>) -> Result<String, String> {
    let workspace_root = Path::new(env!("CARGO_MANIFEST_DIR"));
    let binary = get_binary_path();
    let test_data_dir = workspace_root.join("../test_data");
    
    let mut cmd = Command::new(&binary);
    cmd.arg(pattern);
    if let Some(r) = rename {
        cmd.arg("-t");
        cmd.arg(r);
    }
    cmd.current_dir(&test_data_dir);
    
    let output = cmd.output().map_err(|e| format!("Failed to execute fren: {} (binary: {:?})", e, binary))?;

    if !output.status.success() {
        return Err(format!(
            "fren failed with status {:?}: {}",
            output.status,
            String::from_utf8_lossy(&output.stderr)
        ));
    }

    Ok(String::from_utf8_lossy(&output.stdout).to_string())
}

fn extract_renames(output: &str) -> Vec<(String, String)> {
    let mut renames = Vec::new();
    let lines: Vec<&str> = output.lines().collect();
    let mut in_preview = false;
    for line in lines {
        let line = line.trim();
        if line.contains("Old Name") && line.contains("New Name") {
            in_preview = true;
            continue;
        }
        if line.chars().all(|c| c == '-') {
            continue;
        }
        if in_preview && (line.contains("Preview mode") || line.contains("Successfully") || line.is_empty()) {
            break;
        }
        if in_preview {
            if let Some(arrow_pos) = line.find("->") {
                let old = line[..arrow_pos].trim().to_string();
                let new = line[arrow_pos + 2..].trim().to_string();
                if !old.is_empty() && !new.is_empty() && old != "Old Name" && !old.contains("Name") {
                    renames.push((old, new));
                }
            }
        }
    }
    renames
}

#[test]
fn test_basic_name_extension_no_dot() {
    let output = run_fren("InterDisplay-Regular.ttf", Some("%N.%E")).unwrap();
    let renames = extract_renames(&output);
    assert!(!renames.is_empty());
    let (old, new) = &renames[0];
    assert_eq!(old, "InterDisplay-Regular.ttf");
    assert_eq!(new, "InterDisplay-Regular.ttf");
}

#[test]
fn test_extension_selection() {
    let output = run_fren("InterDisplay-Regular.ttf", Some("%N.%E1-2")).unwrap();
    let renames = extract_renames(&output);
    assert!(!renames.is_empty());
    let (_, new) = &renames[0];
    assert_eq!(new, "InterDisplay-Regular.tt");
}

#[test]
fn test_remove_from_end() {
    let output = run_fren("InterDisplay-Regular.ttf", Some("%N1--8.%E")).unwrap();
    let renames = extract_renames(&output);
    assert!(!renames.is_empty());
    let (_, new) = &renames[0];
    assert_eq!(new, "InterDisplay.ttf");
}

#[test]
fn test_percent_syntax_lowercase_replacement() {
    let output = run_fren("InterDisplay-Regular.ttf", Some("%L%N%R/interdisplay/inter.%E")).unwrap();
    let renames = extract_renames(&output);
    assert!(!renames.is_empty());
    let (_, new) = &renames[0];
    assert_eq!(new, "inter-regular.ttf");
}

#[test]
fn test_percent_hyphen_to_underscore() {
    let output = run_fren("file-with-dashes-1.txt", Some("%N%R/-/_.%E")).unwrap();
    let renames = extract_renames(&output);
    assert!(!renames.is_empty());
    let (_, new) = &renames[0];
    assert_eq!(new, "file_with_dashes_1.txt");
}

#[test]
fn test_counter_with_padding() {
    let output = run_fren("photo_001.jpg", Some("%N_%C3.%E")).unwrap();
    let renames = extract_renames(&output);
    assert!(!renames.is_empty());
    let (_, new) = &renames[0];
    assert!(new.contains("001"));
}

#[test]
fn test_full_filename_placeholder() {
    let output = run_fren("InterDisplay-Regular.ttf", Some("%F_backup")).unwrap();
    let renames = extract_renames(&output);
    assert!(!renames.is_empty());
    let (_, new) = &renames[0];
    assert_eq!(new, "InterDisplay-Regular.ttf_backup");
}

#[test]
fn test_list_only() {
    let output = run_fren("InterDisplay-Regular.ttf", None).unwrap();
    assert!(output.contains("Found 1 matching file"));
    assert!(output.contains("InterDisplay-Regular.ttf"));
}

#[test]
fn test_lowercase_tokens() {
    let output = run_fren("InterDisplay-Regular.ttf", Some("%n.%e")).unwrap();
    let renames = extract_renames(&output);
    assert!(!renames.is_empty());
    let (_, new) = &renames[0];
    assert_eq!(new, "InterDisplay-Regular.ttf");
}

#[test]
fn test_remove_from_end_shorthand() {
    let output = run_fren("InterDisplay-Regular.ttf", Some("%n--8.%e")).unwrap();
    let renames = extract_renames(&output);
    assert!(!renames.is_empty());
    let (_, new) = &renames[0];
    assert_eq!(new, "InterDisplay.ttf");
}

#[test]
fn test_open_ended_range() {
    let output = run_fren("InterDisplay-Regular.ttf", Some("%n14-.%e")).unwrap();
    let renames = extract_renames(&output);
    assert!(!renames.is_empty());
    let (_, new) = &renames[0];
    assert_eq!(new, "Regular.ttf");
}

#[test]
fn test_combined_range_negative() {
    let output = run_fren("InterDisplay-Regular.ttf", Some("%n3--8.%e")).unwrap();
    let renames = extract_renames(&output);
    assert!(!renames.is_empty());
    let (_, new) = &renames[0];
    assert_eq!(new, "terDisplay.ttf");
}

#[test]
fn test_uppercase_modifier() {
    let output = run_fren("photo_001.jpg", Some("%U%n.%E")).unwrap();
    let renames = extract_renames(&output);
    assert!(!renames.is_empty());
    let (_, new) = &renames[0];
    assert_eq!(new, "PHOTO_001.JPG");
}

#[test]
fn test_title_case_modifier() {
    let output = run_fren("file_with_underscores_1.txt", Some("%T%n.%e")).unwrap();
    let renames = extract_renames(&output);
    assert!(!renames.is_empty());
    let (_, new) = &renames[0];
    assert_eq!(new, "File_With_Underscores_1.Txt");
}

#[test]
fn test_parent_placeholder() {
    let output = run_fren("photo_001.jpg", Some("%P_%n.%e")).unwrap();
    let renames = extract_renames(&output);
    assert!(!renames.is_empty());
    let (_, new) = &renames[0];
    // In the test run, the parent of photo_001.jpg is test_data
    assert_eq!(new, "test_data_photo_001.jpg");
}

#[test]
fn test_date_placeholder() {
    let output = run_fren("appendix-A.md", Some("%n_%D.%e")).unwrap();
    let renames = extract_renames(&output);
    assert!(!renames.is_empty());
    let (_, new) = &renames[0];
    // Check if it matches YYYY-MM-DD format (approximate check)
    assert!(new.contains("2025-") || new.contains("2026-"));
}

#[test]
fn test_trim_modifier() {
    let output = run_fren("appendix-B.md", Some("  %n  %M.%e")).unwrap();
    let renames = extract_renames(&output);
    assert!(!renames.is_empty());
    let (_, new) = &renames[0];
    assert_eq!(new, "appendix-B.md");
}

#[test]
fn test_undo_functionality() {
    use std::time::{SystemTime, UNIX_EPOCH};
    
    let workspace_root = Path::new(env!("CARGO_MANIFEST_DIR"));
    let binary = get_binary_path();
    
    // Use a temporary directory for complete isolation
    let timestamp = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap()
        .as_nanos();
    let test_dir = workspace_root.join("../target").join(format!("test_undo_{}", timestamp));
    std::fs::create_dir_all(&test_dir).unwrap();
    
    let f1 = test_dir.join("undo_feat1.txt");
    let f2 = test_dir.join("undo_feat2.txt");
    let history_file = test_dir.join(".fren_history.json");
    
    // Ensure clean state
    let _ = std::fs::remove_file(&f1);
    let _ = std::fs::remove_file(&f2);
    let _ = std::fs::remove_file(&history_file);
    
    // Setup: create the original file
    std::fs::write(&f1, "original").unwrap();
    
    // 1. Rename a file
    let output1 = Command::new(&binary)
        .arg("undo_feat1.txt")
        .arg("-t")
        .arg("undo_feat2.txt")
        .arg("-a")
        .arg("-y")
        .current_dir(&test_dir)
        .output()
        .unwrap();
    
    if !output1.status.success() {
        eprintln!("Rename failed: {}", String::from_utf8_lossy(&output1.stderr));
        eprintln!("Rename stdout: {}", String::from_utf8_lossy(&output1.stdout));
    }
    
    // Verify it was renamed
    assert!(f2.exists(), "Target file should exist after rename");
    assert!(!f1.exists(), "Source file should not exist after rename");
    
    // 2. Undo the rename
    let output2 = Command::new(&binary)
        .arg("-u")
        .arg("-y")  // Skip confirmation
        .current_dir(&test_dir)
        .output()
        .unwrap();
    
    let stdout = String::from_utf8_lossy(&output2.stdout);
    let stderr = String::from_utf8_lossy(&output2.stderr);
    
    if !stdout.contains("Successfully reversed 1 renames") && !stdout.contains("Successfully") {
        panic!("Undo failed. Status: {:?}\nStdout: {}\nStderr: {}", 
               output2.status, stdout, stderr);
    }
    
    // Verify it was reversed
    assert!(f1.exists(), "Original file should exist after undo");
    assert!(!f2.exists(), "Renamed file should not exist after undo");
    
    // Cleanup
    let _ = std::fs::remove_dir_all(&test_dir);
}

#[test]
fn test_exclude_with_globs() {
    let workspace_root = Path::new(env!("CARGO_MANIFEST_DIR"));
    let binary = get_binary_path();
    let test_data_dir = workspace_root.join("../test_data");
    
    // Match all .log files but exclude those containing '1' or '2'
    let output = Command::new(&binary)
        .arg("old_name_*.log")
        .arg("-e")
        .arg("*_1.log")
        .arg("-e")
        .arg("*_2.log")
        .current_dir(&test_data_dir)
        .output()
        .unwrap();
    
    let stdout = String::from_utf8_lossy(&output.stdout);
    assert!(stdout.contains("Found 1 matching file"));
    assert!(stdout.contains("old_name_3.log"));
    assert!(!stdout.contains("old_name_1.log"));
    assert!(!stdout.contains("old_name_2.log"));
}

#[test]
fn test_overwrite_functionality() {
    let workspace_root = Path::new(env!("CARGO_MANIFEST_DIR"));
    let binary = get_binary_path();
    let test_data_dir = workspace_root.join("../test_data");
    
    // Create two dummy files
    let file1 = test_data_dir.join("ov_test1.tmp");
    let file2 = test_data_dir.join("ov_test2.tmp");
    std::fs::write(&file1, "file1 content").unwrap();
    std::fs::write(&file2, "file2 content").unwrap();
    
    // Try to rename file1 to file2 WITHOUT overwrite
    let output = Command::new(&binary)
        .arg("ov_test1.tmp")
        .arg("-t")
        .arg("ov_test2.tmp")
        .arg("-a")
        .arg("-y")
        .current_dir(&test_data_dir)
        .output()
        .unwrap();
    
    let stdout = String::from_utf8_lossy(&output.stdout);
    assert!(stdout.contains("Skipping"));
    assert!(stdout.contains("Target file already exists"));
    assert!(file1.exists()); // file1 should still be there
    
    // Try to rename file1 to file2 WITH overwrite
    let _output = Command::new(&binary)
        .arg("ov_test1.tmp")
        .arg("-t")
        .arg("ov_test2.tmp")
        .arg("-a")
        .arg("-y")
        .arg("-o")
        .current_dir(&test_data_dir)
        .output()
        .unwrap();
    
    assert!(!file1.exists()); // file1 should be gone
    assert!(file2.exists());  // file2 should still be there (overwritten)
    let content = std::fs::read_to_string(&file2).unwrap();
    assert_eq!(content, "file1 content");
    
    // Cleanup
    let _ = std::fs::remove_file(file2);
}

#[test]
fn test_hidden_files_matching() {
    let workspace_root = Path::new(env!("CARGO_MANIFEST_DIR"));
    let binary = get_binary_path();
    let test_data_dir = workspace_root.join("../test_data");
    
    // Create a hidden file for testing
    let hidden = test_data_dir.join(".hidden_test_file");
    std::fs::write(&hidden, "hidden").unwrap();
    
    // '*' should NOT match it
    let output = Command::new(&binary)
        .arg("*")
        .current_dir(&test_data_dir)
        .output()
        .unwrap();
    let stdout = String::from_utf8_lossy(&output.stdout);
    assert!(!stdout.contains(".hidden_test_file"));
    
    // '.*' SHOULD match it
    let output = Command::new(&binary)
        .arg(".*")
        .current_dir(&test_data_dir)
        .output()
        .unwrap();
    let stdout = String::from_utf8_lossy(&output.stdout);
    assert!(stdout.contains(".hidden_test_file"));
    
    // Cleanup
    let _ = std::fs::remove_file(hidden);
}

#[test]
fn test_multiple_modifiers_and_replacements() {
    // Input: "InterDisplay-Regular.ttf"
    // Template: "%U%n%R/-/ %M%R/ /_.%e"
    // 1. %U -> mode Uppercase
    // 2. %n -> "INTERDISPLAY-REGULAR"
    // 3. %R/-/ -> "INTERDISPLAY REGULAR"
    // 4. %M -> "INTERDISPLAY REGULAR" (trim)
    // 5. %R/ /_ -> "INTERDISPLAY_REGULAR"
    // 6. .%e -> ".TTF"
    
    let output = run_fren("InterDisplay-Regular.ttf", Some("%U%n%R/-/ %M%R/ /_.%e")).unwrap();
    let renames = extract_renames(&output);
    assert!(!renames.is_empty());
    let (_, new) = &renames[0];
    assert_eq!(new, "INTERDISPLAY_REGULAR.TTF");
}

#[test]
fn test_undo_with_conflicts() {
    use std::time::{SystemTime, UNIX_EPOCH};
    
    let workspace_root = Path::new(env!("CARGO_MANIFEST_DIR"));
    let binary = get_binary_path();
    
    // Use a temporary directory for complete isolation
    let timestamp = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap()
        .as_nanos();
    let test_dir = workspace_root.join("../target").join(format!("test_undo_conf_{}", timestamp));
    std::fs::create_dir_all(&test_dir).unwrap();
    
    let f1 = test_dir.join("undo_conf1.txt");
    let f2 = test_dir.join("undo_conf2.txt");
    let history_file = test_dir.join(".fren_history.json");
    
    // Ensure clean state
    let _ = std::fs::remove_file(&f1);
    let _ = std::fs::remove_file(&f2);
    let _ = std::fs::remove_file(&history_file);
    
    // Setup: file1.txt -> file2.txt
    std::fs::write(&f1, "original").unwrap();
    
    let output1 = Command::new(&binary)
        .arg("undo_conf1.txt")
        .arg("-t")
        .arg("undo_conf2.txt")
        .arg("-a")
        .arg("-y")
        .current_dir(&test_dir)
        .output()
        .unwrap();
    
    if !output1.status.success() {
        eprintln!("Initial rename failed: {}", String::from_utf8_lossy(&output1.stderr));
        eprintln!("Initial rename stdout: {}", String::from_utf8_lossy(&output1.stdout));
    }
    
    assert!(f2.exists(), "Target file should exist after rename");
    assert!(!f1.exists(), "Source file should not exist after rename");
    
    // Case 1: Target file is missing
    std::fs::remove_file(&f2).unwrap();
    let output2 = Command::new(&binary)
        .arg("-u")
        .current_dir(&test_dir)
        .output()
        .unwrap();
    let stdout = String::from_utf8_lossy(&output2.stdout);
    assert!(stdout.contains("File no longer exists") || stdout.contains("no longer exists"),
            "Should detect missing target file. Output: {}", stdout);
    
    // Reset: clear history and redo rename
    let _ = std::fs::remove_file(&history_file);
    std::fs::write(&f1, "original").unwrap();
    
    let output3 = Command::new(&binary)
        .arg("undo_conf1.txt")
        .arg("-t")
        .arg("undo_conf2.txt")
        .arg("-a")
        .arg("-y")
        .current_dir(&test_dir)
        .output()
        .unwrap();
    
    if !output3.status.success() {
        eprintln!("Second rename failed: {}", String::from_utf8_lossy(&output3.stderr));
        eprintln!("Second rename stdout: {}", String::from_utf8_lossy(&output3.stdout));
    }
    
    assert!(f2.exists(), "Target file should exist after second rename");
    assert!(!f1.exists(), "Source file should not exist after second rename");
        
    // Case 2: Source path is occupied by a new file
    std::fs::write(&f1, "intruder").unwrap();
    let output4 = Command::new(&binary)
        .arg("-u")
        .current_dir(&test_dir)
        .output()
        .unwrap();
    let stdout = String::from_utf8_lossy(&output4.stdout);
    assert!(stdout.contains("Original location occupied") || stdout.contains("location occupied") || stdout.contains("occupied"),
            "Should detect occupied source location. Output: {}", stdout);
    
    // Cleanup
    let _ = std::fs::remove_dir_all(&test_dir);
}

#[test]
fn test_multiple_source_parts() {
    let workspace_root = Path::new(env!("CARGO_MANIFEST_DIR"));
    let binary = get_binary_path();
    let test_data_dir = workspace_root.join("../test_data");
    
    // Simulating 'fren photo_001.jpg photo_002.jpg'
    let output = Command::new(&binary)
        .arg("photo_001.jpg")
        .arg("photo_002.jpg")
        .current_dir(&test_data_dir)
        .output()
        .unwrap();
    
    let stdout = String::from_utf8_lossy(&output.stdout);
    assert!(stdout.contains("Found 2 matching file"));
    assert!(stdout.contains("photo_001.jpg"));
    assert!(stdout.contains("photo_002.jpg"));
}

#[test]
fn test_sequential_case_modifiers() {
    // 1. Start with uppercase
    // 2. Add name
    // 3. Switch to lowercase (applies to entire result so far)
    // 4. Add extension
    let output = run_fren("InterDisplay-Regular.ttf", Some("%U%n%L.%e")).unwrap();
    let renames = extract_renames(&output);
    assert!(!renames.is_empty());
    let (_, new) = &renames[0];
    // %U%n -> "INTERDISPLAY-REGULAR"
    // %L applies lowercase to entire result -> "interdisplay-regular"
    // .%e -> ".ttf" (lowercased)
    assert_eq!(new, "interdisplay-regular.ttf");
}

#[test]
fn test_current_time_placeholder() {
    let output = run_fren("photo_001.jpg", Some("%n_%H.%e")).unwrap();
    let renames = extract_renames(&output);
    assert!(!renames.is_empty());
    let (_, new) = &renames[0];
    // Check if it matches HH-MM-SS format (approximate check for hyphens)
    assert!(new.contains("-"));
}

#[test]
fn test_file_date_time_placeholders() {
    let workspace_root = Path::new(env!("CARGO_MANIFEST_DIR"));
    let test_data_dir = workspace_root.join("../test_data");
    let file_path = test_data_dir.join("photo_001.jpg");
    
    // Ensure file exists
    if !file_path.exists() {
        std::fs::write(&file_path, "dummy").unwrap();
    }

    let output = run_fren("photo_001.jpg", Some("%n_%FD_%FH.%e")).unwrap();
    let renames = extract_renames(&output);
    assert!(!renames.is_empty());
    let (_, new) = &renames[0];
    
    // Should match YYYY-MM-DD and HH-MM-SS
    let date_re = regex::Regex::new(r"\d{4}-\d{2}-\d{2}").unwrap();
    let time_re = regex::Regex::new(r"\d{2}-\d{2}-\d{2}").unwrap();
    assert!(date_re.is_match(new), "Filename '{}' should contain a date", new);
    assert!(time_re.is_match(new), "Filename '{}' should contain a time", new);
}

#[test]
fn test_parent_directory_range() {
    let output = run_fren("photo_001.jpg", Some("%P1-4_%n.%e")).unwrap();
    let renames = extract_renames(&output);
    assert!(!renames.is_empty());
    let (_, new) = &renames[0];
    // parent is "test_data", first 4 chars is "test"
    assert!(new.starts_with("test_"));
}

#[test]
fn test_regex_replacement() {
    // Replace all digits with 'X'
    let output = run_fren("photo_001.jpg", Some("%n%X/\\d/X.%e")).unwrap();
    let renames = extract_renames(&output);
    assert!(!renames.is_empty());
    let (_, new) = &renames[0];
    assert_eq!(new, "photo_XXX.jpg");
}

#[test]
fn test_empty_name_blocking() {
    let binary = get_binary_path();
    let workspace_root = Path::new(env!("CARGO_MANIFEST_DIR"));
    let test_data_dir = workspace_root.join("../test_data");
    
    // Pattern that results in empty name (assuming file has no digits)
    // Using %X to remove everything
    let output = Command::new(&binary)
        .arg(test_data_dir.join("photo_001.jpg"))
        .arg("-t")
        .arg("%X/.//")
        .arg("-a")
        .arg("-y")
        .output()
        .unwrap();

    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(stderr.contains("ERROR: One or more files would have an empty name"));
    assert_ne!(output.status.code(), Some(0));
}

#[test]
fn test_unknown_token_warning() {
    let binary = get_binary_path();
    let workspace_root = Path::new(env!("CARGO_MANIFEST_DIR"));
    let test_data_dir = workspace_root.join("../test_data");
    
    let output = Command::new(&binary)
        .arg(test_data_dir.join("photo_001.jpg"))
        .arg("-t")
        .arg("test%Z")
        .output()
        .unwrap();

    let stdout = String::from_utf8_lossy(&output.stdout);
    assert!(stdout.contains("WARNINGS:"));
    assert!(stdout.contains("Unknown token: %Z"));
}

#[test]
fn test_overwrite_flag_help() {
    let binary = get_binary_path();
    
    let output = Command::new(&binary)
        .arg("--help")
        .output()
        .unwrap();
    let stdout = String::from_utf8_lossy(&output.stdout);
    assert!(stdout.contains("-o, --overwrite"));
    assert!(stdout.contains("-t, --to"));
    assert!(stdout.contains("-a, --apply"));
}
