//! E2E tests for gutter indicator plugins (git gutter and buffer modified)

use crate::common::git_test_helper::{DirGuard, GitTestRepo};
use crate::common::harness::EditorTestHarness;
use crossterm::event::{KeyCode, KeyModifiers};
use fresh::config::Config;

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

/// Get content lines from screen (skip menu bar, tab bar, and bottom UI elements)
/// Content lines start at row 2 (after menu bar and tab bar) and end before status bar
fn get_content_lines(screen: &str) -> Vec<&str> {
    let lines: Vec<&str> = screen.lines().collect();
    // Skip: row 0 (menu bar), row 1 (tab bar)
    // Skip: last 2 rows (status bar, prompt line)
    let content_start = 2;
    let content_end = lines.len().saturating_sub(2);

    if content_end > content_start {
        lines[content_start..content_end].to_vec()
    } else {
        vec![]
    }
}

/// Check if any content line has a gutter indicator symbol
/// Only looks at the first character of each line (the indicator column)
fn has_gutter_indicator(screen: &str, symbol: &str) -> bool {
    for line in get_content_lines(screen) {
        // The indicator column is the very first character
        // Only check the first char to avoid matching other │ characters
        if let Some(first_char) = line.chars().next() {
            if first_char.to_string() == symbol {
                return true;
            }
        }
    }
    false
}

/// Count gutter indicators on content lines
/// Only counts the first character of each line (the indicator column)
fn count_gutter_indicators(screen: &str, symbol: &str) -> usize {
    let mut count = 0;
    for line in get_content_lines(screen) {
        // The indicator column is the very first character
        if let Some(first_char) = line.chars().next() {
            if first_char.to_string() == symbol {
                count += 1;
            }
        }
    }
    count
}

/// Get the set of line numbers (0-indexed, relative to content area) that have a specific indicator
fn get_indicator_lines(screen: &str, symbol: &str) -> Vec<usize> {
    let mut lines_with_indicator = Vec::new();
    for (idx, line) in get_content_lines(screen).iter().enumerate() {
        if let Some(first_char) = line.chars().next() {
            if first_char.to_string() == symbol {
                lines_with_indicator.push(idx);
            }
        }
    }
    lines_with_indicator
}

/// Wait for a gutter indicator to appear on any line
fn wait_for_indicator(harness: &mut EditorTestHarness, symbol: &str) {
    let symbol = symbol.to_string();
    harness
        .wait_until(|h| has_gutter_indicator(&h.screen_to_string(), &symbol))
        .unwrap();
}

/// Wait for gutter indicators to disappear completely
fn wait_for_no_indicators(harness: &mut EditorTestHarness, symbol: &str) {
    let symbol = symbol.to_string();
    harness
        .wait_until(|h| !has_gutter_indicator(&h.screen_to_string(), &symbol))
        .unwrap();
}

/// Wait for a specific line to have an indicator (0-indexed relative to content area)
fn wait_for_indicator_on_line(harness: &mut EditorTestHarness, symbol: &str, line: usize) {
    let symbol = symbol.to_string();
    harness
        .wait_until(|h| {
            let screen = h.screen_to_string();
            get_indicator_lines(&screen, &symbol).contains(&line)
        })
        .unwrap();
}

/// Process async operations once (single iteration, no sleep)
fn process_async_once(harness: &mut EditorTestHarness) {
    let _ = harness.process_async_and_render();
}

/// Trigger the Git Gutter Refresh command via command palette
fn trigger_git_gutter_refresh(harness: &mut EditorTestHarness) {
    harness
        .send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
        .unwrap();
    harness.render().unwrap();
    harness.type_text("Git Gutter").unwrap();
    harness
        .send_key(KeyCode::Enter, KeyModifiers::NONE)
        .unwrap();
    harness.render().unwrap();
}

/// Open a file using the harness's open_file method
fn open_file(harness: &mut EditorTestHarness, repo_path: &std::path::Path, relative_path: &str) {
    let full_path = repo_path.join(relative_path);
    harness.open_file(&full_path).unwrap();
    // Wait for the file content to be visible on screen
    // This ensures the file is loaded and rendered
    harness
        .wait_until(|h| {
            // Check that we're no longer showing the empty scratch buffer
            let screen = h.screen_to_string();
            // The tab should show the filename
            screen.contains(relative_path)
        })
        .unwrap();
}

/// Save the current file
fn save_file(harness: &mut EditorTestHarness) {
    harness
        .send_key(KeyCode::Char('s'), KeyModifiers::CONTROL)
        .unwrap();
    // Process any resulting async operations
    process_async_once(harness);
}

// =============================================================================
// Git Gutter Tests
// =============================================================================

/// Test that git gutter shows indicators for uncommitted changes on file open
// TODO: Fix git gutter tests on Windows - they fail due to git command output differences
#[test]
#[cfg_attr(target_os = "windows", ignore)]
fn test_git_gutter_shows_on_file_open() {
    let repo = GitTestRepo::new();
    repo.setup_typical_project();
    repo.setup_git_gutter_plugin();

    // Change to repo directory so git commands work correctly
    let original_dir = repo.change_to_repo_dir();
    let _guard = DirGuard::new(original_dir);

    // Modify a file in the working copy (not staged, not committed)
    repo.modify_file(
        "src/main.rs",
        r#"fn main() {
    println!("Modified line!");
    let config = load_config();
    start_server(config);
}

fn load_config() -> Config {
    Config::default()
}

fn start_server(config: Config) {
    println!("Starting server...");
}
"#,
    );

    let mut harness = EditorTestHarness::with_config_and_working_dir(
        120,
        40,
        Config::default(),
        repo.path.clone(),
    )
    .unwrap();

    // Open the modified file
    open_file(&mut harness, &repo.path, "src/main.rs");

    // Wait for git gutter to update
    harness
        .wait_until(|h| {
            let screen = h.screen_to_string();
            // Look for the modified indicator (│) in the gutter
            has_gutter_indicator(&screen, "│")
        })
        .unwrap();

    let screen = harness.screen_to_string();
    println!("Git gutter screen:\n{}", screen);
}

/// Test that git gutter updates after saving a file
// TODO: Fix git gutter tests on Windows - they fail due to git command output differences
#[test]
#[cfg_attr(target_os = "windows", ignore)]
fn test_git_gutter_updates_after_save() {
    let repo = GitTestRepo::new();
    repo.setup_typical_project();
    repo.setup_git_gutter_plugin();

    // Change to repo directory so git commands work correctly
    let original_dir = repo.change_to_repo_dir();
    let _guard = DirGuard::new(original_dir);

    let mut harness = EditorTestHarness::with_config_and_working_dir(
        120,
        40,
        Config::default(),
        repo.path.clone(),
    )
    .unwrap();

    // Open an unmodified file
    open_file(&mut harness, &repo.path, "src/main.rs");
    harness.render().unwrap();

    // Initially, there should be no git gutter indicators (file matches HEAD)
    let screen = harness.screen_to_string();
    let initial_indicators = count_gutter_indicators(&screen, "│");

    // Make a change
    harness.type_text("// New comment\n").unwrap();
    harness.render().unwrap();

    // Save the file - this should trigger git gutter update
    save_file(&mut harness);

    // Wait for git gutter to update
    harness
        .wait_until(|h| {
            let screen = h.screen_to_string();
            // After save, there should be git indicators (file differs from HEAD)
            count_gutter_indicators(&screen, "│") > initial_indicators
        })
        .unwrap();

    let screen = harness.screen_to_string();
    println!("After save screen:\n{}", screen);
}

/// Test that git gutter shows added lines indicator
// TODO: Fix git gutter tests on Windows - they fail due to git command output differences
#[test]
#[cfg_attr(target_os = "windows", ignore)]
fn test_git_gutter_added_lines() {
    let repo = GitTestRepo::new();
    repo.setup_typical_project();
    repo.setup_git_gutter_plugin();

    // Change to repo directory so git commands work correctly
    let original_dir = repo.change_to_repo_dir();
    let _guard = DirGuard::new(original_dir);

    // Add new lines to a file
    repo.modify_file(
        "src/main.rs",
        r#"fn main() {
    println!("Hello, world!");
    let config = load_config();
    start_server(config);
}

// New function added
fn new_function() {
    println!("This is new!");
}

fn load_config() -> Config {
    Config::default()
}

fn start_server(config: Config) {
    println!("Starting server...");
}
"#,
    );

    let mut harness = EditorTestHarness::with_config_and_working_dir(
        120,
        40,
        Config::default(),
        repo.path.clone(),
    )
    .unwrap();

    open_file(&mut harness, &repo.path, "src/main.rs");

    // Wait for indicators
    harness
        .wait_until(|h| {
            let screen = h.screen_to_string();
            // Should have multiple added line indicators
            count_gutter_indicators(&screen, "│") >= 3
        })
        .unwrap();

    let screen = harness.screen_to_string();
    println!("Added lines screen:\n{}", screen);
}

/// Test that git gutter shows deleted lines indicator
// TODO: Fix git gutter tests on Windows - they fail due to git command output differences
#[test]
#[cfg_attr(target_os = "windows", ignore)]
fn test_git_gutter_deleted_lines() {
    let repo = GitTestRepo::new();
    repo.setup_typical_project();
    repo.setup_git_gutter_plugin();

    // Change to repo directory so git commands work correctly
    let original_dir = repo.change_to_repo_dir();
    let _guard = DirGuard::new(original_dir);

    // Delete some lines from a file
    repo.modify_file(
        "src/main.rs",
        r#"fn main() {
    start_server(Config::default());
}

fn start_server(config: Config) {
    println!("Starting server...");
}
"#,
    );

    let mut harness = EditorTestHarness::with_config_and_working_dir(
        120,
        40,
        Config::default(),
        repo.path.clone(),
    )
    .unwrap();

    open_file(&mut harness, &repo.path, "src/main.rs");

    // Wait for indicators - deleted lines show as ▾
    harness
        .wait_until(|h| {
            let screen = h.screen_to_string();
            has_gutter_indicator(&screen, "▾") || has_gutter_indicator(&screen, "│")
        })
        .unwrap();

    let screen = harness.screen_to_string();
    println!("Deleted lines screen:\n{}", screen);
}

/// Test git gutter with staged changes (should still show diff vs HEAD)
// TODO: Fix git gutter tests on Windows - they fail due to git command output differences
#[test]
#[cfg_attr(target_os = "windows", ignore)]
fn test_git_gutter_staged_changes() {
    let repo = GitTestRepo::new();
    repo.setup_typical_project();
    repo.setup_git_gutter_plugin();

    // Change to repo directory so git commands work correctly
    let original_dir = repo.change_to_repo_dir();
    let _guard = DirGuard::new(original_dir);

    // Modify and stage a file
    repo.modify_file(
        "src/main.rs",
        r#"fn main() {
    println!("Staged change!");
    let config = load_config();
    start_server(config);
}

fn load_config() -> Config {
    Config::default()
}

fn start_server(config: Config) {
    println!("Starting server...");
}
"#,
    );
    repo.stage_file("src/main.rs");

    let mut harness = EditorTestHarness::with_config_and_working_dir(
        120,
        40,
        Config::default(),
        repo.path.clone(),
    )
    .unwrap();

    open_file(&mut harness, &repo.path, "src/main.rs");

    // Wait for indicators - staged changes should still show vs HEAD
    harness
        .wait_until(|h| {
            let screen = h.screen_to_string();
            has_gutter_indicator(&screen, "│")
        })
        .unwrap();

    let screen = harness.screen_to_string();
    println!("Staged changes screen:\n{}", screen);
}

/// Test that git gutter clears after committing changes
// TODO: Fix git gutter tests on Windows - they fail due to git command output differences
#[test]
#[cfg_attr(target_os = "windows", ignore)]
fn test_git_gutter_clears_after_commit() {
    let repo = GitTestRepo::new();
    repo.setup_typical_project();
    repo.setup_git_gutter_plugin();

    // Change to repo directory so git commands work correctly
    let original_dir = repo.change_to_repo_dir();
    let _guard = DirGuard::new(original_dir);

    // First, create a change and commit it
    repo.modify_file(
        "src/main.rs",
        r#"fn main() {
    println!("Committed change!");
    let config = load_config();
    start_server(config);
}

fn load_config() -> Config {
    Config::default()
}

fn start_server(config: Config) {
    println!("Starting server...");
}
"#,
    );
    repo.git_add_all();
    repo.git_commit("Update main.rs");

    let mut harness = EditorTestHarness::with_config_and_working_dir(
        120,
        40,
        Config::default(),
        repo.path.clone(),
    )
    .unwrap();

    open_file(&mut harness, &repo.path, "src/main.rs");

    // Wait a bit for git gutter to process
    harness.sleep(std::time::Duration::from_millis(500));
    harness.render().unwrap();

    let screen = harness.screen_to_string();
    println!("After commit screen:\n{}", screen);

    // After commit, there should be no git indicators (file matches HEAD)
    let indicators = count_gutter_indicators(&screen, "│");
    assert_eq!(
        indicators, 0,
        "Git gutter should have no indicators after changes are committed"
    );
}

/// Test git gutter on untracked file (should show no indicators)
// TODO: Fix git gutter tests on Windows - they fail due to git command output differences
#[test]
#[cfg_attr(target_os = "windows", ignore)]
fn test_git_gutter_untracked_file() {
    let repo = GitTestRepo::new();
    repo.setup_typical_project();
    repo.setup_git_gutter_plugin();

    // Change to repo directory so git commands work correctly
    let original_dir = repo.change_to_repo_dir();
    let _guard = DirGuard::new(original_dir);

    // Create a new untracked file
    repo.create_file("src/new_file.rs", "fn new_function() {}\n");

    let mut harness = EditorTestHarness::with_config_and_working_dir(
        120,
        40,
        Config::default(),
        repo.path.clone(),
    )
    .unwrap();

    open_file(&mut harness, &repo.path, "src/new_file.rs");

    // Wait a bit for git gutter to process
    harness.sleep(std::time::Duration::from_millis(500));
    harness.render().unwrap();

    let screen = harness.screen_to_string();
    println!("Untracked file screen:\n{}", screen);

    // Untracked files should have no git indicators
    let indicators = count_gutter_indicators(&screen, "│");
    assert_eq!(
        indicators, 0,
        "Git gutter should have no indicators for untracked files"
    );
}

// =============================================================================
// Buffer Modified Tests
// =============================================================================

/// Test that buffer modified shows indicators for unsaved changes
#[test]
#[cfg_attr(windows, ignore)] // Uses git plugins which timeout on Windows CI
fn test_buffer_modified_shows_on_edit() {
    let repo = GitTestRepo::new();
    repo.setup_typical_project();
    repo.setup_buffer_modified_plugin();

    // Change to repo directory so plugin can find files correctly
    let original_dir = repo.change_to_repo_dir();
    let _guard = DirGuard::new(original_dir);

    let mut harness = EditorTestHarness::with_config_and_working_dir(
        120,
        40,
        Config::default(),
        repo.path.clone(),
    )
    .unwrap();

    // Open a file
    open_file(&mut harness, &repo.path, "src/main.rs");

    // Initial state - no modifications
    let screen = harness.screen_to_string();
    let initial_indicators = count_gutter_indicators(&screen, "│");

    // Make an edit (but don't save)
    harness.type_text("// Unsaved change\n").unwrap();
    harness.render().unwrap();

    // Wait a bit for plugin to update
    harness.sleep(std::time::Duration::from_millis(100));
    harness.render().unwrap();

    let screen = harness.screen_to_string();
    println!("After edit screen:\n{}", screen);

    let new_indicators = count_gutter_indicators(&screen, "│");
    assert!(
        new_indicators > initial_indicators,
        "Buffer modified should show indicator for unsaved changes"
    );
}

/// Test that buffer modified clears after save
#[test]
#[cfg_attr(windows, ignore)] // Uses git plugins which timeout on Windows CI
fn test_buffer_modified_clears_after_save() {
    let repo = GitTestRepo::new();
    repo.setup_typical_project();
    repo.setup_buffer_modified_plugin();

    // Change to repo directory so plugin can find files correctly
    let original_dir = repo.change_to_repo_dir();
    let _guard = DirGuard::new(original_dir);

    let mut harness = EditorTestHarness::with_config_and_working_dir(
        120,
        40,
        Config::default(),
        repo.path.clone(),
    )
    .unwrap();

    open_file(&mut harness, &repo.path, "src/main.rs");

    // Make an edit
    harness.type_text("// Unsaved change\n").unwrap();
    harness.render().unwrap();
    harness.sleep(std::time::Duration::from_millis(100));

    // Verify we have indicators before save
    harness.render().unwrap();
    let screen_before = harness.screen_to_string();
    let indicators_before = count_gutter_indicators(&screen_before, "│");

    // Save the file
    save_file(&mut harness);

    // Wait for plugin to update
    harness.sleep(std::time::Duration::from_millis(200));
    harness.render().unwrap();

    let screen_after = harness.screen_to_string();
    println!("After save screen:\n{}", screen_after);

    let indicators_after = count_gutter_indicators(&screen_after, "│");

    // After save, buffer modified indicators should be gone
    // (but git gutter might show indicators if git_gutter plugin is also loaded)
    assert!(
        indicators_after < indicators_before || indicators_after == 0,
        "Buffer modified indicators should clear after save"
    );
}

// =============================================================================
// Combined Tests (Both Plugins)
// =============================================================================

/// Test that both git gutter and buffer modified can coexist
// TODO: Fix git gutter tests on Windows - they fail due to git command output differences
#[test]
#[cfg_attr(target_os = "windows", ignore)]
fn test_both_plugins_coexist() {
    let repo = GitTestRepo::new();
    repo.setup_typical_project();
    repo.setup_gutter_plugins(); // Sets up both plugins

    // Change to repo directory so git commands work correctly
    let original_dir = repo.change_to_repo_dir();
    let _guard = DirGuard::new(original_dir);

    // Create an uncommitted change on disk
    repo.modify_file(
        "src/main.rs",
        r#"fn main() {
    println!("Git change on disk!");
    let config = load_config();
    start_server(config);
}

fn load_config() -> Config {
    Config::default()
}

fn start_server(config: Config) {
    println!("Starting server...");
}
"#,
    );

    let mut harness = EditorTestHarness::with_config_and_working_dir(
        120,
        40,
        Config::default(),
        repo.path.clone(),
    )
    .unwrap();

    open_file(&mut harness, &repo.path, "src/main.rs");

    // Wait for git gutter indicators
    harness
        .wait_until(|h| {
            let screen = h.screen_to_string();
            has_gutter_indicator(&screen, "│")
        })
        .unwrap();

    // Now make an additional in-memory edit
    harness
        .send_key(KeyCode::End, KeyModifiers::CONTROL)
        .unwrap();
    harness.type_text("\n// Unsaved edit").unwrap();
    harness.render().unwrap();
    harness.sleep(std::time::Duration::from_millis(100));
    harness.render().unwrap();

    let screen = harness.screen_to_string();
    println!("Both plugins screen:\n{}", screen);

    // Should still have indicators (from either or both plugins)
    let total_indicators = count_gutter_indicators(&screen, "│");
    assert!(
        total_indicators >= 1,
        "Should have indicators from both git changes and unsaved changes"
    );
}

/// Test that git gutter priority is higher than buffer modified
/// (git gutter uses priority 10, buffer modified uses priority 5)
// TODO: Fix git gutter tests on Windows - they fail due to git command output differences
#[test]
#[cfg_attr(target_os = "windows", ignore)]
fn test_git_gutter_priority_over_buffer_modified() {
    let repo = GitTestRepo::new();
    repo.setup_typical_project();
    repo.setup_gutter_plugins();

    // Change to repo directory so git commands work correctly
    let original_dir = repo.change_to_repo_dir();
    let _guard = DirGuard::new(original_dir);

    // Create a committed file first, then modify on disk (for git diff)
    repo.modify_file(
        "src/main.rs",
        r#"fn main() {
    println!("Modified for git!");
    let config = load_config();
    start_server(config);
}

fn load_config() -> Config {
    Config::default()
}

fn start_server(config: Config) {
    println!("Starting server...");
}
"#,
    );

    let mut harness = EditorTestHarness::with_config_and_working_dir(
        120,
        40,
        Config::default(),
        repo.path.clone(),
    )
    .unwrap();

    open_file(&mut harness, &repo.path, "src/main.rs");

    // Wait for git gutter to load
    harness.sleep(std::time::Duration::from_millis(500));
    harness.render().unwrap();

    let screen = harness.screen_to_string();
    println!("Priority test screen:\n{}", screen);

    // The git gutter indicator (priority 10) should be visible,
    // not overridden by buffer_modified (priority 5)
    // Both use │ symbol but with different colors
    assert!(
        has_gutter_indicator(&screen, "│"),
        "Higher priority indicator should be visible"
    );
}

// =============================================================================
// Comprehensive Indicator Behavior Test
// =============================================================================

/// Comprehensive test for gutter indicator behavior:
/// 1. Create a file and commit it
/// 2. Make a change to a specific line, verify git indicators appear on that line
/// 3. Add a newline before the change, verify indicators shift down
/// 4. Verify the newly inserted line gets an unsaved-change indicator
/// 5. Save the file and verify git indicators update correctly
// TODO: Fix git gutter tests on Windows - they fail due to git command output differences
#[test]
#[cfg_attr(target_os = "windows", ignore)]
fn test_gutter_indicators_comprehensive() {
    use std::fs;

    // Create a fresh git repo with a simple test file
    let repo = GitTestRepo::new();

    // Change to repo directory so git commands work correctly
    let original_dir = repo.change_to_repo_dir();
    let _guard = DirGuard::new(original_dir);

    // Create a simple file with numbered lines for easy tracking
    let initial_content = r#"line 1: unchanged
line 2: unchanged
line 3: will be modified
line 4: unchanged
line 5: unchanged
"#;
    repo.create_file("test.txt", initial_content);
    repo.git_add_all();
    repo.git_commit("Initial commit");

    // Set up the gutter plugins
    repo.setup_gutter_plugins();

    // Modify line 3 on disk (simulating a change that will show in git diff)
    let modified_content = r#"line 1: unchanged
line 2: unchanged
line 3: MODIFIED!
line 4: unchanged
line 5: unchanged
"#;
    fs::write(repo.path.join("test.txt"), modified_content).unwrap();

    // Create harness and open the file
    let mut harness = EditorTestHarness::with_config_and_working_dir(
        120,
        40,
        Config::default(),
        repo.path.clone(),
    )
    .unwrap();

    open_file(&mut harness, &repo.path, "test.txt");

    // Manually trigger git gutter refresh to ensure it runs
    trigger_git_gutter_refresh(&mut harness);

    // Wait for git gutter indicator to appear on line 2 (0-indexed, which is line 3 in the file)
    wait_for_indicator_on_line(&mut harness, "│", 2);

    let screen = harness.screen_to_string();
    println!("=== After opening modified file ===\n{}", screen);

    // STEP 1: Verify git gutter shows indicator on the modified line (line 3, 0-indexed = line 2)
    let indicator_lines = get_indicator_lines(&screen, "│");
    println!("Indicator lines after open: {:?}", indicator_lines);

    // STEP 2: Now make an in-editor change - insert a newline before line 3
    // First, go to the beginning of line 3
    harness
        .send_key(KeyCode::Char('g'), KeyModifiers::CONTROL)
        .unwrap(); // Go to beginning
    harness.render().unwrap();

    // Go down to line 3 (press Down twice from line 1)
    harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
    harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
    harness.render().unwrap();

    // Go to beginning of line
    harness.send_key(KeyCode::Home, KeyModifiers::NONE).unwrap();
    harness.render().unwrap();

    // Insert a new line above (this should push line 3 down to line 4)
    harness.type_text("NEW LINE INSERTED\n").unwrap();
    harness.render().unwrap();

    // Wait for indicator to appear on the newly inserted line (now at index 2)
    wait_for_indicator_on_line(&mut harness, "│", 2);

    let screen_after_insert = harness.screen_to_string();
    println!("=== After inserting new line ===\n{}", screen_after_insert);

    // STEP 3: Verify indicators
    let indicator_lines_after = get_indicator_lines(&screen_after_insert, "│");
    println!("Indicator lines after insert: {:?}", indicator_lines_after);

    // After inserting a line before line 3:
    // - The newly inserted line (now line 3) should have an unsaved-changes indicator
    // - The originally modified line (now line 4) should still have a git indicator
    // Both use │ symbol, so we should see indicators on at least 2 lines

    // Count total indicators - should have at least 2 (one for unsaved change, one for git change)
    let indicator_count = count_gutter_indicators(&screen_after_insert, "│");
    println!("Total indicators after insert: {}", indicator_count);

    // We expect indicators on:
    // - Line index 2: the newly inserted "NEW LINE INSERTED" (unsaved change)
    // - Line index 3: the original "line 3: MODIFIED!" which moved down (git change)

    // STEP 4: Save the file and verify git indicators update
    save_file(&mut harness);

    // Trigger git gutter refresh after save
    trigger_git_gutter_refresh(&mut harness);

    // Wait for indicators to update (should still have git indicators after save)
    wait_for_indicator(&mut harness, "│");

    let screen_after_save = harness.screen_to_string();
    println!("=== After save ===\n{}", screen_after_save);

    let indicator_lines_after_save = get_indicator_lines(&screen_after_save, "│");
    println!(
        "Indicator lines after save: {:?}",
        indicator_lines_after_save
    );

    // After save:
    // - Unsaved-changes indicators should be cleared (buffer matches disk)
    // - Git indicators should show for all lines that differ from HEAD
    // - This includes: the newly inserted line AND the modified line

    // The test passes if we can see that the indicator system is working
    // Even if async timing makes exact line matching difficult
    println!("\n=== Test Summary ===");
    println!(
        "Initial indicator count: {}",
        get_indicator_lines(&screen, "│").len()
    );
    println!("After insert indicator count: {}", indicator_count);
    println!(
        "After save indicator count: {}",
        indicator_lines_after_save.len()
    );

    // Basic sanity check - after editing, we should have some indicators
    // (either from git gutter or buffer modified plugin)
    assert!(
        indicator_count >= 1 || indicator_lines_after_save.len() >= 1,
        "Should have at least one indicator after making changes. \
         After insert: {}, After save: {}",
        indicator_count,
        indicator_lines_after_save.len()
    );
}

/// Test that unsaved changes get indicators from buffer_modified plugin
#[test]
fn test_unsaved_changes_get_indicators() {
    let repo = GitTestRepo::new();

    // Change to repo directory so plugin can find files correctly
    let original_dir = repo.change_to_repo_dir();
    let _guard = DirGuard::new(original_dir);

    // Create and commit a simple file
    let initial_content = "line 1\nline 2\nline 3\n";
    repo.create_file("test.txt", initial_content);
    repo.git_add_all();
    repo.git_commit("Initial commit");

    // Only set up buffer_modified plugin (not git_gutter) to isolate the test
    repo.setup_buffer_modified_plugin();

    let mut harness = EditorTestHarness::with_config_and_working_dir(
        120,
        40,
        Config::default(),
        repo.path.clone(),
    )
    .unwrap();

    open_file(&mut harness, &repo.path, "test.txt");

    let screen_before = harness.screen_to_string();
    let indicators_before = count_gutter_indicators(&screen_before, "│");
    println!("=== Before edit ===\n{}", screen_before);
    println!("Indicators before edit: {}", indicators_before);

    // Make an edit - modify line 2
    harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap(); // Go to line 2
    harness.send_key(KeyCode::End, KeyModifiers::NONE).unwrap(); // Go to end of line
    harness.type_text(" MODIFIED").unwrap();
    harness.render().unwrap();

    // Wait for indicator to appear on the edited line (line 1, 0-indexed)
    wait_for_indicator_on_line(&mut harness, "│", 1);

    let screen_after = harness.screen_to_string();
    let indicators_after = count_gutter_indicators(&screen_after, "│");
    println!("=== After edit ===\n{}", screen_after);
    println!("Indicators after edit: {}", indicators_after);

    // Should have at least one indicator on the modified line
    assert!(
        indicators_after > indicators_before,
        "Should have more indicators after editing. Before: {}, After: {}",
        indicators_before,
        indicators_after
    );

    // Save and verify indicators clear
    save_file(&mut harness);

    // Wait for buffer modified indicators to clear after save
    wait_for_no_indicators(&mut harness, "│");

    let screen_after_save = harness.screen_to_string();
    let indicators_after_save = count_gutter_indicators(&screen_after_save, "│");
    println!("=== After save ===\n{}", screen_after_save);
    println!("Indicators after save: {}", indicators_after_save);

    // After save, buffer_modified indicators should clear
    // (there might still be git indicators if git_gutter was also loaded)
    assert!(
        indicators_after_save <= indicators_after,
        "Indicators should not increase after save. After edit: {}, After save: {}",
        indicators_after,
        indicators_after_save
    );
}

/// Test that reverting an edit clears the buffer_modified indicator on the same line (no off-by-one)
#[test]
fn test_buffer_modified_clears_after_undo_on_same_line() {
    let repo = GitTestRepo::new();

    // Change to repo directory so plugin can find files correctly
    let original_dir = repo.change_to_repo_dir();
    let _guard = DirGuard::new(original_dir);

    // Create and commit a multi-line file
    let initial_content = (1..=15)
        .map(|i| format!("line {:02}\n", i))
        .collect::<String>();
    repo.create_file("test.txt", &initial_content);
    repo.git_add_all();
    repo.git_commit("Initial commit");

    repo.setup_buffer_modified_plugin();

    let mut harness = EditorTestHarness::with_config_and_working_dir(
        120,
        40,
        Config::default(),
        repo.path.clone(),
    )
    .unwrap();

    open_file(&mut harness, &repo.path, "test.txt");
    process_async_once(&mut harness);

    // Move to line 1, append text
    harness.send_key(KeyCode::Home, KeyModifiers::NONE).unwrap();
    harness.send_key(KeyCode::End, KeyModifiers::NONE).unwrap();
    harness.type_text(" MOD").unwrap();
    harness.render().unwrap();
    wait_for_indicator_on_line(&mut harness, "│", 0);

    let screen_after = harness.screen_to_string();
    let indicators_after = get_indicator_lines(&screen_after, "│");
    println!("=== After edit ===\n{}", screen_after);
    assert_eq!(
        indicators_after,
        vec![0],
        "Indicator should appear on edited line (line 0), got {:?}",
        indicators_after
    );

    // Undo the edit (4 chars)
    for _ in 0..4 {
        harness
            .send_key(KeyCode::Char('z'), KeyModifiers::CONTROL)
            .unwrap();
    }
    harness.render().unwrap();
    wait_for_no_indicators(&mut harness, "│");

    let screen_after_undo = harness.screen_to_string();
    let indicators_after_undo = get_indicator_lines(&screen_after_undo, "│");
    println!("=== After undo ===\n{}", screen_after_undo);
    assert!(
        indicators_after_undo.is_empty(),
        "Indicators should clear after undo to saved state, got {:?}",
        indicators_after_undo
    );
}

/// Test that editing one line in a multi-line file only marks that line, and clears after undo
#[test]
fn test_buffer_modified_single_line_in_multi_line_file() {
    let repo = GitTestRepo::new();

    // Change to repo directory so plugin can find files correctly
    let original_dir = repo.change_to_repo_dir();
    let _guard = DirGuard::new(original_dir);

    // Create and commit a multi-line file
    let initial_content = (1..=15)
        .map(|i| format!("line {:02}\n", i))
        .collect::<String>();
    repo.create_file("test.txt", &initial_content);
    repo.git_add_all();
    repo.git_commit("Initial commit");

    repo.setup_buffer_modified_plugin();

    let mut harness = EditorTestHarness::with_config_and_working_dir(
        120,
        40,
        Config::default(),
        repo.path.clone(),
    )
    .unwrap();

    open_file(&mut harness, &repo.path, "test.txt");
    process_async_once(&mut harness);

    // Move to line 10 (0-based index 9) and edit it
    for _ in 0..9 {
        harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
    }
    harness.send_key(KeyCode::End, KeyModifiers::NONE).unwrap();
    harness.type_text(" MOD").unwrap();
    harness.render().unwrap();
    wait_for_indicator_on_line(&mut harness, "│", 9);

    let screen_after = harness.screen_to_string();
    let indicators_after = get_indicator_lines(&screen_after, "│");
    println!("=== After edit (multi-line) ===\n{}", screen_after);
    assert_eq!(
        indicators_after,
        vec![9],
        "Only the edited line should have indicator, got {:?}",
        indicators_after
    );

    // Undo the edit
    for _ in 0..4 {
        harness
            .send_key(KeyCode::Char('z'), KeyModifiers::CONTROL)
            .unwrap();
    }
    harness.render().unwrap();
    wait_for_no_indicators(&mut harness, "│");

    let screen_after_undo = harness.screen_to_string();
    let indicators_after_undo = get_indicator_lines(&screen_after_undo, "│");
    println!("=== After undo (multi-line) ===\n{}", screen_after_undo);
    assert!(
        indicators_after_undo.is_empty(),
        "Indicators should clear after undo, got {:?}",
        indicators_after_undo
    );
}

/// Test that inserting a newline only marks the affected lines, not the entire rest of the buffer
/// This is a regression test for a bug where line-by-line comparison would mark all subsequent
/// lines as changed because they shifted down by one.
#[test]
fn test_buffer_modified_newline_insert_only_marks_affected_lines() {
    let repo = GitTestRepo::new();

    // Change to repo directory so plugin can find files correctly
    let original_dir = repo.change_to_repo_dir();
    let _guard = DirGuard::new(original_dir);

    // Create and commit a file with multiple lines
    let initial_content = "line 1\nline 2\nline 3\nline 4\nline 5\n";
    repo.create_file("test.txt", initial_content);
    repo.git_add_all();
    repo.git_commit("Initial commit");

    repo.setup_buffer_modified_plugin();

    let mut harness = EditorTestHarness::with_config_and_working_dir(
        120,
        40,
        Config::default(),
        repo.path.clone(),
    )
    .unwrap();

    open_file(&mut harness, &repo.path, "test.txt");
    process_async_once(&mut harness);

    // Go to end of line 2 and insert a newline (creating a new empty line)
    harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap(); // line 2
    harness.send_key(KeyCode::End, KeyModifiers::NONE).unwrap();
    harness
        .send_key(KeyCode::Enter, KeyModifiers::NONE)
        .unwrap();
    harness.render().unwrap();
    wait_for_indicator(&mut harness, "│");

    let screen = harness.screen_to_string();
    let indicators = get_indicator_lines(&screen, "│");
    println!("=== After inserting newline ===\n{}", screen);
    println!("Indicator lines: {:?}", indicators);

    // Only lines 1-2 (0-indexed) should be marked - the modified line and the new line
    // Lines 3, 4, 5 should NOT have indicators even though they shifted down
    assert!(
        indicators.len() <= 2,
        "Only the modified lines should have indicators, not the entire rest of the buffer. Got {:?}",
        indicators
    );

    // Specifically, lines 3+ should not have indicators
    let has_line_3_plus = indicators.iter().any(|&line| line >= 3);
    assert!(
        !has_line_3_plus,
        "Lines 3+ should not have indicators (they just shifted, content unchanged). Got {:?}",
        indicators
    );
}

/// Test that manually deleting added text (without undo) clears the indicator
/// This tests that the diff compares actual content, not just tree structure
#[test]
fn test_buffer_modified_clears_after_manual_delete_restores_content() {
    // Install signal handler to dump thread backtraces on timeout/SIGINT
    fresh::services::signal_handler::install_signal_handlers();

    let repo = GitTestRepo::new();

    // Change to repo directory so plugin can find files correctly
    let original_dir = repo.change_to_repo_dir();
    let _guard = DirGuard::new(original_dir);

    // Create and commit a file
    let initial_content = "line 01\nline 02\nline 03\nline 04\nline 05\n";
    repo.create_file("test.txt", initial_content);
    repo.git_add_all();
    repo.git_commit("Initial commit");

    repo.setup_buffer_modified_plugin();

    let mut harness = EditorTestHarness::with_config_and_working_dir(
        120,
        40,
        Config::default(),
        repo.path.clone(),
    )
    .unwrap();

    open_file(&mut harness, &repo.path, "test.txt");
    process_async_once(&mut harness);

    // Go to line 3, end of line, add text
    harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
    harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
    harness.send_key(KeyCode::End, KeyModifiers::NONE).unwrap();
    harness.type_text(" ADDED").unwrap();
    harness.render().unwrap();
    wait_for_indicator_on_line(&mut harness, "│", 2);

    let screen_after_add = harness.screen_to_string();
    let indicators_after_add = get_indicator_lines(&screen_after_add, "│");
    println!("=== After adding text ===\n{}", screen_after_add);
    assert!(
        indicators_after_add.contains(&2),
        "Line 2 (0-indexed) should have indicator after adding text, got {:?}",
        indicators_after_add
    );

    // Now manually delete the " ADDED" text (6 chars) using backspace
    for _ in 0..6 {
        harness
            .send_key(KeyCode::Backspace, KeyModifiers::NONE)
            .unwrap();
    }
    harness.render().unwrap();
    wait_for_no_indicators(&mut harness, "│");

    let screen_after_delete = harness.screen_to_string();
    let indicators_after_delete = get_indicator_lines(&screen_after_delete, "│");
    println!(
        "=== After manually deleting text ===\n{}",
        screen_after_delete
    );
    assert!(
        indicators_after_delete.is_empty(),
        "Indicators should clear when content is manually restored to saved state, got {:?}",
        indicators_after_delete
    );
}

/// Test that pasting original content back clears the indicator
#[test]
#[ignore = "flaky test - times out intermittently"]
fn test_buffer_modified_clears_after_paste_restores_content() {
    let repo = GitTestRepo::new();

    // Change to repo directory so plugin can find files correctly
    let original_dir = repo.change_to_repo_dir();
    let _guard = DirGuard::new(original_dir);

    // Create and commit a file
    let initial_content = "hello world\n";
    repo.create_file("test.txt", initial_content);
    repo.git_add_all();
    repo.git_commit("Initial commit");

    repo.setup_buffer_modified_plugin();

    let mut harness = EditorTestHarness::with_config_and_working_dir(
        120,
        40,
        Config::default(),
        repo.path.clone(),
    )
    .unwrap();

    open_file(&mut harness, &repo.path, "test.txt");
    process_async_once(&mut harness);

    // Select "world", cut it (Ctrl+X cuts and copies)
    // Go to position of 'w' in "world"
    harness.send_key(KeyCode::Home, KeyModifiers::NONE).unwrap();
    for _ in 0..6 {
        // Move past "hello "
        harness
            .send_key(KeyCode::Right, KeyModifiers::NONE)
            .unwrap();
    }
    // Select "world" (5 chars)
    for _ in 0..5 {
        harness
            .send_key(KeyCode::Right, KeyModifiers::SHIFT)
            .unwrap();
    }
    // Cut (copies to clipboard and deletes)
    harness
        .send_key(KeyCode::Char('x'), KeyModifiers::CONTROL)
        .unwrap();
    harness.render().unwrap();
    wait_for_indicator(&mut harness, "│");

    let screen_after_cut = harness.screen_to_string();
    let indicators_after_cut = get_indicator_lines(&screen_after_cut, "│");
    println!("=== After cutting 'world' ===\n{}", screen_after_cut);
    assert!(
        !indicators_after_cut.is_empty(),
        "Should have indicator after cutting text"
    );

    // Now paste "world" back
    harness
        .send_key(KeyCode::Char('v'), KeyModifiers::CONTROL)
        .unwrap();
    harness.render().unwrap();
    wait_for_no_indicators(&mut harness, "│");

    let screen_after_paste = harness.screen_to_string();
    let indicators_after_paste = get_indicator_lines(&screen_after_paste, "│");
    println!("=== After pasting 'world' back ===\n{}", screen_after_paste);
    assert!(
        indicators_after_paste.is_empty(),
        "Indicators should clear when content is restored via paste, got {:?}",
        indicators_after_paste
    );
}

/// Test that adding lines shifts indicators correctly
// TODO: Fix git gutter tests on Windows - they fail due to git command output differences
#[test]
#[cfg_attr(target_os = "windows", ignore)]
fn test_indicator_line_shifting() {
    use std::fs;

    let repo = GitTestRepo::new();

    // Change to repo directory so git commands work correctly
    let original_dir = repo.change_to_repo_dir();
    let _guard = DirGuard::new(original_dir);

    // Create a file with a modification on a specific line
    let initial_content = "line 1\nline 2\nline 3\nline 4\nline 5\n";
    repo.create_file("test.txt", initial_content);
    repo.git_add_all();
    repo.git_commit("Initial commit");

    // Modify line 3 on disk
    let modified_content = "line 1\nline 2\nline 3 CHANGED\nline 4\nline 5\n";
    fs::write(repo.path.join("test.txt"), modified_content).unwrap();

    repo.setup_git_gutter_plugin();

    let mut harness = EditorTestHarness::with_config_and_working_dir(
        120,
        40,
        Config::default(),
        repo.path.clone(),
    )
    .unwrap();

    open_file(&mut harness, &repo.path, "test.txt");
    trigger_git_gutter_refresh(&mut harness);
    wait_for_indicator(&mut harness, "│");

    let screen_initial = harness.screen_to_string();
    let lines_initial = get_indicator_lines(&screen_initial, "│");
    println!("=== Initial state ===\n{}", screen_initial);
    println!("Initial indicator lines: {:?}", lines_initial);

    // Remember which content lines had indicators
    let content_lines = get_content_lines(&screen_initial);
    println!("Content lines count: {}", content_lines.len());

    // Now insert two lines at the beginning of the file
    harness
        .send_key(KeyCode::Char('g'), KeyModifiers::CONTROL)
        .unwrap(); // Go to beginning
    harness.render().unwrap();
    harness
        .type_text("inserted line A\ninserted line B\n")
        .unwrap();
    harness.render().unwrap();

    // Save so git diff can see the changes
    save_file(&mut harness);
    trigger_git_gutter_refresh(&mut harness);
    // Wait for indicators to appear (the inserted lines should show as added)
    wait_for_indicator(&mut harness, "│");

    let screen_after = harness.screen_to_string();
    let lines_after = get_indicator_lines(&screen_after, "│");
    println!(
        "=== After inserting 2 lines at beginning ===\n{}",
        screen_after
    );
    println!("Indicator lines after: {:?}", lines_after);

    // The original line 3 (which was modified) is now at line 5
    // Plus the two new lines should also show as added
    // So we expect indicators on lines that are different from the original commit

    // At minimum, we should have indicators for the changes
    assert!(
        !lines_after.is_empty() || lines_initial.is_empty(),
        "After inserting lines and saving, git diff should show changes"
    );

    println!("\n=== Shift Test Summary ===");
    println!("Initial indicators: {:?}", lines_initial);
    println!("After shift indicators: {:?}", lines_after);
}
