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

/// Test file explorer toggle
#[test]
fn test_file_explorer_toggle() {
    let mut harness = EditorTestHarness::new(120, 40).unwrap();

    // Initially file explorer should not be visible
    harness.render().unwrap();
    let screen_before = harness.screen_to_string();

    // Toggle file explorer on with Ctrl+E
    harness
        .send_key(KeyCode::Char('e'), KeyModifiers::CONTROL)
        .unwrap();
    // Wait for file explorer to appear
    harness
        .wait_until(|h| {
            let screen = h.screen_to_string();
            screen.contains("File Explorer")
        })
        .unwrap();

    // Screen should show file explorer (check for the border or title)
    let screen_after = harness.screen_to_string();

    // Should show "File Explorer" in the UI
    assert!(
        screen_after.contains("File Explorer"),
        "Screen should show file explorer after toggle"
    );

    // Toggle file explorer off with Ctrl+E
    harness
        .send_key(KeyCode::Char('e'), KeyModifiers::CONTROL)
        .unwrap();
    harness.render().unwrap();

    // File Explorer text should no longer be visible
    let screen_final = harness.screen_to_string();
    if screen_before.contains("File Explorer") {
        // If it was there before, it should still be there
        assert!(screen_final.contains("File Explorer"));
    } else {
        // If it wasn't there before, check that the screen returned to normal
        // (allowing for status message changes)
        assert_eq!(
            screen_before.lines().count(),
            screen_final.lines().count(),
            "Screen should return to similar state when file explorer is toggled off"
        );
    }
}

/// Test file explorer displays directory structure
#[test]
fn test_file_explorer_shows_directory_structure() {
    // Create harness with isolated temp project
    let mut harness = EditorTestHarness::with_temp_project(120, 40).unwrap();
    let project_root = harness.project_dir().unwrap();

    // Create some files and directories
    fs::create_dir(project_root.join("src")).unwrap();
    fs::write(project_root.join("src/main.rs"), "fn main() {}").unwrap();
    fs::write(project_root.join("Cargo.toml"), "[package]").unwrap();
    fs::create_dir(project_root.join("tests")).unwrap();
    fs::write(project_root.join("README.md"), "# Project").unwrap();

    // Toggle file explorer on with Ctrl+E
    harness
        .send_key(KeyCode::Char('e'), KeyModifiers::CONTROL)
        .unwrap();
    harness.sleep(std::time::Duration::from_millis(100));
    let _ = harness.editor_mut().process_async_messages();
    harness.render().unwrap();

    // Wait a moment for async file system operations
    harness.sleep(std::time::Duration::from_millis(100));
    harness.render().unwrap();

    // Check that we see the project structure
    // Note: The exact rendering might differ, but we should see some files
    let screen = harness.screen_to_string();
    println!("File explorer screen:\n{screen}");

    // Should show at least the root directory name or some indication of files
    // (This is a basic check - the exact content depends on rendering)
}

/// Test file explorer navigation
#[test]
fn test_file_explorer_navigation() {
    // Create harness with isolated temp project
    let mut harness = EditorTestHarness::with_temp_project(120, 40).unwrap();
    let project_root = harness.project_dir().unwrap();

    fs::write(project_root.join("file1.txt"), "File 1").unwrap();
    fs::write(project_root.join("file2.txt"), "File 2").unwrap();
    fs::write(project_root.join("file3.txt"), "File 3").unwrap();

    // Toggle file explorer on with Ctrl+E
    harness
        .send_key(KeyCode::Char('e'), KeyModifiers::CONTROL)
        .unwrap();
    harness.sleep(std::time::Duration::from_millis(100));
    let _ = harness.editor_mut().process_async_messages();

    // Wait for initialization
    harness.sleep(std::time::Duration::from_millis(100));
    harness.render().unwrap();

    let _screen_initial = harness.screen_to_string();

    // Navigate down with Alt+J
    harness
        .send_key(KeyCode::Char('j'), KeyModifiers::ALT)
        .unwrap();
    harness.render().unwrap();

    let screen_after_down = harness.screen_to_string();

    // Screen should change (selection moved)
    // Note: This might be subtle depending on rendering
    println!("After navigate down:\n{screen_after_down}");

    // Navigate up with Alt+K
    harness
        .send_key(KeyCode::Char('k'), KeyModifiers::ALT)
        .unwrap();
    harness.render().unwrap();
}

/// Test file explorer expand/collapse
#[test]
fn test_file_explorer_expand_collapse() {
    // Create harness with isolated temp project
    let mut harness = EditorTestHarness::with_temp_project(120, 40).unwrap();
    let project_root = harness.project_dir().unwrap();

    fs::create_dir(project_root.join("src")).unwrap();
    fs::write(project_root.join("src/lib.rs"), "// lib").unwrap();
    fs::write(project_root.join("src/main.rs"), "fn main() {}").unwrap();

    // Toggle file explorer on with Ctrl+E
    harness
        .send_key(KeyCode::Char('e'), KeyModifiers::CONTROL)
        .unwrap();
    harness.sleep(std::time::Duration::from_millis(100));
    let _ = harness.editor_mut().process_async_messages();

    // Wait for initialization
    harness.sleep(std::time::Duration::from_millis(100));
    harness.render().unwrap();

    let screen_before_expand = harness.screen_to_string();
    println!("Before expand:\n{screen_before_expand}");

    // Expand the root directory with Alt+L
    harness
        .send_key(KeyCode::Char('l'), KeyModifiers::ALT)
        .unwrap();

    // Wait for async operation
    harness.sleep(std::time::Duration::from_millis(100));
    let _ = harness.editor_mut().process_async_messages();
    harness.render().unwrap();

    let screen_after_expand = harness.screen_to_string();
    println!("After expand:\n{screen_after_expand}");

    // The screen should show more content after expanding
    // (exact assertion depends on rendering details)

    // Collapse with Alt+L (toggle)
    harness
        .send_key(KeyCode::Char('l'), KeyModifiers::ALT)
        .unwrap();

    harness.sleep(std::time::Duration::from_millis(100));
    let _ = harness.editor_mut().process_async_messages();
    harness.render().unwrap();
}

/// Test opening a file from file explorer
#[test]
fn test_file_explorer_open_file() {
    // Create harness with isolated temp project
    let mut harness = EditorTestHarness::with_temp_project(120, 40).unwrap();
    let project_root = harness.project_dir().unwrap();

    let test_file = project_root.join("simple.txt");
    let test_content = "Hello World";
    fs::write(&test_file, test_content).unwrap();

    // Toggle file explorer on with Ctrl+E
    harness
        .send_key(KeyCode::Char('e'), KeyModifiers::CONTROL)
        .unwrap();
    // Wait for file explorer to be visible
    harness
        .wait_until(|h| {
            let screen = h.screen_to_string();
            screen.contains("File Explorer")
        })
        .unwrap();

    let screen_with_explorer = harness.screen_to_string();

    // Verify file explorer is showing
    assert!(
        screen_with_explorer.contains("File Explorer"),
        "File explorer should be visible"
    );

    // The file might already be visible if root auto-expanded (single item case)
    // If not visible, expand the root directory
    if !screen_with_explorer.contains("simple.txt") {
        harness
            .send_key(KeyCode::Right, KeyModifiers::NONE)
            .unwrap();
        // Wait for expansion by checking the screen shows the file
        harness
            .wait_until(|h| h.screen_to_string().contains("simple.txt"))
            .unwrap();
    }

    // Navigate down to the file (first child after root)
    harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
    harness.render().unwrap();

    // Try to open with Enter - should work if we're on a file
    harness
        .send_key(KeyCode::Enter, KeyModifiers::NONE)
        .unwrap();

    // Wait for the file to be opened (content should appear)
    harness
        .wait_until(|h| h.screen_to_string().contains("Hello World"))
        .unwrap();

    // Verify the file was opened
    let buffer_content = harness.get_buffer_content().unwrap();
    assert_eq!(
        buffer_content, test_content,
        "Buffer should contain the opened file's content"
    );
}

/// Test file explorer refresh
#[test]
fn test_file_explorer_refresh() {
    // Create harness with isolated temp project
    let mut harness = EditorTestHarness::with_temp_project(120, 40).unwrap();
    let project_root = harness.project_dir().unwrap();

    // Create initial file
    fs::write(project_root.join("file1.txt"), "File 1").unwrap();

    // Toggle file explorer on
    harness.editor_mut().toggle_file_explorer();
    harness.sleep(std::time::Duration::from_millis(100));
    let _ = harness.editor_mut().process_async_messages();

    // Wait for initialization
    harness.sleep(std::time::Duration::from_millis(100));
    harness.render().unwrap();

    // Add a new file to the directory
    fs::write(project_root.join("file2.txt"), "File 2").unwrap();

    // Refresh the file explorer
    harness.editor_mut().file_explorer_refresh();

    // Wait for refresh
    harness.sleep(std::time::Duration::from_millis(100));
    let _ = harness.editor_mut().process_async_messages();
    harness.render().unwrap();

    // The new file should now be visible
    // (This is hard to assert precisely without introspecting the tree structure)
    let screen = harness.screen_to_string();
    println!("After refresh:\n{screen}");
}

/// Test focus switching between file explorer and editor
#[test]
fn test_file_explorer_focus_switching() {
    let mut harness = EditorTestHarness::new(120, 40).unwrap();

    // Open file explorer
    harness.editor_mut().toggle_file_explorer();
    harness.sleep(std::time::Duration::from_millis(100));
    let _ = harness.editor_mut().process_async_messages();
    harness.render().unwrap();

    // File explorer should be visible and focused
    assert!(harness.editor().file_explorer_visible());

    // Try using arrow keys - in FileExplorer context, these should navigate the explorer
    harness
        .send_key(KeyCode::Down, KeyModifiers::empty())
        .unwrap();
    harness.render().unwrap();

    // Toggle file explorer off
    harness.editor_mut().toggle_file_explorer();
    harness.render().unwrap();

    // File explorer should be hidden now
    assert!(!harness.editor().file_explorer_visible());

    // Toggle file explorer back on
    harness.editor_mut().toggle_file_explorer();
    harness.sleep(std::time::Duration::from_millis(100));
    let _ = harness.editor_mut().process_async_messages();
    harness.render().unwrap();

    // Should be visible again
    assert!(harness.editor().file_explorer_visible());

    // Focus the editor (without toggling file explorer off)
    harness.editor_mut().active_window_mut().focus_editor();
    harness.render().unwrap();

    // File explorer should still be visible, just not focused
    assert!(harness.editor().file_explorer_visible());
}

/// Run `command` from the palette and return the resulting screen.
///
/// `EditorTestHarness::run_palette_command` is what waits for the row to be
/// listed before pressing Enter; this only adds the render + capture.
fn run_from_palette(harness: &mut EditorTestHarness, command: &str) -> String {
    harness.run_palette_command(command).unwrap();
    harness.render().unwrap();
    harness.screen_to_string()
}

/// Commands that don't need a focused buffer still run from the palette while
/// the file explorer holds the keyboard. Before this, the explorer disabled
/// every `Normal`-context command, so Enter produced "not available in current
/// context" for editor-wide ones too.
#[test]
fn test_palette_runs_focus_independent_command_while_explorer_focused() {
    let mut harness = EditorTestHarness::new(120, 40).unwrap();

    harness.editor_mut().focus_file_explorer();
    let _ = harness.editor_mut().process_async_messages();
    harness.render().unwrap();
    assert!(harness.editor().file_explorer_visible());

    // Editor-wide: the global view toggles don't care what has focus, and the
    // status line reports the new state — proof it ran.
    let screen = run_from_palette(&mut harness, "Toggle Line Wrap");
    assert!(
        !screen.contains("not available in current context"),
        "the palette refused an editor-wide toggle in the explorer\nScreen:\n{screen}"
    );
    assert!(
        screen.contains("Line wrap"),
        "Toggle Line Wrap should have run and reported the new state\nScreen:\n{screen}"
    );

    // And a command whose keybinding already falls through to the explorer
    // (`is_terminal_ui_action`) is no longer greyed out in the palette either.
    let screen = run_from_palette(&mut harness, "Toggle Utility Dock");
    assert!(
        screen.contains("No Utility Dock open"),
        "Toggle Utility Dock should have run and reported no dock\nScreen:\n{screen}"
    );
}

/// Saving is the keymap's application-wide exception — Ctrl+S already saves
/// from the explorer — so the palette offers it there too, and it lands on the
/// buffer that had focus before the tree took it.
#[test]
fn test_palette_saves_last_focused_buffer_from_explorer() {
    let mut harness = EditorTestHarness::with_temp_project(120, 40).unwrap();
    let project_root = harness.project_dir().unwrap();
    let file = project_root.join("saveme.txt");
    fs::write(&file, "before\n").unwrap();

    harness.open_file(&file).unwrap();
    harness.type_text("EDITED").unwrap();
    harness.render().unwrap();
    // The tab carries the modified marker while the edit is unsaved.
    harness.assert_screen_contains("saveme.txt*");

    harness.editor_mut().focus_file_explorer();
    let _ = harness.editor_mut().process_async_messages();
    harness.render().unwrap();

    let screen = run_from_palette(&mut harness, "Save File");
    assert!(
        !screen.contains("not available in current context"),
        "Save File must not be refused in the explorer — Ctrl+S works there\nScreen:\n{screen}"
    );

    // The marker clears: the save reached the buffer that had focus before the
    // tree did, not the tree's selection.
    harness
        .wait_until(|h| !h.screen_to_string().contains("saveme.txt*"))
        .expect("the modified marker should clear once the buffer is saved");
    assert!(
        fs::read_to_string(&file).unwrap().contains("EDITED"),
        "the edit should have reached disk"
    );
}

/// The other half of the rule: the explorer owns the keyboard, so commands that
/// act on the focused buffer's cursor are *not* offered through it.
#[test]
fn test_palette_refuses_buffer_command_while_explorer_focused() {
    let mut harness = EditorTestHarness::new(120, 40).unwrap();

    // Typed before focusing the tree — the explorer swallows typing.
    harness.type_text("UNIQUELINE").unwrap();
    harness.render().unwrap();

    harness.editor_mut().focus_file_explorer();
    let _ = harness.editor_mut().process_async_messages();
    harness.render().unwrap();

    let screen = run_from_palette(&mut harness, "Duplicate Line");

    assert!(
        screen.contains("not available in current context"),
        "Duplicate Line needs the buffer's cursor and should be refused from the explorer\nScreen:\n{screen}"
    );
    assert_eq!(
        screen.matches("UNIQUELINE").count(),
        1,
        "the line must not have been duplicated\nScreen:\n{screen}"
    );
}

/// Test that file explorer keybindings only work when explorer has focus
#[test]
fn test_file_explorer_context_aware_keybindings() {
    // Create harness with isolated temp project
    let mut harness = EditorTestHarness::with_temp_project(120, 40).unwrap();
    let project_root = harness.project_dir().unwrap();
    std::fs::write(project_root.join("test.txt"), "content").unwrap();

    // Open file explorer (starts with focus)
    harness.editor_mut().toggle_file_explorer();
    harness.sleep(std::time::Duration::from_millis(100));
    let _ = harness.editor_mut().process_async_messages();
    harness.render().unwrap();

    // Arrow keys should work in file explorer context
    harness
        .send_key(KeyCode::Down, KeyModifiers::empty())
        .unwrap();
    harness.render().unwrap();

    // Switch to editor context
    harness
        .send_key(KeyCode::Esc, KeyModifiers::empty())
        .unwrap();
    harness.render().unwrap();

    // Now arrow keys should work for editor navigation, not file explorer
    harness
        .send_key(KeyCode::Down, KeyModifiers::empty())
        .unwrap();
    harness.render().unwrap();
}

/// Test opening file explorer with focus
#[test]
fn test_focus_file_explorer_action() {
    let mut harness = EditorTestHarness::new(120, 40).unwrap();

    // Initially, file explorer is not visible
    assert!(!harness.editor().file_explorer_visible());

    // Open and focus file explorer
    harness.editor_mut().focus_file_explorer();
    harness.sleep(std::time::Duration::from_millis(100));
    let _ = harness.editor_mut().process_async_messages();
    harness.render().unwrap();

    // File explorer should now be visible and focused
    assert!(harness.editor().file_explorer_visible());

    // Switch focus back to editor
    harness.editor_mut().active_window_mut().focus_editor();
    harness.render().unwrap();

    // File explorer should still be visible
    assert!(harness.editor().file_explorer_visible());

    // Focus file explorer again
    harness.editor_mut().focus_file_explorer();
    harness.render().unwrap();

    // Should still be visible
    assert!(harness.editor().file_explorer_visible());
}

/// Test that opening a file from file explorer actually displays its content
/// This reproduces the bug where a new buffer is created but the content area
/// still shows the old buffer
#[test]
#[ignore = "File explorer directory expansion not working properly - needs investigation of Alt+L keybinding"]
fn test_file_explorer_displays_opened_file_content() {
    // Create harness with isolated temp project
    let mut harness = EditorTestHarness::with_temp_project(120, 40).unwrap();
    let project_root = harness.project_dir().unwrap();

    let file1 = project_root.join("first.txt");
    let file2 = project_root.join("second.txt");
    let content1 = "This is the FIRST file content";
    let content2 = "This is the SECOND file content";

    fs::write(&file1, content1).unwrap();
    fs::write(&file2, content2).unwrap();

    // Open the first file directly
    harness.open_file(&file1).unwrap();
    let screen1 = harness.screen_to_string();
    println!("Screen after opening first file:\n{screen1}");

    // Verify first file content is displayed on screen
    assert!(
        screen1.contains(content1),
        "First file content should be visible on screen after opening"
    );

    // Now open file explorer with Ctrl+E
    harness
        .send_key(KeyCode::Char('e'), KeyModifiers::CONTROL)
        .unwrap();
    harness.sleep(std::time::Duration::from_millis(100));
    let _ = harness.editor_mut().process_async_messages();
    harness.render().unwrap();

    // Wait for async file system operations
    harness.sleep(std::time::Duration::from_millis(100));
    let _ = harness.editor_mut().process_async_messages();
    harness.render().unwrap();

    // Expand the root directory with Alt+L
    harness
        .send_key(KeyCode::Char('l'), KeyModifiers::ALT)
        .unwrap();
    harness.sleep(std::time::Duration::from_millis(100));
    let _ = harness.editor_mut().process_async_messages();
    harness.render().unwrap();

    // Navigate down to find second.txt with Alt+J
    // We need to find it in the list (first.txt comes before second.txt alphabetically)
    for _ in 0..3 {
        harness
            .send_key(KeyCode::Char('j'), KeyModifiers::ALT)
            .unwrap();
    }
    harness.render().unwrap();

    let screen_before_open = harness.screen_to_string();
    println!("Screen before opening second file:\n{screen_before_open}");

    // Open the selected file from file explorer with Alt+Enter
    let result = harness.send_key(KeyCode::Enter, KeyModifiers::ALT);
    assert!(result.is_ok(), "Failed to send Alt+Enter: {result:?}");

    harness.sleep(std::time::Duration::from_millis(50));
    harness.render().unwrap();

    let screen_after_open = harness.screen_to_string();
    println!("Screen after opening second file:\n{screen_after_open}");

    // The critical assertion: the screen should now show the second file's content
    // NOT the first file's content
    assert!(
        screen_after_open.contains(content2),
        "Second file content should be visible on screen after opening from file explorer.\nScreen:\n{screen_after_open}"
    );

    assert!(
        !screen_after_open.contains(content1),
        "First file content should NOT be visible anymore after opening second file.\nScreen:\n{screen_after_open}"
    );
}

/// Test that file_explorer_toggle_hidden can be called (smoke test)
#[test]
fn test_file_explorer_toggle_hidden_smoke() {
    let mut harness = EditorTestHarness::new(120, 40).unwrap();

    // Toggle file explorer on
    harness.editor_mut().toggle_file_explorer();
    harness.sleep(std::time::Duration::from_millis(100));
    let _ = harness.editor_mut().process_async_messages();
    harness.sleep(std::time::Duration::from_millis(100));
    let _ = harness.editor_mut().process_async_messages();
    harness.render().unwrap();

    // Call toggle_hidden - should not panic
    harness.editor_mut().file_explorer_toggle_hidden();
    harness.render().unwrap();

    // Call again to toggle back
    harness.editor_mut().file_explorer_toggle_hidden();
    harness.render().unwrap();

    // Test passes if no panic occurs
}

/// Regression test for issue #2073: file explorer should sort
/// filenames naturally — `chapter-2.md` before `chapter-10.md`, not
/// after.
#[test]
fn test_file_explorer_natural_sort_order() {
    let mut harness = EditorTestHarness::with_temp_project(120, 40).unwrap();
    let project_root = harness.project_dir().unwrap();

    for name in &[
        "chapter-1.md",
        "chapter-2.md",
        "chapter-10.md",
        "chapter-11.md",
        "chapter-3.md",
    ] {
        fs::write(project_root.join(name), "x").unwrap();
    }

    harness.editor_mut().focus_file_explorer();
    harness.wait_for_file_explorer().unwrap();
    harness.wait_for_file_explorer_item("chapter-1.md").unwrap();
    harness.render().unwrap();

    let screen = harness.screen_to_string();

    let order = [
        "chapter-1.md",
        "chapter-2.md",
        "chapter-3.md",
        "chapter-10.md",
        "chapter-11.md",
    ];
    let positions: Vec<_> = order
        .iter()
        .map(|name| {
            screen
                .find(name)
                .unwrap_or_else(|| panic!("missing {name} in explorer screen:\n{screen}"))
        })
        .collect();

    for window in positions.windows(2) {
        assert!(
            window[0] < window[1],
            "Files should appear in natural order. positions={:?}\nScreen:\n{}",
            positions,
            screen
        );
    }
}

/// Test that file_explorer_toggle_gitignored can be called (smoke test)
#[test]
fn test_file_explorer_toggle_gitignored_smoke() {
    let mut harness = EditorTestHarness::new(120, 40).unwrap();

    // Toggle file explorer on
    harness.editor_mut().toggle_file_explorer();
    harness.sleep(std::time::Duration::from_millis(100));
    let _ = harness.editor_mut().process_async_messages();
    harness.sleep(std::time::Duration::from_millis(100));
    let _ = harness.editor_mut().process_async_messages();
    harness.render().unwrap();

    // Call toggle_gitignored - should not panic
    harness.editor_mut().file_explorer_toggle_gitignored();
    harness.render().unwrap();

    // Call again to toggle back
    harness.editor_mut().file_explorer_toggle_gitignored();
    harness.render().unwrap();

    // Test passes if no panic occurs
}

/// Test that git status decorations show in the file explorer
#[test]
#[cfg_attr(windows, ignore)] // Git plugin tests are flaky on Windows CI
fn test_file_explorer_git_change_indicator() {
    let repo = GitTestRepo::new();
    repo.setup_git_explorer_plugin();
    repo.create_file("changed.txt", "one");
    repo.create_file("subdir/child.txt", "alpha");
    repo.git_add_all();
    repo.git_commit("Initial commit");

    fs::write(repo.path.join("changed.txt"), "two").unwrap();
    fs::write(repo.path.join("subdir/child.txt"), "beta").unwrap();

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

    harness.editor_mut().toggle_file_explorer();
    harness
        .wait_until(|h| h.screen_to_string().contains("File Explorer"))
        .unwrap();

    harness
        .wait_until(|h| {
            let screen = h.screen_to_string();
            // Status indicator is now right-aligned, so look for both on same line
            screen
                .lines()
                .any(|line| line.contains("changed.txt") && line.contains("M"))
        })
        .unwrap();

    harness
        .wait_until(|h| {
            let screen = h.screen_to_string();
            screen
                .lines()
                .any(|line| line.contains("subdir") && line.contains("●"))
        })
        .unwrap();
}

/// Test that file_explorer_new_file can be called (smoke test)
#[test]
fn test_file_explorer_new_file_smoke() {
    // Create harness with isolated temp project
    let mut harness = EditorTestHarness::with_temp_project(120, 40).unwrap();

    // Toggle file explorer on
    harness.editor_mut().toggle_file_explorer();
    harness.sleep(std::time::Duration::from_millis(100));
    let _ = harness.editor_mut().process_async_messages();
    harness.sleep(std::time::Duration::from_millis(100));
    let _ = harness.editor_mut().process_async_messages();
    harness.render().unwrap();

    // Call new_file - should not panic (actual file creation depends on runtime)
    harness.editor_mut().file_explorer_new_file();
    harness.sleep(std::time::Duration::from_millis(100));
    harness.render().unwrap();

    // Test passes if no panic occurs
}

/// Test that file_explorer_new_directory can be called (smoke test)
#[test]
fn test_file_explorer_new_directory_smoke() {
    // Create harness with isolated temp project
    let mut harness = EditorTestHarness::with_temp_project(120, 40).unwrap();

    // Toggle file explorer on
    harness.editor_mut().toggle_file_explorer();
    harness.sleep(std::time::Duration::from_millis(100));
    let _ = harness.editor_mut().process_async_messages();
    harness.sleep(std::time::Duration::from_millis(100));
    let _ = harness.editor_mut().process_async_messages();
    harness.render().unwrap();

    // Call new_directory - should not panic (actual creation depends on runtime)
    harness.editor_mut().file_explorer_new_directory();
    harness.sleep(std::time::Duration::from_millis(100));
    harness.render().unwrap();

    // Test passes if no panic occurs
}

/// Test that file_explorer_delete can be called (smoke test)
#[test]
fn test_file_explorer_delete_smoke() {
    // Create harness with isolated temp project
    let mut harness = EditorTestHarness::with_temp_project(120, 40).unwrap();
    let project_root = harness.project_dir().unwrap();

    // Create a test file
    fs::write(project_root.join("test.txt"), "test").unwrap();

    // Open and focus file explorer
    harness.editor_mut().focus_file_explorer();
    harness.wait_for_file_explorer().unwrap();

    // Root is auto-expanded during init, wait for file to appear
    harness.wait_for_file_explorer_item("test.txt").unwrap();

    // Navigate to the file using Down key (user-facing action)
    harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
    harness.render().unwrap();

    // Call delete using Delete key (user-facing action)
    harness
        .send_key(KeyCode::Delete, KeyModifiers::NONE)
        .unwrap();
    harness.sleep(std::time::Duration::from_millis(100));
    harness.render().unwrap();

    // Test passes if no panic occurs
}

/// Test that focus returns to file explorer after confirming file deletion
#[test]
fn test_file_explorer_focus_after_delete() {
    let mut harness = EditorTestHarness::with_temp_project(120, 40).unwrap();
    let project_root = harness.project_dir().unwrap();

    // Create multiple test files
    fs::write(project_root.join("file1.txt"), "content 1").unwrap();
    fs::write(project_root.join("file2.txt"), "content 2").unwrap();

    // Open and focus file explorer
    harness.editor_mut().focus_file_explorer();
    harness.wait_for_file_explorer().unwrap();
    harness.wait_for_file_explorer_item("file1.txt").unwrap();

    // Verify we're in file explorer context
    let key_context_before = harness.editor().get_key_context();
    println!("Key context before deletion: {:?}", key_context_before);
    assert!(
        matches!(
            key_context_before,
            fresh::input::keybindings::KeyContext::FileExplorer
        ),
        "Should be in FileExplorer context before deletion"
    );

    // Navigate to file1.txt
    harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
    harness.render().unwrap();

    let screen_before = harness.screen_to_string();
    println!("Screen before deletion:\n{}", screen_before);

    // Initiate deletion using the method directly (since Delete key isn't bound by default)
    harness.editor_mut().file_explorer_delete();
    harness.render().unwrap();

    let screen_prompt = harness.screen_to_string();
    println!("Screen with delete prompt:\n{}", screen_prompt);

    // Should see confirmation prompt
    assert!(
        screen_prompt.contains("Delete") || screen_prompt.contains("delete"),
        "Should show delete confirmation prompt. Screen:\n{}",
        screen_prompt
    );

    // Confirm deletion with 'y'
    harness.type_text("y").unwrap();
    harness
        .send_key(KeyCode::Enter, KeyModifiers::NONE)
        .unwrap();
    harness.sleep(std::time::Duration::from_millis(100));
    harness.render().unwrap();

    let screen_after = harness.screen_to_string();
    println!("Screen after deletion:\n{}", screen_after);

    // Check that focus is back to file explorer
    let key_context_after = harness.editor().get_key_context();
    println!("Key context after deletion: {:?}", key_context_after);

    // The critical assertion: focus should be on file explorer after deletion
    assert!(
        matches!(
            key_context_after,
            fresh::input::keybindings::KeyContext::FileExplorer
        ),
        "Should be in FileExplorer context after deletion. Got: {:?}",
        key_context_after
    );

    // Verify file explorer is still visible
    assert!(
        screen_after.contains("File Explorer"),
        "File Explorer should still be visible after deletion"
    );

    // Verify the deleted file is gone from the file explorer tree
    // (but it may appear in status messages like "Moved to trash: file1.txt")
    // Check that file2.txt is visible but file1.txt tree entry is gone
    // Note: file1.txt might still appear in status message, so check tree area specifically
    assert!(
        screen_after.contains("file2.txt"),
        "File explorer should still show file2.txt after deletion. Screen:\n{}",
        screen_after
    );

    // Verify arrow keys work in file explorer (not captured by editor)
    harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
    harness.render().unwrap();
    // If we can navigate without error, the focus is correctly on file explorer
}

/// Test Feature 1: Enter key on directory toggles expand/collapse
#[test]
fn test_enter_toggles_directory() {
    let mut harness = EditorTestHarness::with_temp_project(120, 40).unwrap();
    let project_root = harness.project_dir().unwrap();

    // Create a directory with files
    fs::create_dir(project_root.join("testdir")).unwrap();
    fs::write(project_root.join("testdir/file1.txt"), "content1").unwrap();
    fs::write(project_root.join("testdir/file2.txt"), "content2").unwrap();

    // Open file explorer
    harness.editor_mut().focus_file_explorer();
    harness.wait_for_file_explorer().unwrap();

    // Root should already be expanded (Feature 3), navigate to testdir
    harness
        .send_key(KeyCode::Down, KeyModifiers::empty())
        .unwrap();
    harness.render().unwrap();

    let screen_before_expand = harness.screen_to_string();
    println!("Before expand:\n{}", screen_before_expand);
    let testdir_line_before = screen_before_expand
        .lines()
        .find(|line| line.contains("testdir"))
        .unwrap_or("");

    // Should be on testdir now - verify it's collapsed
    assert!(
        testdir_line_before.contains('>') && testdir_line_before.contains("testdir"),
        "testdir should initially be collapsed.\nLine: {testdir_line_before:?}\nScreen:\n{screen_before_expand}"
    );

    // Press Enter to expand testdir
    harness
        .send_key(KeyCode::Enter, KeyModifiers::empty())
        .unwrap();
    harness.wait_for_file_explorer_item("file1.txt").unwrap();

    let screen_after_expand = harness.screen_to_string();
    println!("After expand:\n{}", screen_after_expand);
    let testdir_line_after = screen_after_expand
        .lines()
        .find(|line| line.contains("testdir"))
        .unwrap_or("");

    // After expansion, should see the files inside testdir (file1.txt, file2.txt)
    assert!(
        screen_after_expand.contains("file1.txt") || screen_after_expand.contains("file2.txt"),
        "Should see files inside testdir after expansion"
    );

    // Verify testdir is now expanded
    assert!(
        testdir_line_after.contains('▼') && testdir_line_after.contains("testdir"),
        "testdir should show expanded indicator (▼).\nLine: {testdir_line_after:?}\nScreen:\n{screen_after_expand}"
    );

    // Press Enter again to collapse testdir
    harness
        .send_key(KeyCode::Enter, KeyModifiers::empty())
        .unwrap();
    // Wait for the directory to collapse (files should be gone)
    harness
        .wait_until(|h| !h.screen_to_string().contains("file1.txt"))
        .unwrap();

    let screen_after_collapse = harness.screen_to_string();

    println!("Screen after collapse:\n{}", screen_after_collapse);
    let testdir_line_after_collapse = screen_after_collapse
        .lines()
        .find(|line| line.contains("testdir"))
        .unwrap_or("");

    // After collapsing, directory tree structure should return to original state
    // We check that testdir shows collapsed indicator (>)
    assert!(
        testdir_line_after_collapse.contains('>') && testdir_line_after_collapse.contains("testdir"),
        "testdir should be collapsed after pressing Enter again.\nLine: {testdir_line_after_collapse:?}\nScreen:\n{screen_after_collapse}"
    );

    // Verify files inside testdir are no longer visible
    assert!(
        !screen_after_collapse.contains("file1.txt")
            && !screen_after_collapse.contains("file2.txt"),
        "Files inside testdir should not be visible when collapsed"
    );
}

/// Test Feature 2: Enter key on file opens it and switches focus to editor
#[test]
fn test_enter_opens_file_and_switches_focus() {
    let mut harness = EditorTestHarness::with_temp_project(120, 40).unwrap();
    let project_root = harness.project_dir().unwrap();

    // Create a test file with distinctive content
    let test_content = "Feature 2: Enter opens file and switches focus";
    fs::write(project_root.join("testfile.txt"), test_content).unwrap();

    // Open file explorer (should have focus)
    harness.editor_mut().focus_file_explorer();
    harness.wait_for_file_explorer().unwrap();

    // Root directory should already be expanded (Feature 3)
    // Navigate down to the file (testfile.txt)
    harness
        .send_key(KeyCode::Down, KeyModifiers::empty())
        .unwrap();
    harness.render().unwrap();

    let screen_before = harness.screen_to_string();
    println!("Before opening file:\n{}", screen_before);

    // Verify we're on the test file
    // The selected item should be visible in the file explorer
    assert!(
        screen_before.contains("testfile.txt"),
        "testfile.txt should be visible in file explorer"
    );

    // File explorer should be visible and have focus
    assert!(
        screen_before.contains("File Explorer"),
        "File explorer should be visible"
    );

    // Press Enter on the file
    harness
        .send_key(KeyCode::Enter, KeyModifiers::empty())
        .unwrap();
    harness.wait_for_screen_contains(test_content).unwrap();

    let screen_after = harness.screen_to_string();

    // File content should be visible in the editor
    assert!(
        screen_after.contains(test_content),
        "File content should be displayed in editor after pressing Enter"
    );

    // Verify focus switched to editor by checking that arrow keys now move cursor
    // in the editor (not file explorer). We can test this by sending a Down key
    // and checking if editor content area changed (cursor moved)
    harness
        .send_key(KeyCode::Right, KeyModifiers::empty())
        .unwrap();
    harness.render().unwrap();

    let screen_after_movement = harness.screen_to_string();

    // After moving right, the cursor column should have changed in the status bar
    // The screen should show cursor position changed
    assert_ne!(
        screen_after, screen_after_movement,
        "Arrow keys should move cursor in editor after opening file (focus should be on editor)"
    );
}

/// Test Feature 3: Project directory should be expanded when file explorer first opens
#[test]
#[ignore]
fn test_project_directory_expanded_on_open() {
    let mut harness = EditorTestHarness::with_temp_project(120, 40).unwrap();
    let project_root = harness.project_dir().unwrap();

    // Create some files in the project root
    fs::write(project_root.join("file1.txt"), "content1").unwrap();
    fs::write(project_root.join("file2.txt"), "content2").unwrap();
    fs::create_dir(project_root.join("subdir")).unwrap();

    // Open file explorer for the first time
    harness.editor_mut().focus_file_explorer();
    harness.sleep(std::time::Duration::from_millis(100));
    let _ = harness.editor_mut().process_async_messages();
    harness.sleep(std::time::Duration::from_millis(100));
    let _ = harness.editor_mut().process_async_messages();
    harness.render().unwrap();

    let screen = harness.screen_to_string();

    // Root directory should be expanded (show ▼ not >)
    assert!(
        screen.contains("▼"),
        "Root directory should be expanded on initial open"
    );

    // Should see files/directories under root
    assert!(
        screen.contains("file1.txt") || screen.contains("file2.txt") || screen.contains("subdir"),
        "Should see files and directories under root when initially opened"
    );

    // Verify we see multiple entries (more than just the root)
    // Count lines that contain file/directory names (have indentation and text)
    let entry_count = screen
        .lines()
        .filter(|l| {
            (l.contains("file1.txt")
                || l.contains("file2.txt")
                || l.contains("subdir")
                || l.contains("project_root"))
                && (l.contains("▼") || l.contains(">") || l.contains("  "))
        })
        .count();

    assert!(
        entry_count > 1,
        "Should see more than just the root directory (found {} entries)",
        entry_count
    );
}

/// Test Feature 4: No [D][T] indicators, only show indicators for unsaved changes
#[test]
fn test_unsaved_change_indicators() {
    let mut harness = EditorTestHarness::with_temp_project(120, 40).unwrap();
    let project_root = harness.project_dir().unwrap();

    // Create a test file
    fs::write(project_root.join("test.txt"), "original content").unwrap();
    fs::write(project_root.join("test.rs"), "fn main() {}").unwrap();
    fs::create_dir(project_root.join("mydir")).unwrap();

    // Open file explorer
    harness.editor_mut().focus_file_explorer();
    harness.wait_for_file_explorer().unwrap();

    let screen_initial = harness.screen_to_string();

    // Should NOT see [D], [T], [F], [R], [P] etc. indicators anymore
    assert!(
        !screen_initial.contains("[D]")
            && !screen_initial.contains("[T]")
            && !screen_initial.contains("[F]")
            && !screen_initial.contains("[R]")
            && !screen_initial.contains("[P]"),
        "Should not show file type indicators like [D], [T], [F], [R], [P] in file explorer"
    );

    // Open a file and modify it without saving
    // Navigate past mydir and other items to test.txt
    // Press down multiple times to get to a file (not directory)
    for _ in 0..3 {
        harness
            .send_key(KeyCode::Down, KeyModifiers::empty())
            .unwrap();
        harness.render().unwrap();
    }

    let screen_before_open = harness.screen_to_string();
    println!("Before opening file:\n{}", screen_before_open);

    // Open the selected file
    harness
        .send_key(KeyCode::Enter, KeyModifiers::empty())
        .unwrap();
    // Wait for file content to load
    harness
        .wait_until(|h| {
            let s = h.screen_to_string();
            s.contains("original content") || s.contains("fn main")
        })
        .unwrap();

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

    // Verify we're actually in the editor with file content
    assert!(
        screen_after_open.contains("original content") || screen_after_open.contains("fn main"),
        "Should have opened a file and see its content"
    );

    // Now in editor - type something to make changes
    harness
        .send_key(KeyCode::Char('X'), KeyModifiers::empty())
        .unwrap();
    harness.render().unwrap();

    // Go back to file explorer
    harness.editor_mut().focus_file_explorer();
    harness.wait_for_file_explorer().unwrap();

    let screen_with_unsaved = harness.screen_to_string();

    println!("Screen with unsaved changes:\n{}", screen_with_unsaved);
    println!(
        "File explorer visible: {}",
        harness.editor().file_explorer_visible()
    );

    // Should now see an unsaved change indicator (●) next to test.txt
    assert!(
        screen_with_unsaved.contains("●") || screen_with_unsaved.contains("*"),
        "Should show unsaved change indicator next to modified file. Screen:\n{}",
        screen_with_unsaved
    );

    // test.rs should not have an indicator
    // We can verify by checking the lines containing the filenames
    let test_txt_line = screen_with_unsaved
        .lines()
        .find(|l| l.contains("test.txt"))
        .unwrap_or("");
    let test_rs_line = screen_with_unsaved
        .lines()
        .find(|l| l.contains("test.rs"))
        .unwrap_or("");

    assert!(
        test_txt_line.contains("●") || test_txt_line.contains("*"),
        "test.txt should have unsaved indicator"
    );
    assert!(
        !test_rs_line.contains("●") && !test_rs_line.contains("*"),
        "test.rs should not have unsaved indicator"
    );
}

/// Test Feature 5: Cursor should reach top before scrolling up (like it does for down)
#[test]
fn test_scroll_allows_cursor_to_top() {
    let mut harness = EditorTestHarness::with_temp_project(120, 10).unwrap(); // Small height to force scrolling
    let project_root = harness.project_dir().unwrap();

    // Create many files to force scrolling (need more than viewport height)
    for i in 0..25 {
        fs::write(
            project_root.join(format!("file{:02}.txt", i)),
            format!("content {}", i),
        )
        .unwrap();
    }

    // Open file explorer and wait for it to be ready with files loaded
    harness.editor_mut().focus_file_explorer();
    harness.wait_for_file_explorer().unwrap();
    // Wait for files to be loaded (file00.txt should appear)
    harness
        .wait_until(|h| h.screen_to_string().contains("file00.txt"))
        .unwrap();

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

    // Navigate down to the bottom of the list
    // This will cause the explorer to scroll down
    for _ in 0..25 {
        harness
            .send_key(KeyCode::Down, KeyModifiers::empty())
            .unwrap();
        harness.render().unwrap();
    }

    let screen_at_bottom = harness.screen_to_string();
    println!("Screen at bottom (scrolled down):\n{}", screen_at_bottom);
    assert!(
        screen_at_bottom
            .lines()
            .any(|line| line.starts_with('│') && line.contains("project_root")),
        "the project root should remain pinned while its children scroll"
    );

    // Now we're at the bottom and the view has scrolled down.
    // The test: when we press Up, the cursor should move WITHIN the viewport
    // for (visible ordinary rows - 1) times before the view scrolls. Sticky
    // ancestor rows (the project root here) intentionally consume part of
    // the viewport and are not included in `get_visible_files`.

    // Track which files are visible to detect scrolling. Only scan rows
    // that start with the explorer's left border so the tab bar and
    // status bar (which reflect the current preview buffer and would
    // change with every cursor move) don't leak into the viewport check.
    let get_visible_files = |screen: &str| -> Vec<String> {
        screen
            .lines()
            .filter(|line| line.starts_with('│'))
            .filter_map(|line| {
                for word in line.split_whitespace() {
                    if word.starts_with("file") && word.ends_with(".txt") {
                        return Some(word.to_string());
                    }
                }
                None
            })
            .collect()
    };

    let initial_visible = get_visible_files(&screen_at_bottom);
    println!("Initially visible files: {:?}", initial_visible);

    // Press Up while there are still earlier ordinary rows in the viewport.
    // The visible files should stay the same (no scrolling yet)
    for i in 0..initial_visible.len().saturating_sub(1) {
        harness
            .send_key(KeyCode::Up, KeyModifiers::empty())
            .unwrap();
        harness.render().unwrap();

        let screen_after_up = harness.screen_to_string();
        let visible_after_up = get_visible_files(&screen_after_up);

        println!("\nAfter {} up presses:", i + 1);
        println!("Visible files: {:?}", visible_after_up);

        // Within the viewport, the same files should still be visible
        // (cursor is moving, but view isn't scrolling)
        assert_eq!(
            initial_visible, visible_after_up,
            "After {} up presses, viewport should not have scrolled yet (cursor should move within viewport first). Initial: {:?}, After: {:?}",
            i + 1,
            initial_visible,
            visible_after_up
        );
    }

    // Now press Up one more time - THIS should cause scrolling
    // because the cursor should now be at the top of the viewport
    harness
        .send_key(KeyCode::Up, KeyModifiers::empty())
        .unwrap();
    harness.render().unwrap();

    let screen_after_scroll = harness.screen_to_string();
    let visible_after_scroll = get_visible_files(&screen_after_scroll);

    println!("\nAfter scrolling up:");
    println!("Visible files: {:?}", visible_after_scroll);

    // After this press, the view SHOULD have scrolled (different files visible)
    assert_ne!(
        initial_visible, visible_after_scroll,
        "After cursor reaches top of viewport, the next up should scroll the view. Initial: {:?}, After scroll: {:?}",
        initial_visible,
        visible_after_scroll
    );
}

/// Test Feature 6: Editor tabs should be above editor area only, not above file explorer
#[test]
fn test_tabs_above_editor_area_only() {
    let mut harness = EditorTestHarness::with_temp_project(120, 40).unwrap();
    let project_root = harness.project_dir().unwrap();

    // Create and open multiple files to have tabs
    fs::write(project_root.join("file1.txt"), "content1").unwrap();
    fs::write(project_root.join("file2.txt"), "content2").unwrap();

    // Open first file
    harness
        .editor_mut()
        .open_file(&project_root.join("file1.txt"))
        .unwrap();
    harness.render().unwrap();

    // Open second file
    harness
        .editor_mut()
        .open_file(&project_root.join("file2.txt"))
        .unwrap();
    harness.render().unwrap();

    // Open file explorer
    harness.editor_mut().focus_file_explorer();
    harness.wait_for_file_explorer().unwrap();

    let screen = harness.screen_to_string();
    println!("Screen with file explorer and tabs:\n{}", screen);

    // The tabs should be above the editor area, not spanning the full width
    // We can verify this by checking that the file explorer border and tabs
    // are on the same line or the tabs start after the file explorer width

    let lines: Vec<&str> = screen.lines().collect();

    // Find the line with File Explorer title
    let explorer_line_idx = lines.iter().position(|l| l.contains("File Explorer"));

    // Find lines with tab content (file1.txt, file2.txt)
    let tab_line_idx = lines
        .iter()
        .position(|l| l.contains("file1.txt") && l.contains("file2.txt"));

    if let (Some(explorer_idx), Some(tab_idx)) = (explorer_line_idx, tab_line_idx) {
        // Tabs and file explorer should be on the same line (line 0)
        // The tab line should contain both the explorer border and the tabs
        let tab_line = lines.get(tab_idx).unwrap_or(&"");

        println!(
            "Tab line index: {}, Explorer line index: {}",
            tab_idx, explorer_idx
        );
        println!("Tab line: '{}'", tab_line);

        // The critical check: tabs should be on the same line as file explorer header
        // This means they're only above the editor area, not spanning full width
        assert_eq!(
            tab_idx, explorer_idx,
            "Tabs and File Explorer should be on the same line (tabs above editor area only)"
        );

        // The line should contain both file explorer and tabs
        assert!(
            tab_line.contains("File Explorer") && tab_line.contains("file1.txt"),
            "Tab line should contain both file explorer and tab content"
        );
    } else {
        panic!("Could not find both file explorer and tabs in output");
    }
}

/// Test Feature 7: Auto-expand and select file on focus switch
#[test]
fn test_auto_select_file_on_focus_switch() {
    let mut harness = EditorTestHarness::with_temp_project(120, 40).unwrap();
    let project_root = harness.project_dir().unwrap();

    // Create files in nested directories
    fs::create_dir_all(project_root.join("src/components")).unwrap();
    fs::write(project_root.join("src/components/App.js"), "app content").unwrap();
    fs::write(project_root.join("src/index.js"), "index content").unwrap();
    fs::write(project_root.join("README.md"), "readme").unwrap();

    // Open file explorer
    harness.editor_mut().focus_file_explorer();
    harness.wait_for_file_explorer().unwrap();

    // Open a deeply nested file
    harness
        .editor_mut()
        .open_file(&project_root.join("src/components/App.js"))
        .unwrap();
    harness.render().unwrap();

    // Switch focus to file explorer
    harness.editor_mut().focus_file_explorer();
    // Wait for file explorer to show App.js (auto-expand to currently edited file)
    harness.wait_for_file_explorer_item("App.js").unwrap();

    let screen = harness.screen_to_string();
    println!("Screen after opening nested file:\n{}", screen);

    // The file explorer should have auto-expanded to show App.js
    // Check that src is expanded (▼ not >)
    let lines: Vec<&str> = screen.lines().collect();
    let src_line = lines.iter().find(|l| l.contains("src")).unwrap_or(&"");

    println!("src line: '{}'", src_line);

    assert!(
        src_line.contains("▼") || src_line.contains("▼  src"),
        "src directory should be expanded (▼). Line: {}",
        src_line
    );

    // Should see components directory in the tree
    assert!(
        screen.contains("components"),
        "Should see components directory in file explorer tree"
    );

    // Should see App.js in the file explorer tree (not just in tabs)
    let app_line = lines
        .iter()
        .find(|l| l.contains("App.js") && l.contains("│"))
        .unwrap_or(&"");
    assert!(
        !app_line.is_empty() && app_line.contains("│"),
        "App.js should be visible in file explorer tree. Found line: {}",
        app_line
    );

    // App.js should be selected (we can't easily verify selection visually,
    // but we can check it's visible which means path was expanded)
    // For a more robust test, we could check the internal state
    // but for e2e, visibility is a good proxy

    // Now open a different file and switch focus again
    harness.editor_mut().active_window_mut().focus_editor();
    harness
        .editor_mut()
        .open_file(&project_root.join("README.md"))
        .unwrap();
    harness.render().unwrap();

    // Switch focus back to file explorer
    harness.editor_mut().focus_file_explorer();
    harness.wait_for_file_explorer_item("README.md").unwrap();

    let screen2 = harness.screen_to_string();
    println!("Screen after switching to README.md:\n{}", screen2);

    // Should now show README.md (which is at root level)
    assert!(
        screen2.contains("README.md"),
        "File explorer should show README.md after switching focus"
    );
}

/// Test bug: Explorer sync fails after hide -> tab switch -> show
#[test]
fn test_file_explorer_sync_after_hide_and_tab_switch() {
    let mut harness = EditorTestHarness::with_temp_project(120, 40).unwrap();
    let project_root = harness.project_dir().unwrap();

    // Create two files
    fs::write(project_root.join("file1.txt"), "content 1").unwrap();
    fs::write(project_root.join("file2.txt"), "content 2").unwrap();

    // Open file explorer
    harness.editor_mut().focus_file_explorer();
    harness.wait_for_file_explorer().unwrap();

    // Open file1.txt
    harness
        .editor_mut()
        .open_file(&project_root.join("file1.txt"))
        .unwrap();
    harness.render().unwrap();

    // Open file2.txt (should auto-sync explorer to file2)
    harness
        .editor_mut()
        .open_file(&project_root.join("file2.txt"))
        .unwrap();
    harness.render().unwrap();

    // Close (hide) the file explorer
    harness.editor_mut().toggle_file_explorer();
    harness.render().unwrap();

    harness
        .wait_until(|h| !h.screen_to_string().contains("File Explorer"))
        .unwrap();

    // Switch to file1.txt (while explorer is hidden)
    harness.editor_mut().prev_buffer();
    harness.render().unwrap();

    // Verify we're on file1.txt
    harness
        .wait_until(|h| h.screen_to_string().contains("file1.txt"))
        .unwrap();

    // Re-open the file explorer
    harness.editor_mut().toggle_file_explorer();
    harness.wait_for_file_explorer().unwrap();

    // Wait for file explorer to sync to file1.txt
    harness
        .wait_until(|h| {
            h.editor()
                .file_explorer()
                .and_then(|e| e.get_selected_entry())
                .map(|e| e.name.as_str() == "file1.txt")
                .unwrap_or(false)
        })
        .unwrap();
}

/// Test that file explorer shows the keybinding for toggling it (or just the title if no binding)
#[test]
fn test_file_explorer_shows_keybinding_in_title() {
    let mut harness = EditorTestHarness::new(120, 40).unwrap();

    // Toggle file explorer on with Ctrl+E
    harness
        .send_key(KeyCode::Char('e'), KeyModifiers::CONTROL)
        .unwrap();
    harness
        .wait_until(|h| h.screen_to_string().contains("File Explorer"))
        .unwrap();

    let screen = harness.screen_to_string();

    // File explorer should show in the UI
    // If ToggleFileExplorer has a keybinding, it should appear in parentheses
    // Otherwise, just "File Explorer" should appear
    assert!(
        screen.contains("File Explorer"),
        "File explorer title should be visible. Screen:\n{}",
        screen
    );

    // If a keybinding is shown, verify it's in the correct format
    if screen.contains("File Explorer (") {
        // Keybinding format should be correct (e.g., "Ctrl+E", "Ctrl+Shift+B", etc.)
        let has_valid_format = screen.contains("File Explorer (Ctrl+")
            || screen.contains("File Explorer (Alt+")
            || screen.contains("File Explorer (Shift+");
        assert!(
            has_valid_format,
            "File explorer keybinding should be in a valid format. Screen:\n{}",
            screen
        );
    }
}

/// Test that file explorer keybinding is shown when focused (or just title if no binding)
#[test]
fn test_file_explorer_keybinding_when_focused() {
    let mut harness = EditorTestHarness::new(120, 40).unwrap();

    // Toggle file explorer on
    harness
        .send_key(KeyCode::Char('e'), KeyModifiers::CONTROL)
        .unwrap();
    // Wait for file explorer to appear
    harness
        .wait_until(|h| {
            let screen = h.screen_to_string();
            screen.contains("File Explorer")
        })
        .unwrap();

    // Focus the file explorer
    harness
        .send_key(KeyCode::Char('\\'), KeyModifiers::CONTROL)
        .unwrap();
    harness.render().unwrap();

    let screen = harness.screen_to_string();

    // File explorer should be visible when focused
    assert!(
        screen.contains("File Explorer"),
        "File explorer title should be visible when focused. Screen:\n{}",
        screen
    );
}

/// Test that the file explorer can be toggled and the title is present
#[test]
fn test_file_explorer_keybinding_matches_behavior() {
    let mut harness = EditorTestHarness::new(120, 40).unwrap();

    // Toggle file explorer on with Ctrl+E
    harness
        .send_key(KeyCode::Char('e'), KeyModifiers::CONTROL)
        .unwrap();
    // Wait for file explorer to appear
    harness
        .wait_until(|h| {
            let screen = h.screen_to_string();
            screen.contains("File Explorer")
        })
        .unwrap();

    let screen_with_explorer = harness.screen_to_string();

    // File explorer should be visible
    assert!(
        screen_with_explorer.contains("File Explorer"),
        "File explorer title should be visible after toggling on"
    );

    // Toggle it off using the same key
    harness
        .send_key(KeyCode::Char('e'), KeyModifiers::CONTROL)
        .unwrap();
    harness.render().unwrap();

    let screen_without_explorer = harness.screen_to_string();

    // Screen should change when toggling
    assert_ne!(
        screen_with_explorer, screen_without_explorer,
        "File explorer should toggle off when the keybinding is pressed again"
    );
}

/// Test that Ctrl+E toggles focus between file explorer and editor
/// When in editor context, Ctrl+E focuses the file explorer
/// When in file explorer context, Ctrl+E focuses the editor (keeps explorer open)
#[test]
fn test_ctrl_e_toggles_focus_between_explorer_and_editor() {
    let mut harness = EditorTestHarness::with_temp_project(120, 40).unwrap();
    let project_root = harness.project_dir().unwrap();

    // Create a test file
    fs::write(project_root.join("test.txt"), "test content").unwrap();

    // Open the file so we have something in the editor
    harness
        .editor_mut()
        .open_file(&project_root.join("test.txt"))
        .unwrap();
    harness.render().unwrap();

    // Initially, file explorer should not be visible
    assert!(
        !harness.editor().file_explorer_visible(),
        "File explorer should not be visible initially"
    );

    // Press Ctrl+E to open and focus file explorer
    harness
        .send_key(KeyCode::Char('e'), KeyModifiers::CONTROL)
        .unwrap();
    harness
        .wait_until(|h| h.screen_to_string().contains("File Explorer"))
        .unwrap();

    // File explorer should now be visible
    assert!(
        harness.editor().file_explorer_visible(),
        "File explorer should be visible after Ctrl+E"
    );

    let screen_explorer_focused = harness.screen_to_string();
    println!("Screen with explorer focused:\n{}", screen_explorer_focused);

    // Press Ctrl+E again - should switch focus back to editor but keep explorer open
    harness
        .send_key(KeyCode::Char('e'), KeyModifiers::CONTROL)
        .unwrap();
    harness.render().unwrap();

    // File explorer should STILL be visible (not toggled off)
    assert!(
        harness.editor().file_explorer_visible(),
        "File explorer should remain visible after Ctrl+E from explorer context"
    );

    let screen_editor_focused = harness.screen_to_string();
    println!("Screen with editor focused:\n{}", screen_editor_focused);

    // Verify focus is on editor by typing and seeing it appear in the buffer
    harness.type_text("TYPED").unwrap();
    harness.render().unwrap();

    let screen_after_typing = harness.screen_to_string();
    assert!(
        screen_after_typing.contains("TYPED"),
        "Typing should work after Ctrl+E switches focus to editor. Screen:\n{}",
        screen_after_typing
    );

    // File explorer should still be visible
    assert!(
        screen_after_typing.contains("File Explorer"),
        "File explorer should still be visible after typing in editor"
    );
}

/// Test that clicking on empty area in file explorer then clicking on editor allows typing
/// Bug: When clicking on empty area in file explorer (below files), the key_context is set
/// to FileExplorer. When clicking back on the editor, the key_context was not being reset
/// to Normal, so typing would have "No binding found" and do nothing.
#[test]
fn test_click_empty_explorer_area_then_editor_allows_typing() {
    use tracing_subscriber::EnvFilter;

    // Initialize tracing
    let _ = tracing_subscriber::fmt()
        .with_env_filter(EnvFilter::from_default_env().add_directive(tracing::Level::DEBUG.into()))
        .with_test_writer()
        .try_init();

    let mut harness = EditorTestHarness::with_temp_project(120, 40).unwrap();
    let project_root = harness.project_dir().unwrap();

    // Create a few files so there's empty space below them in the file explorer
    fs::write(project_root.join("file1.txt"), "content 1").unwrap();
    fs::write(project_root.join("file2.txt"), "content 2").unwrap();

    // Open file explorer
    harness.editor_mut().focus_file_explorer();
    harness.sleep(std::time::Duration::from_millis(100));
    let _ = harness.editor_mut().process_async_messages();
    harness.sleep(std::time::Duration::from_millis(100));
    let _ = harness.editor_mut().process_async_messages();
    harness.render().unwrap();

    // Verify file explorer is visible
    assert!(
        harness.editor().file_explorer_visible(),
        "File explorer should be visible"
    );

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

    // File explorer is on the left side (about 30% width = ~36 columns on 120-width terminal)
    // Click on an empty area below the files in the file explorer
    // The files are near the top, so clicking at row 20+ should be empty area
    let explorer_col = 15; // In the file explorer area
    let empty_row = 25; // Below where files would be displayed

    println!(
        "Clicking empty area in file explorer at ({}, {})",
        explorer_col, empty_row
    );
    harness.mouse_click(explorer_col, empty_row).unwrap();
    harness.render().unwrap();

    // Check key_context after file explorer click
    let key_context_after_explorer = harness.editor().get_key_context();
    println!(
        "Key context after explorer click: {:?}",
        key_context_after_explorer
    );

    // Now click on the editor area (right side of the screen)
    // With file explorer taking ~30% width, editor starts around column 40+
    let editor_col = 70; // Well into the editor area
    let editor_row = 10; // In the content area

    println!("Clicking editor area at ({}, {})", editor_col, editor_row);
    harness.mouse_click(editor_col, editor_row).unwrap();
    harness.render().unwrap();

    // Check key_context after editor click
    let key_context_after_editor = harness.editor().get_key_context();
    println!(
        "Key context after editor click: {:?}",
        key_context_after_editor
    );

    // Get the buffer content before typing
    let content_before = harness.get_buffer_content().unwrap_or_default();
    println!("Buffer content before typing: '{}'", content_before);

    // Now try to type something - this is the key test
    // If key_context is still FileExplorer, this will do nothing (bug)
    // If key_context was properly reset to Normal, this will insert text (fix)
    harness.type_text("TYPED_TEXT").unwrap();
    harness.render().unwrap();

    // Check that the text was actually inserted
    let content_after = harness.get_buffer_content().unwrap_or_default();
    println!("Buffer content after typing: '{}'", content_after);

    // The critical assertion: text should have been inserted
    assert!(
        content_after.contains("TYPED_TEXT"),
        "Typing should work after clicking on empty file explorer area then clicking on editor. \
         Bug: key_context stays as FileExplorer after clicking on empty area, preventing typing. \
         Content before: '{}', Content after: '{}'",
        content_before,
        content_after
    );
}

/// Test that closing the last buffer focuses the file explorer
#[test]
fn test_close_last_buffer_focuses_file_explorer() {
    // Use a fresh editor with no files open initially
    let mut harness = EditorTestHarness::new(120, 40).unwrap();
    harness.render().unwrap();

    // The editor starts with a single scratch buffer [No Name]
    let screen_initial = harness.screen_to_string();
    println!("Initial screen:\n{}", screen_initial);

    // File explorer should not be visible initially
    assert!(
        !harness.editor().file_explorer_visible(),
        "File explorer should not be visible initially"
    );

    // Close the scratch buffer (Alt+W)
    harness
        .send_key(KeyCode::Char('w'), KeyModifiers::ALT)
        .unwrap();

    // Wait for file explorer to be visible AND rendered on screen
    harness
        .wait_until(|h| {
            h.editor().file_explorer_visible() && h.screen_to_string().contains("File Explorer")
        })
        .unwrap();

    let screen_after_close = harness.screen_to_string();
    println!("Screen after closing last buffer:\n{}", screen_after_close);

    // File explorer should now be visible
    assert!(
        harness.editor().file_explorer_visible(),
        "File explorer should be visible after closing last buffer"
    );

    // Should see File Explorer in the screen
    assert!(
        screen_after_close.contains("File Explorer"),
        "File Explorer should be visible on screen after closing last buffer. Screen:\n{}",
        screen_after_close
    );
}

/// Test that folders containing modified files show a modified indicator (●)
/// This tests Issue #526: Show changed file on folder
#[test]
fn test_folder_shows_modified_indicator_for_unsaved_file() {
    let mut harness = EditorTestHarness::with_temp_project(120, 40).unwrap();
    let project_root = harness.project_dir().unwrap();

    // Create a folder with a file inside
    fs::create_dir(project_root.join("src")).unwrap();
    fs::write(project_root.join("src/main.rs"), "fn main() {}").unwrap();
    // Also create a file at root level for comparison
    fs::write(project_root.join("README.md"), "readme").unwrap();

    // Open file explorer first to verify initial state
    harness.editor_mut().focus_file_explorer();
    harness.wait_for_file_explorer().unwrap();

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

    // The src folder should NOT have a modified indicator initially
    // Look for the folder line - it should have "> src" or "▼ src" but NOT "●"
    let src_line_initial = screen_initial
        .lines()
        .find(|l| l.contains("src") && (l.contains(">") || l.contains("▼")))
        .unwrap_or("");
    println!("Initial src line: '{}'", src_line_initial);

    // Expand the src folder to see main.rs
    harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
    harness.render().unwrap();
    harness
        .send_key(KeyCode::Enter, KeyModifiers::NONE)
        .unwrap();
    harness.wait_for_file_explorer_item("main.rs").unwrap();

    // Navigate to main.rs and open it
    harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
    harness.render().unwrap();
    harness
        .send_key(KeyCode::Enter, KeyModifiers::NONE)
        .unwrap();
    harness.wait_for_screen_contains("fn main").unwrap();

    // Now modify the file without saving
    harness.send_key(KeyCode::End, KeyModifiers::NONE).unwrap();
    harness.type_text(" // modified").unwrap();
    harness.render().unwrap();

    // Go back to file explorer
    harness.editor_mut().focus_file_explorer();
    harness.wait_for_file_explorer().unwrap();

    let screen_after_modify = harness.screen_to_string();
    println!("Screen after modification:\n{}", screen_after_modify);

    // Now the src folder SHOULD have a modified indicator (●)
    // The line with "src" should contain "●"
    let src_line_after = screen_after_modify
        .lines()
        .find(|l| l.contains("src") && (l.contains(">") || l.contains("▼")))
        .unwrap_or("");
    println!("After modify src line: '{}'", src_line_after);

    assert!(
        src_line_after.contains("●"),
        "src folder should show modified indicator (●) when it contains a modified file. Line: '{}'",
        src_line_after
    );

    // The main.rs file itself should also have a modified indicator
    // Look for main.rs within the file explorer (lines containing "│" border)
    let main_rs_line = screen_after_modify
        .lines()
        .find(|l| l.contains("main.rs") && l.contains("│"))
        .unwrap_or("");
    // If main.rs is visible in the explorer, it should have the indicator
    // Note: main.rs might be inside the collapsed src folder, so we check if it's visible
    if !main_rs_line.is_empty() {
        assert!(
            main_rs_line.contains("●"),
            "main.rs should show modified indicator (●) in file explorer. Line: '{}'",
            main_rs_line
        );
    }

    // README.md should NOT have a modified indicator (it wasn't modified)
    // Look for README.md within the file explorer (lines containing "│" border)
    let readme_line = screen_after_modify
        .lines()
        .find(|l| l.contains("README.md") && l.contains("│"))
        .unwrap_or("");
    if !readme_line.is_empty() {
        assert!(
            !readme_line.contains("●"),
            "README.md should NOT show modified indicator. Line: '{}'",
            readme_line
        );
    }
}

/// Test that nested folder hierarchy shows modified indicator up the tree
#[test]
fn test_nested_folder_shows_modified_indicator() {
    // Disable compact-directory rendering so `src` and `components` keep
    // their own rows; this test asserts on the per-folder modified
    // indicator propagating up the tree, which the compact-chain
    // breadcrumb (`src/components` on one row) would otherwise collapse.
    let mut config = Config::default();
    config.file_explorer.compact_directories = false;
    let mut harness = EditorTestHarness::with_temp_project_and_config(120, 40, config).unwrap();
    let project_root = harness.project_dir().unwrap();

    // Create a nested folder structure: src/components/Button.js
    fs::create_dir_all(project_root.join("src/components")).unwrap();
    fs::write(
        project_root.join("src/components/Button.js"),
        "export default Button;",
    )
    .unwrap();

    // Open file explorer
    harness.editor_mut().focus_file_explorer();
    harness.wait_for_file_explorer().unwrap();

    // Expand src folder
    harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
    harness
        .send_key(KeyCode::Enter, KeyModifiers::NONE)
        .unwrap();
    harness.wait_for_file_explorer_item("components").unwrap();

    // Expand components folder
    harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
    harness
        .send_key(KeyCode::Enter, KeyModifiers::NONE)
        .unwrap();
    harness.wait_for_file_explorer_item("Button.js").unwrap();

    // Open Button.js
    harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
    harness
        .send_key(KeyCode::Enter, KeyModifiers::NONE)
        .unwrap();
    harness.wait_for_screen_contains("export default").unwrap();

    // Modify the file
    harness.send_key(KeyCode::End, KeyModifiers::NONE).unwrap();
    harness.type_text(" // changed").unwrap();
    harness.render().unwrap();

    // Go back to file explorer
    harness.editor_mut().focus_file_explorer();
    harness.wait_for_file_explorer().unwrap();

    let screen = harness.screen_to_string();
    println!("Screen with nested modification:\n{}", screen);

    // Both src and components folders should show modified indicator
    // Look for lines within the file explorer area (containing "│" border)
    let src_line = screen
        .lines()
        .find(|l| l.contains("src") && !l.contains("components") && l.contains("│"))
        .unwrap_or("");
    let components_line = screen
        .lines()
        .find(|l| l.contains("components") && l.contains("│"))
        .unwrap_or("");
    let button_line = screen
        .lines()
        .find(|l| l.contains("Button.js") && l.contains("│"))
        .unwrap_or("");

    println!("src line: '{}'", src_line);
    println!("components line: '{}'", components_line);
    println!("Button.js line: '{}'", button_line);

    assert!(
        src_line.contains("●"),
        "src folder should show modified indicator (●) for nested modified file. Line: '{}'",
        src_line
    );
    assert!(
        components_line.contains("●"),
        "components folder should show modified indicator (●). Line: '{}'",
        components_line
    );
    // Button.js might be inside the collapsed components folder, so check if visible
    if !button_line.is_empty() {
        assert!(
            button_line.contains("●"),
            "Button.js should show modified indicator (●). Line: '{}'",
            button_line
        );
    }
}

/// Test that folder modified indicator is cleared after saving
#[test]
fn test_folder_modified_indicator_cleared_after_save() {
    let mut harness = EditorTestHarness::with_temp_project(120, 40).unwrap();
    let project_root = harness.project_dir().unwrap();

    // Create a folder with a file
    fs::create_dir(project_root.join("mydir")).unwrap();
    fs::write(project_root.join("mydir/test.txt"), "original").unwrap();

    // Open file explorer and navigate to the file
    harness.editor_mut().focus_file_explorer();
    harness.wait_for_file_explorer().unwrap();

    // Expand mydir
    harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
    harness
        .send_key(KeyCode::Enter, KeyModifiers::NONE)
        .unwrap();
    harness.wait_for_file_explorer_item("test.txt").unwrap();

    // Open test.txt
    harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
    harness
        .send_key(KeyCode::Enter, KeyModifiers::NONE)
        .unwrap();
    harness.wait_for_screen_contains("original").unwrap();

    // Modify the file
    harness.type_text("modified ").unwrap();
    harness.render().unwrap();

    // Go to file explorer and verify modified indicator
    harness.editor_mut().focus_file_explorer();
    harness.wait_for_file_explorer().unwrap();

    let screen_before_save = harness.screen_to_string();
    // Look for mydir within the file explorer area (containing "│" border)
    let mydir_line_before = screen_before_save
        .lines()
        .find(|l| l.contains("mydir") && l.contains("│"))
        .unwrap_or("");
    assert!(
        mydir_line_before.contains("●"),
        "mydir should show modified indicator before save. Line: '{}'",
        mydir_line_before
    );

    // Focus editor and save the file
    harness.editor_mut().active_window_mut().focus_editor();
    harness
        .send_key(KeyCode::Char('s'), KeyModifiers::CONTROL)
        .unwrap();
    harness.render().unwrap();

    // Wait a bit for save to complete
    harness.sleep(std::time::Duration::from_millis(100));

    // Go back to file explorer
    harness.editor_mut().focus_file_explorer();
    harness.wait_for_file_explorer().unwrap();

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

    // Look for mydir within the file explorer area (containing "│" border)
    let mydir_line_after = screen_after_save
        .lines()
        .find(|l| l.contains("mydir") && l.contains("│"))
        .unwrap_or("");
    println!("mydir line after save: '{}'", mydir_line_after);

    // The modified indicator should be gone after saving
    // Note: The folder line should have either "> " or "▼ " but not "●"
    // We check that there's no "●" next to the folder name
    let has_modified_indicator = mydir_line_after.contains("●");
    assert!(
        !has_modified_indicator,
        "mydir should NOT show modified indicator after save. Line: '{}'",
        mydir_line_after
    );
}

/// Test that creating a new file from file explorer opens a rename prompt
/// and opens the file in the buffer
/// Tests:
/// 1. Prompt starts empty (not showing the randomly generated name)
/// 2. After renaming, focus switches to the editor buffer
/// 3. Buffer tab name is synced with the renamed file name
#[test]
fn test_file_explorer_new_file_opens_rename_prompt_and_buffer() {
    let mut harness = EditorTestHarness::with_temp_project(120, 40).unwrap();
    let project_root = harness.project_dir().unwrap();

    // Open file explorer
    harness.editor_mut().focus_file_explorer();
    harness.wait_for_file_explorer().unwrap();

    let screen_before = harness.screen_to_string();
    println!("Screen before new file:\n{}", screen_before);

    // Create new file using Ctrl+n
    harness
        .send_key(KeyCode::Char('n'), KeyModifiers::CONTROL)
        .unwrap();
    harness.render().unwrap();

    let screen_after = harness.screen_to_string();
    println!("Screen after new file:\n{}", screen_after);

    // A prompt should be visible asking for the new file name
    assert!(
        screen_after.contains("New file name:"),
        "A new file name prompt should appear after creating new file. Screen:\n{}",
        screen_after
    );

    // The prompt should start EMPTY (not showing the generated filename)
    // The generated filename (untitled_XXX.txt) should NOT appear in the prompt input area
    // Note: the file IS created on disk with the generated name, but the prompt is empty
    // so user can type the desired name from scratch
    let prompt_line = screen_after
        .lines()
        .find(|l| l.contains("New file name:"))
        .unwrap_or("");
    println!("Prompt line: '{}'", prompt_line);

    // Verify prompt is empty (just "New file name:" without any filename after)
    // The prompt line should end with "New file name:" followed by only whitespace
    assert!(
        prompt_line.trim() == "New file name:" || prompt_line.trim().ends_with("New file name:"),
        "Prompt should start empty (no pre-filled filename). Got: '{}'",
        prompt_line
    );

    // Type the new name (prompt starts empty, so just type directly)
    harness.type_text("my_new_file.rs").unwrap();
    harness.render().unwrap();

    let screen_typing = harness.screen_to_string();
    println!("Screen while typing:\n{}", screen_typing);

    // Confirm the rename
    harness
        .send_key(KeyCode::Enter, KeyModifiers::NONE)
        .unwrap();
    harness.sleep(std::time::Duration::from_millis(100));
    harness.render().unwrap();

    let screen_final = harness.screen_to_string();
    println!("Screen after rename:\n{}", screen_final);

    // Verify 1: The file should be open in the buffer (shown in tabs)
    assert!(
        screen_final.contains("my_new_file.rs"),
        "The new file should be visible in tabs after renaming. Screen:\n{}",
        screen_final
    );

    // Verify 2: Focus should be on the editor (Normal key context), not file explorer
    let key_context = harness.editor().get_key_context();
    assert!(
        matches!(key_context, fresh::input::keybindings::KeyContext::Normal),
        "Focus should be on editor (Normal context) after rename. Got: {:?}",
        key_context
    );

    // Verify 3: The file should exist on disk with the new name
    assert!(
        project_root.join("my_new_file.rs").exists(),
        "The renamed file should exist on disk"
    );

    // Verify 4: Typing should work in the buffer (proves focus is on editor)
    harness.type_text("fn main() {}").unwrap();
    harness.render().unwrap();

    let buffer_content = harness.get_buffer_content().unwrap_or_default();
    assert!(
        buffer_content.contains("fn main() {}"),
        "Should be able to type in the buffer after rename. Content: '{}'",
        buffer_content
    );

    // Verify 5: The old generated filename should NOT exist on disk
    // (it was renamed to my_new_file.rs)
    let untitled_files: Vec<_> = std::fs::read_dir(&project_root)
        .unwrap()
        .filter_map(|e| e.ok())
        .filter(|e| e.file_name().to_string_lossy().starts_with("untitled_"))
        .collect();
    assert!(
        untitled_files.is_empty(),
        "The old generated filename should not exist on disk. Found: {:?}",
        untitled_files
    );
}

/// Test that renaming an existing file from file explorer updates buffer metadata
/// but keeps focus in file explorer (not switching to editor)
#[test]
fn test_file_explorer_rename_existing_file_keeps_focus() {
    let mut harness = EditorTestHarness::with_temp_project(120, 40).unwrap();
    let project_root = harness.project_dir().unwrap();

    // Create a test file
    fs::write(project_root.join("original.txt"), "file content").unwrap();

    // Open the file in the editor
    harness
        .editor_mut()
        .open_file(&project_root.join("original.txt"))
        .unwrap();
    harness.render().unwrap();

    // Verify file is open with correct name in tab
    let screen_with_file = harness.screen_to_string();
    assert!(
        screen_with_file.contains("original.txt"),
        "File should be open in editor. Screen:\n{}",
        screen_with_file
    );

    // Open file explorer and navigate to the file
    harness.editor_mut().focus_file_explorer();
    harness.wait_for_file_explorer().unwrap();
    harness.wait_for_file_explorer_item("original.txt").unwrap();

    // Navigate down to select the file
    harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
    harness.render().unwrap();

    // Verify we're in FileExplorer context
    let key_context_before = harness.editor().get_key_context();
    assert!(
        matches!(
            key_context_before,
            fresh::input::keybindings::KeyContext::FileExplorer
        ),
        "Should be in FileExplorer context before rename. Got: {:?}",
        key_context_before
    );

    // Trigger rename on the existing file (using the file_explorer_rename method)
    harness.editor_mut().file_explorer_rename();
    harness.render().unwrap();

    let screen_rename_prompt = harness.screen_to_string();
    println!("Screen with rename prompt:\n{}", screen_rename_prompt);

    // Rename prompt should appear with the old name pre-filled
    assert!(
        screen_rename_prompt.contains("Rename to:"),
        "Rename prompt should appear. Screen:\n{}",
        screen_rename_prompt
    );
    assert!(
        screen_rename_prompt.contains("original.txt"),
        "Rename prompt should show old name. Screen:\n{}",
        screen_rename_prompt
    );

    // Type new name (clear old name first with Ctrl+A, then type)
    harness
        .send_key(KeyCode::Char('a'), KeyModifiers::CONTROL)
        .unwrap();
    harness.type_text("renamed.txt").unwrap();
    harness
        .send_key(KeyCode::Enter, KeyModifiers::NONE)
        .unwrap();
    harness.sleep(std::time::Duration::from_millis(100));
    harness.render().unwrap();

    let screen_after_rename = harness.screen_to_string();
    println!("Screen after rename:\n{}", screen_after_rename);

    // Verify 1: Buffer tab should show new name
    assert!(
        screen_after_rename.contains("renamed.txt"),
        "Buffer tab should show new name. Screen:\n{}",
        screen_after_rename
    );

    // Verify 2: Focus should STILL be on file explorer (not switched to editor)
    let key_context_after = harness.editor().get_key_context();
    assert!(
        matches!(
            key_context_after,
            fresh::input::keybindings::KeyContext::FileExplorer
        ),
        "Focus should stay on FileExplorer after renaming existing file. Got: {:?}",
        key_context_after
    );

    // Verify 3: File explorer should still be visible
    assert!(
        screen_after_rename.contains("File Explorer"),
        "File explorer should still be visible. Screen:\n{}",
        screen_after_rename
    );

    // Verify 4: File on disk was renamed
    assert!(
        project_root.join("renamed.txt").exists(),
        "File should be renamed on disk"
    );
    assert!(
        !project_root.join("original.txt").exists(),
        "Old file should not exist on disk"
    );
}

/// Test that hovering over file explorer status indicator shows a tooltip
#[test]
#[cfg_attr(windows, ignore)] // Git plugin tests are flaky on Windows CI
fn test_file_explorer_status_indicator_hover_tooltip() {
    let repo = GitTestRepo::new();
    repo.setup_git_explorer_plugin();
    repo.create_file("modified.txt", "original content");
    repo.git_add_all();
    repo.git_commit("Initial commit");

    // Modify the file to create a git change
    fs::write(
        repo.path.join("modified.txt"),
        "changed content\nwith more lines",
    )
    .unwrap();

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

    harness.editor_mut().toggle_file_explorer();
    harness.wait_for_screen_contains("File Explorer").unwrap();

    // Wait for modified.txt with M indicator to appear (git status plugin async)
    harness
        .wait_until(|h| {
            let screen = h.screen_to_string();
            screen
                .lines()
                .any(|line| line.contains("modified.txt") && line.contains("M"))
        })
        .unwrap();

    // Find the row with modified.txt and the M indicator position
    let screen = harness.screen_to_string();
    let lines: Vec<&str> = screen.lines().collect();
    let (row, _line) = lines
        .iter()
        .enumerate()
        .find(|(_, l)| l.contains("modified.txt") && l.contains("M"))
        .expect("Should find modified.txt line");

    // Ensure layout cache (explorer area, trailing-slot bounds) is current.
    harness.render().unwrap();

    wait_for_hover_tooltip(
        &mut harness,
        row as u16,
        "Modified - File has unstaged changes",
    );

    println!("Screen after hover:\n{}", harness.screen_to_string());
}

/// Test that hovering over directory status indicator shows tooltip with modified files list
#[test]
#[cfg_attr(windows, ignore)] // Git plugin tests are flaky on Windows CI
fn test_file_explorer_directory_status_indicator_hover_tooltip() {
    let repo = GitTestRepo::new();
    repo.setup_git_explorer_plugin();

    // Create a subdirectory with files
    std::fs::create_dir(repo.path.join("subdir")).unwrap();
    repo.create_file("subdir/file1.txt", "original 1");
    repo.create_file("subdir/file2.txt", "original 2");
    repo.git_add_all();
    repo.git_commit("Initial commit");

    // Modify files in the subdirectory to create git changes
    fs::write(repo.path.join("subdir/file1.txt"), "modified 1").unwrap();
    fs::write(repo.path.join("subdir/file2.txt"), "modified 2").unwrap();

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

    harness.editor_mut().toggle_file_explorer();
    harness.wait_for_screen_contains("File Explorer").unwrap();

    // Wait for subdir with ● indicator to appear (bubbled indicator for directory with changes)
    harness
        .wait_until(|h| {
            let screen = h.screen_to_string();
            screen
                .lines()
                .any(|line| line.contains("subdir") && line.contains("●"))
        })
        .unwrap();

    // Find the row with subdir and the ● indicator position
    let screen = harness.screen_to_string();
    let lines: Vec<&str> = screen.lines().collect();
    let (row, _line) = lines
        .iter()
        .enumerate()
        .find(|(_, l)| l.contains("subdir") && l.contains("●"))
        .expect("Should find subdir line");

    // Ensure layout cache (explorer area, trailing-slot bounds) is current.
    harness.render().unwrap();

    wait_for_hover_tooltip(&mut harness, row as u16, "Modified files:");
    harness
        .wait_until(|h| {
            let screen = h.screen_to_string();
            screen.contains("file1.txt") && screen.contains("file2.txt")
        })
        .unwrap();

    println!("Screen after hover:\n{}", harness.screen_to_string());
}

/// Test file explorer search functionality - typing characters to filter files
#[test]
fn test_file_explorer_search_basic() {
    // Create harness with isolated temp project
    let mut harness = EditorTestHarness::with_temp_project(120, 40).unwrap();
    let project_root = harness.project_dir().unwrap();

    // Create files with different names for searching
    // Note: files are sorted alphabetically, so order is: README.md, config.toml, main.rs, src/, test_helper.rs
    fs::write(project_root.join("main.rs"), "fn main() {}").unwrap();
    fs::write(project_root.join("config.toml"), "[settings]").unwrap();
    fs::write(project_root.join("README.md"), "# Project").unwrap();
    fs::write(project_root.join("test_helper.rs"), "// test helper").unwrap();
    fs::create_dir(project_root.join("src")).unwrap();
    fs::write(project_root.join("src/lib.rs"), "// lib").unwrap();

    // Toggle file explorer on
    harness.editor_mut().toggle_file_explorer();
    harness.wait_for_screen_contains("File Explorer").unwrap();

    // The panel chrome (title) shows immediately with a "Loading…" body;
    // wait for the async tree build itself to land before driving the
    // search — an item on screen is the semantic ready signal.
    harness.wait_for_file_explorer_item("main.rs").unwrap();
    harness.render().unwrap();

    let screen_before = harness.screen_to_string();
    println!("Screen before search:\n{screen_before}");

    // Get the initially selected entry (should be the root or first item)
    let initial_selection = harness
        .editor()
        .file_explorer()
        .and_then(|e| e.get_selected_entry())
        .map(|e| e.name.clone());
    println!("Initial selection: {:?}", initial_selection);

    // Type a search query - "main" to find main.rs
    harness
        .send_key(KeyCode::Char('m'), KeyModifiers::empty())
        .unwrap();
    harness.render().unwrap();
    harness
        .send_key(KeyCode::Char('a'), KeyModifiers::empty())
        .unwrap();
    harness.render().unwrap();
    harness
        .send_key(KeyCode::Char('i'), KeyModifiers::empty())
        .unwrap();
    harness.render().unwrap();
    harness
        .send_key(KeyCode::Char('n'), KeyModifiers::empty())
        .unwrap();
    harness.render().unwrap();

    // Search bar should be visible with the query
    let screen_with_search = harness.screen_to_string();
    println!("Screen with search:\n{screen_with_search}");

    // Should show the search indicator "/" with query
    assert!(
        screen_with_search.contains("/main"),
        "Should show search bar with query. Screen:\n{screen_with_search}"
    );

    // CRITICAL: Selection should have jumped to main.rs (the matching file)
    let selection_after_search = harness
        .editor()
        .file_explorer()
        .and_then(|e| e.get_selected_entry())
        .map(|e| e.name.clone());
    println!("Selection after search: {:?}", selection_after_search);

    assert_eq!(
        selection_after_search,
        Some("main.rs".to_string()),
        "Selection should jump to matching file 'main.rs'. Got: {:?}",
        selection_after_search
    );

    // Test backspace removes characters from search
    harness
        .send_key(KeyCode::Backspace, KeyModifiers::empty())
        .unwrap();
    harness.render().unwrap();

    let screen_after_backspace = harness.screen_to_string();
    assert!(
        screen_after_backspace.contains("/mai"),
        "Should show search with one character removed. Screen:\n{screen_after_backspace}"
    );

    // Test Escape clears the search
    harness
        .send_key(KeyCode::Esc, KeyModifiers::empty())
        .unwrap();
    harness.render().unwrap();

    let screen_after_escape = harness.screen_to_string();
    // After clearing search, the search bar should not show a query
    assert!(
        !screen_after_escape.contains("/mai"),
        "Search should be cleared after Escape. Screen:\n{screen_after_escape}"
    );
}

/// Test that file explorer search generates match positions for highlighting
#[test]
fn test_file_explorer_search_highlight_positions() {
    let mut harness = EditorTestHarness::with_temp_project(120, 40).unwrap();
    let project_root = harness.project_dir().unwrap();

    fs::write(project_root.join("main.rs"), "fn main() {}").unwrap();

    harness.editor_mut().toggle_file_explorer();
    harness.wait_for_screen_contains("File Explorer").unwrap();

    // The panel title shows immediately over a "Loading…" body; wait for
    // the async tree build itself to land before driving the search.
    harness.wait_for_file_explorer_item("main.rs").unwrap();
    harness.render().unwrap();

    // Type "main" to search
    for c in "main".chars() {
        harness
            .send_key(KeyCode::Char(c), KeyModifiers::empty())
            .unwrap();
        harness.render().unwrap();
    }

    // Get the file explorer and check if we can get match positions for the selected node
    let explorer = harness
        .editor()
        .file_explorer()
        .expect("File explorer should exist");
    assert!(explorer.is_search_active(), "Search should be active");

    // Get the selected node and check for match positions
    if let Some(selected_entry) = explorer.get_selected_entry() {
        println!("Selected entry: {}", selected_entry.name);

        // The get_match_for_node requires a node_id, so let's check via the view
        // We can verify that the fuzzy match logic works by checking the search module directly
        let query = explorer.search_query();
        println!("Search query: {}", query);

        // Use the fuzzy match directly to verify match positions
        let fuzzy_result = fresh::input::fuzzy::fuzzy_match(query, &selected_entry.name);
        println!(
            "Fuzzy match result: matched={}, positions={:?}",
            fuzzy_result.matched, fuzzy_result.match_positions
        );

        assert!(fuzzy_result.matched, "Should match");
        assert!(
            !fuzzy_result.match_positions.is_empty(),
            "Match positions should not be empty for highlighting. Got: {:?}",
            fuzzy_result.match_positions
        );

        // For "main" matching "main.rs", positions should be [0, 1, 2, 3]
        assert_eq!(
            fuzzy_result.match_positions,
            vec![0, 1, 2, 3],
            "Match positions should be [0,1,2,3] for 'main' in 'main.rs'"
        );

        // Verify that get_match_for_node also returns positions
        // We need to get the selected node ID to verify
        let selected_index = explorer.get_selected_index();
        println!("Selected index: {:?}", selected_index);

        if let Some(idx) = selected_index {
            if let Some(node_id) = explorer.get_node_at_index(idx) {
                let match_result = explorer.get_match_for_node(node_id);
                println!("get_match_for_node result: {:?}", match_result);
                assert!(
                    match_result.is_some(),
                    "get_match_for_node should return Some for matching node"
                );
                let m = match_result.unwrap();
                assert_eq!(
                    m.match_positions,
                    vec![0, 1, 2, 3],
                    "get_match_for_node should return correct positions"
                );
            }
        }
    } else {
        panic!("Should have a selected entry");
    }
}

/// Test file explorer search navigation with up/down keys
#[test]
fn test_file_explorer_search_navigation() {
    let mut harness = EditorTestHarness::with_temp_project(120, 40).unwrap();
    let project_root = harness.project_dir().unwrap();

    // Create multiple files with similar names
    fs::write(project_root.join("test1.rs"), "// test 1").unwrap();
    fs::write(project_root.join("test2.rs"), "// test 2").unwrap();
    fs::write(project_root.join("other.rs"), "// other").unwrap();
    fs::write(project_root.join("test3.rs"), "// test 3").unwrap();

    harness.editor_mut().toggle_file_explorer();
    harness.wait_for_screen_contains("File Explorer").unwrap();

    // The panel title shows immediately over a "Loading…" body; wait for
    // the async tree build itself to land before driving the search.
    harness.wait_for_file_explorer_item("test1.rs").unwrap();
    harness.render().unwrap();

    // Type "test" to filter to only test files
    for c in "test".chars() {
        harness
            .send_key(KeyCode::Char(c), KeyModifiers::empty())
            .unwrap();
        harness.render().unwrap();
    }

    let screen = harness.screen_to_string();
    println!("Screen with 'test' filter:\n{screen}");

    // Verify search bar shows
    assert!(screen.contains("/test"), "Should show search query");

    // Navigate down through matches
    harness
        .send_key(KeyCode::Down, KeyModifiers::empty())
        .unwrap();
    harness.render().unwrap();

    harness
        .send_key(KeyCode::Down, KeyModifiers::empty())
        .unwrap();
    harness.render().unwrap();

    // Navigate up
    harness
        .send_key(KeyCode::Up, KeyModifiers::empty())
        .unwrap();
    harness.render().unwrap();

    // Selection should change (we're cycling through matches)
    let screen_after_nav = harness.screen_to_string();
    println!("Screen after navigation:\n{screen_after_nav}");
}

/// Test that Escape in file explorer clears search but stays in file explorer
#[test]
fn test_file_explorer_escape_clears_search() {
    let mut harness = EditorTestHarness::with_temp_project(120, 40).unwrap();
    let project_root = harness.project_dir().unwrap();

    fs::write(project_root.join("file.txt"), "content").unwrap();

    harness.editor_mut().toggle_file_explorer();
    harness.wait_for_screen_contains("File Explorer").unwrap();

    // The panel title shows immediately over a "Loading…" body; wait for
    // the async tree build itself to land before driving the search.
    harness.wait_for_file_explorer_item("file.txt").unwrap();
    harness.render().unwrap();

    // Verify we're in file explorer context
    assert!(
        matches!(
            harness.editor().get_key_context(),
            fresh::input::keybindings::KeyContext::FileExplorer
        ),
        "Should be in FileExplorer context"
    );

    // Type a search query
    harness
        .send_key(KeyCode::Char('f'), KeyModifiers::empty())
        .unwrap();
    harness.render().unwrap();

    // Verify search is active
    assert!(
        harness.editor().file_explorer().unwrap().is_search_active(),
        "Search should be active after typing"
    );

    // Press Escape to clear search
    harness
        .send_key(KeyCode::Esc, KeyModifiers::empty())
        .unwrap();
    harness.render().unwrap();

    // Search should be cleared
    assert!(
        !harness.editor().file_explorer().unwrap().is_search_active(),
        "Search should be cleared after Escape"
    );

    // Should still be in file explorer context
    assert!(
        matches!(
            harness.editor().get_key_context(),
            fresh::input::keybindings::KeyContext::FileExplorer
        ),
        "Should still be in FileExplorer context after Escape"
    );

    // Press Escape again - should now exit file explorer focus to the editor
    // (Escape with no active search transfers focus)
    harness
        .send_key(KeyCode::Esc, KeyModifiers::empty())
        .unwrap();
    harness.render().unwrap();

    assert!(
        matches!(
            harness.editor().get_key_context(),
            fresh::input::keybindings::KeyContext::Normal
        ),
        "Should exit FileExplorer context on second Escape (no search to clear)"
    );
}

/// Test that dotfiles (files/folders starting with .) are hidden by default
/// and that toggling "Show hidden files" controls their visibility.
///
/// Reproduces https://github.com/sinelaw/fresh/issues/1079:
/// Dotfiles are always visible in the explorer regardless of the
/// "Show hidden files" toggle.
#[test]
fn test_dotfiles_hidden_by_default_and_toggle_controls_visibility() {
    let mut harness = EditorTestHarness::with_temp_project(120, 40).unwrap();
    let project_root = harness.project_dir().unwrap();

    // Create both regular files and dotfiles/dotfolders
    fs::write(project_root.join("visible_file.txt"), "visible").unwrap();
    fs::write(project_root.join(".hidden_file"), "hidden").unwrap();
    fs::create_dir(project_root.join(".hidden_dir")).unwrap();
    fs::write(project_root.join(".hidden_dir/inner.txt"), "inner hidden").unwrap();

    // Open and focus file explorer
    harness.editor_mut().focus_file_explorer();
    harness.wait_for_file_explorer().unwrap();

    // Wait for the regular file to appear (proves the tree has loaded)
    harness
        .wait_for_file_explorer_item("visible_file.txt")
        .unwrap();

    // Dotfiles should NOT be visible by default (show_hidden defaults to false)
    let screen = harness.screen_to_string();
    assert!(
        !screen.contains(".hidden_file"),
        "Dotfile '.hidden_file' should NOT be visible when 'Show hidden files' is off.\nScreen:\n{}",
        screen
    );
    assert!(
        !screen.contains(".hidden_dir"),
        "Dotfolder '.hidden_dir' should NOT be visible when 'Show hidden files' is off.\nScreen:\n{}",
        screen
    );

    // Toggle "Show hidden files" on
    harness.editor_mut().file_explorer_toggle_hidden();
    harness.render().unwrap();

    // Now dotfiles SHOULD be visible
    let screen = harness.screen_to_string();
    assert!(
        screen.contains(".hidden_file"),
        "Dotfile '.hidden_file' SHOULD be visible after toggling 'Show hidden files' on.\nScreen:\n{}",
        screen
    );
    assert!(
        screen.contains(".hidden_dir"),
        "Dotfolder '.hidden_dir' SHOULD be visible after toggling 'Show hidden files' on.\nScreen:\n{}",
        screen
    );

    // Expand .hidden_dir so its children are loaded, then navigate to it
    // First find and select .hidden_dir by navigating down
    harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
    // Keep going down until we find .hidden_dir (it's sorted among the entries)
    for _ in 0..10 {
        let entry = harness
            .editor()
            .file_explorer()
            .and_then(|e| e.get_selected_entry())
            .map(|e| e.name.clone());
        if entry.as_deref() == Some(".hidden_dir") {
            break;
        }
        harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
    }
    // Expand it with Right arrow
    harness
        .send_key(KeyCode::Right, KeyModifiers::NONE)
        .unwrap();
    harness.sleep(std::time::Duration::from_millis(200));
    let _ = harness.editor_mut().process_async_messages();
    harness.render().unwrap();

    // inner.txt should now be visible (hidden dir is expanded, show_hidden is on)
    let screen = harness.screen_to_string();
    assert!(
        screen.contains("inner.txt"),
        "inner.txt should be visible when .hidden_dir is expanded and show_hidden is on.\nScreen:\n{}",
        screen
    );

    // Toggle "Show hidden files" off again
    harness.editor_mut().file_explorer_toggle_hidden();
    harness.render().unwrap();

    // Dotfiles AND their children should be hidden
    let screen = harness.screen_to_string();
    assert!(
        !screen.contains(".hidden_file"),
        "Dotfile '.hidden_file' should NOT be visible after toggling 'Show hidden files' off.\nScreen:\n{}",
        screen
    );
    assert!(
        !screen.contains(".hidden_dir"),
        "Dotfolder '.hidden_dir' should NOT be visible after toggling 'Show hidden files' off.\nScreen:\n{}",
        screen
    );
    assert!(
        !screen.contains("inner.txt"),
        "Children of hidden dirs should NOT be visible when 'Show hidden files' is off.\nScreen:\n{}",
        screen
    );
}

/// Regression test for https://github.com/sinelaw/fresh/issues/1388:
/// A file that is both a dotfile and matched by .gitignore should stay hidden
/// while "Show Gitignored Files" is off, even if "Show Hidden Files" is on.
/// Previously the explorer classified such files as Hidden first and only
/// consulted the hidden-files toggle, letting gitignored dotfiles slip in.
#[test]
fn test_hidden_gitignored_file_respects_gitignore_toggle() {
    let mut harness = EditorTestHarness::with_temp_project(120, 40).unwrap();
    let project_root = harness.project_dir().unwrap();

    fs::write(project_root.join(".gitignore"), ".DS_Store\n").unwrap();
    fs::write(project_root.join(".DS_Store"), "macos junk").unwrap();
    fs::write(project_root.join("visible_file.txt"), "visible").unwrap();

    harness.editor_mut().focus_file_explorer();
    harness.wait_for_file_explorer().unwrap();
    harness
        .wait_for_file_explorer_item("visible_file.txt")
        .unwrap();

    // Turn on "Show hidden files". "Show gitignored files" stays off.
    harness.editor_mut().file_explorer_toggle_hidden();
    harness.render().unwrap();

    let screen = harness.screen_to_string();
    assert!(
        screen.contains(".gitignore"),
        ".gitignore is hidden but not gitignored; should be visible with \
         show_hidden=true.\nScreen:\n{}",
        screen
    );
    assert!(
        !screen.contains(".DS_Store"),
        ".DS_Store is gitignored AND hidden; it should stay out of the \
         explorer while 'Show Gitignored Files' is off, regardless of \
         'Show Hidden Files'.\nScreen:\n{}",
        screen
    );

    // Turning on "Show gitignored files" reveals the file.
    harness.editor_mut().file_explorer_toggle_gitignored();
    harness.render().unwrap();

    let screen = harness.screen_to_string();
    assert!(
        screen.contains(".DS_Store"),
        ".DS_Store should be visible when both 'Show Hidden Files' and \
         'Show Gitignored Files' are on.\nScreen:\n{}",
        screen
    );
}

/// Editing .gitignore in the editor and saving must reload the patterns
/// so the file explorer picks up the new rules without a restart.
#[test]
fn test_saving_gitignore_reloads_ignore_patterns() {
    let mut harness = EditorTestHarness::with_temp_project(120, 40).unwrap();
    // Canonicalize so path comparisons match the tree's internal form on
    // macOS (tempdirs live under /var/folders → /private/var/folders).
    let project_root = harness.project_dir().unwrap().canonicalize().unwrap();

    // Start with a .gitignore that does NOT match .target_file.
    let gitignore_path = project_root.join(".gitignore");
    fs::write(&gitignore_path, "other_pattern\n").unwrap();
    fs::write(project_root.join(".target_file"), "content").unwrap();

    harness.editor_mut().focus_file_explorer();
    harness.wait_for_file_explorer().unwrap();

    // Show hidden so .target_file would be visible on the hidden-filter side.
    harness.editor_mut().file_explorer_toggle_hidden();
    harness.wait_for_file_explorer_item(".target_file").unwrap();
    assert!(
        !harness
            .editor()
            .file_explorer()
            .unwrap()
            .ignore_patterns()
            .is_ignored(&project_root.join(".target_file"), false),
        "pre-save: .target_file should be visible (not yet gitignored)"
    );

    // Open .gitignore, append `.target_file`, save.
    harness.editor_mut().open_file(&gitignore_path).unwrap();
    harness.editor_mut().active_window_mut().focus_editor();
    harness
        .send_key(KeyCode::End, KeyModifiers::CONTROL)
        .unwrap();
    harness.type_text(".target_file\n").unwrap();
    harness.editor_mut().save().unwrap();
    harness.render().unwrap();

    // Sanity: the on-disk file really has the new rule.
    let disk = fs::read_to_string(&gitignore_path).unwrap();
    assert!(
        disk.contains(".target_file"),
        "pre-check: .gitignore on disk must contain .target_file after save. Got:\n{}",
        disk
    );

    // After save, the new rule should be live — no restart, no manual refresh.
    assert!(
        harness
            .editor()
            .file_explorer()
            .unwrap()
            .ignore_patterns()
            .is_ignored(&project_root.join(".target_file"), false),
        "post-save: .target_file should be filtered out by the reloaded .gitignore. \
         On-disk .gitignore:\n{}",
        disk
    );
}

/// Nested .gitignore — editing a per-directory .gitignore must reload patterns
/// for that specific directory (not just the root).
#[test]
fn test_saving_nested_gitignore_reloads_for_that_dir() {
    let mut harness = EditorTestHarness::with_temp_project(120, 40).unwrap();
    let project_root = harness.project_dir().unwrap().canonicalize().unwrap();
    let subdir = project_root.join("subdir");
    fs::create_dir(&subdir).unwrap();

    let nested_gitignore = subdir.join(".gitignore");
    fs::write(&nested_gitignore, "\n").unwrap();
    fs::write(subdir.join(".target_file"), "content").unwrap();

    harness.editor_mut().focus_file_explorer();
    harness.wait_for_file_explorer().unwrap();
    harness.editor_mut().file_explorer_toggle_hidden();
    harness.render().unwrap();

    // Before: subdir/.target_file is not matched by any loaded gitignore.
    assert!(
        !harness
            .editor()
            .file_explorer()
            .unwrap()
            .ignore_patterns()
            .is_ignored(&subdir.join(".target_file"), false),
        "pre-save: nested .target_file should not be filtered yet"
    );

    // Edit the nested .gitignore.
    harness.editor_mut().open_file(&nested_gitignore).unwrap();
    harness.editor_mut().active_window_mut().focus_editor();
    harness
        .send_key(KeyCode::End, KeyModifiers::CONTROL)
        .unwrap();
    harness.type_text(".target_file\n").unwrap();
    harness.editor_mut().save().unwrap();
    harness.render().unwrap();

    // Reloaded — the subdir-scoped rule is live.
    assert!(
        harness
            .editor()
            .file_explorer()
            .unwrap()
            .ignore_patterns()
            .is_ignored(&subdir.join(".target_file"), false),
        "post-save: nested .target_file should be filtered by the reloaded per-dir .gitignore"
    );
    // And it doesn't leak upward: a sibling file at the root is not affected.
    fs::write(project_root.join(".target_file"), "content").unwrap();
    assert!(
        !harness
            .editor()
            .file_explorer()
            .unwrap()
            .ignore_patterns()
            .is_ignored(&project_root.join(".target_file"), false),
        "a nested .gitignore rule must not filter a same-named file at the project root"
    );
}

/// External edits to a .gitignore must be picked up by the file-tree poll,
/// and deleting a .gitignore must drop its rules.
#[test]
fn test_external_gitignore_edit_and_delete_reload_via_poll() {
    let mut harness = EditorTestHarness::with_temp_project(120, 40).unwrap();
    let project_root = harness.project_dir().unwrap().canonicalize().unwrap();

    let gitignore_path = project_root.join(".gitignore");
    fs::write(&gitignore_path, "foo.txt\n").unwrap();
    fs::write(project_root.join("foo.txt"), "").unwrap();
    fs::write(project_root.join("bar.txt"), "").unwrap();

    harness.editor_mut().focus_file_explorer();
    harness.wait_for_file_explorer().unwrap();
    harness.render().unwrap();

    let check = |h: &EditorTestHarness, name: &str| {
        h.editor()
            .file_explorer()
            .unwrap()
            .ignore_patterns()
            .is_ignored(&project_root.join(name), false)
    };
    assert!(
        check(&harness, "foo.txt"),
        "initial: foo.txt should be gitignored"
    );
    assert!(
        !check(&harness, "bar.txt"),
        "initial: bar.txt should not be gitignored"
    );

    // Sleep past typical 1s mtime granularity so the edit produces a distinct
    // mtime. External write + poll tick.
    std::thread::sleep(std::time::Duration::from_millis(1100));
    fs::write(&gitignore_path, "bar.txt\n").unwrap();
    harness.advance_time(std::time::Duration::from_secs(5));
    harness.editor_mut().poll_file_tree_changes();

    assert!(
        !check(&harness, "foo.txt"),
        "after external edit: foo.txt should be visible"
    );
    assert!(
        check(&harness, "bar.txt"),
        "after external edit: bar.txt should be gitignored"
    );

    // Delete .gitignore — the dropped entry should no longer filter anything.
    fs::remove_file(&gitignore_path).unwrap();
    harness.advance_time(std::time::Duration::from_secs(5));
    harness.editor_mut().poll_file_tree_changes();

    assert!(
        !check(&harness, "foo.txt"),
        "after delete: foo.txt should be visible"
    );
    assert!(
        !check(&harness, "bar.txt"),
        "after delete: bar.txt should be visible"
    );
}

/// Creating a .gitignore externally in an already-expanded directory must
/// be picked up by the poll: the dir's mtime bumps, refresh_node re-lists,
/// and the rules for that dir get loaded for the first time.
#[test]
fn test_externally_creating_gitignore_loads_rules_via_poll() {
    let mut harness = EditorTestHarness::with_temp_project(120, 40).unwrap();
    let project_root = harness.project_dir().unwrap().canonicalize().unwrap();

    fs::write(project_root.join("foo.txt"), "").unwrap();

    harness.editor_mut().focus_file_explorer();
    harness.wait_for_file_explorer().unwrap();
    harness.render().unwrap();

    // Sanity: no gitignore loaded at start.
    assert_eq!(
        harness
            .editor()
            .file_explorer()
            .unwrap()
            .ignore_patterns()
            .loaded_gitignore_dirs()
            .len(),
        0
    );

    // Prime the dir-mtime store so the later change is detected as a
    // difference, not as a first-sighting. Two ticks: spawn + receive.
    harness.advance_time(std::time::Duration::from_secs(5));
    harness.editor_mut().poll_file_tree_changes();
    std::thread::sleep(std::time::Duration::from_millis(50));
    harness.advance_time(std::time::Duration::from_secs(5));
    harness.editor_mut().poll_file_tree_changes();

    // Externally create a .gitignore that matches foo.txt. Sleep past 1s
    // filesystem mtime granularity so the parent dir's mtime is distinguishable.
    std::thread::sleep(std::time::Duration::from_millis(1100));
    fs::write(project_root.join(".gitignore"), "foo.txt\n").unwrap();

    // First tick spawns the bg dir-poll; subsequent ticks process its
    // results (where refresh_node + reload-gitignore runs). Advance time
    // past the poll interval on every iteration so the guard lets us in.
    let foo = project_root.join("foo.txt");
    let mut picked_up = false;
    for _ in 0..50 {
        harness.advance_time(std::time::Duration::from_secs(5));
        harness.editor_mut().poll_file_tree_changes();
        if harness
            .editor()
            .file_explorer()
            .unwrap()
            .ignore_patterns()
            .is_ignored(&foo, false)
        {
            picked_up = true;
            break;
        }
        std::thread::sleep(std::time::Duration::from_millis(20));
    }
    assert!(
        picked_up,
        "new .gitignore should be loaded via dir-mtime poll + refresh path"
    );
}

/// Helper: find the right border column of the file explorer on screen.
///
/// Scans for the box-drawing corner characters that ratatui's `Block` draws
/// for the file explorer.  Tries "┐" (top-right) first on each row from the
/// top, then falls back to "┘" (bottom-right).
fn find_explorer_border_col(harness: &EditorTestHarness) -> u16 {
    let height = 40u16; // match test terminal height
                        // Look for ┐ (top-right corner) — it's on the top border row of the explorer block.
    for row in 0..height {
        let text = harness.get_row_text(row);
        for (i, ch) in text.chars().enumerate() {
            if ch == '┐' {
                return i as u16;
            }
        }
    }
    // Fallback: look for ┘ (bottom-right corner)
    for row in (0..height).rev() {
        let text = harness.get_row_text(row);
        for (i, ch) in text.chars().enumerate() {
            if ch == '┘' {
                return i as u16;
            }
        }
    }
    panic!(
        "Could not find file explorer border on screen.\nScreen:\n{}",
        harness.screen_to_string()
    );
}

/// Test that hovering on the file explorer right border highlights that border
/// and does NOT overwrite the adjacent editor column.
#[test]
fn test_file_explorer_border_hover_does_not_overwrite_editor_column() {
    let mut harness = EditorTestHarness::with_temp_project(120, 40).unwrap();
    let project_root = harness.project_dir().unwrap();
    fs::write(project_root.join("hello.txt"), "hello world\n").unwrap();

    // Open and wait for file explorer
    harness.editor_mut().focus_file_explorer();
    harness.wait_for_file_explorer().unwrap();
    harness.render().unwrap();

    assert!(
        harness.editor().file_explorer_visible(),
        "File explorer should be visible"
    );

    // Find the right border column of the file explorer
    let border_col = find_explorer_border_col(&harness);
    let editor_first_col = border_col + 1;

    // Record the character in the first column of the editor area (just right of border)
    let mid_row = 10u16; // a content row inside the explorer area
    let row_text_before = harness.get_row_text(mid_row);
    let editor_char_before: char = row_text_before
        .chars()
        .nth(editor_first_col as usize)
        .unwrap();

    // Hover on the border column
    harness.mouse_move(border_col, mid_row).unwrap();

    // The editor column right of the border should NOT have been overwritten with "│"
    let row_text_after = harness.get_row_text(mid_row);
    let editor_char_after: char = row_text_after
        .chars()
        .nth(editor_first_col as usize)
        .unwrap();

    assert_eq!(
        editor_char_before,
        editor_char_after,
        "Hovering on the file explorer border should NOT overwrite the first column of the \
         editor area (border_col={}, editor_col={}). \
         Before hover: '{}', After hover: '{}'.\nScreen:\n{}",
        border_col,
        editor_first_col,
        editor_char_before,
        editor_char_after,
        harness.screen_to_string()
    );
}

/// Test that dragging the file explorer border changes the explorer width.
#[test]
fn test_file_explorer_border_drag_resizes() {
    let mut harness = EditorTestHarness::with_temp_project(120, 40).unwrap();
    let project_root = harness.project_dir().unwrap();
    fs::write(project_root.join("hello.txt"), "hello world\n").unwrap();

    // Open and wait for file explorer
    harness.editor_mut().focus_file_explorer();
    harness.wait_for_file_explorer().unwrap();
    harness.render().unwrap();

    let border_col_before = find_explorer_border_col(&harness);

    // Use a row in the middle of the content area (inside the explorer block's borders)
    let mid_row = 15u16;
    let drag_delta = 10u16;
    harness
        .mouse_drag(
            border_col_before,
            mid_row,
            border_col_before + drag_delta,
            mid_row,
        )
        .unwrap();

    let border_col_after = find_explorer_border_col(&harness);

    assert!(
        border_col_after > border_col_before,
        "After dragging the border right, the file explorer should be wider. \
         Border was at col {}, expected it to move right but found it at col {}.\nScreen:\n{}",
        border_col_before,
        border_col_after,
        harness.screen_to_string()
    );

    // The delta should be approximately what we dragged (allow some rounding from percentage math)
    let actual_delta = border_col_after as i32 - border_col_before as i32;
    assert!(
        (actual_delta - drag_delta as i32).abs() <= 2,
        "Border should have moved roughly {} columns, but moved {}",
        drag_delta,
        actual_delta
    );
}

/// Test that clicking a markdown file in the file explorer keeps focus on the explorer,
/// so pressing Enter does NOT modify the buffer.
/// Reproduces bug: clicking a .md file in the explorer and pressing Enter leaks
/// the keypress into the buffer (inserts a newline) even though the explorer should
/// have focus. This appears to be related to the markdown source plugin.
#[test]
fn test_file_explorer_click_markdown_enter_does_not_modify_buffer() {
    let mut harness = EditorTestHarness::with_temp_project(80, 24).unwrap();
    let project_dir = harness.project_dir().unwrap();

    let test_content = "hello";
    fs::write(project_dir.join("readme.md"), test_content).unwrap();

    // Open file explorer with Ctrl+E (focuses the explorer)
    harness
        .send_key(KeyCode::Char('e'), KeyModifiers::CONTROL)
        .unwrap();
    harness.wait_for_file_explorer().unwrap();
    harness
        .wait_until(|h| h.screen_to_string().contains("File Explorer"))
        .unwrap();

    // Wait for the file to appear in the tree
    harness.wait_for_file_explorer_item("readme.md").unwrap();

    // Find the file's position on screen so we can click it
    harness.render().unwrap();
    let (click_col, click_row) = harness
        .find_text_on_screen("readme.md")
        .expect("readme.md should be visible in the file explorer");

    // Click on the file in the explorer — this opens it but focus should stay on explorer
    harness.mouse_click(click_col, click_row).unwrap();
    harness.render().unwrap();

    // Wait for the file to be opened in a buffer
    harness
        .wait_until(|h| h.screen_to_string().contains("hello"))
        .unwrap();

    // Focus is on the file explorer. Pressing Enter should NOT modify the buffer.
    // Bug: for markdown files, Enter leaks through and inserts a newline.
    harness
        .send_key(KeyCode::Enter, KeyModifiers::NONE)
        .unwrap();
    harness.render().unwrap();

    let content = harness.get_buffer_content().unwrap();
    assert_eq!(
        content, "hello",
        "After clicking a markdown file in the explorer, Enter should not modify the buffer \
         (focus should be on the explorer). But buffer content changed to {:?} — \
         the keypress leaked into the buffer.",
        content
    );
}

/// Test that git status markers clear after an external commit.
/// The editor polls `.git/index` mtime and refreshes decorations automatically.
/// Regression test for https://github.com/sinelaw/fresh/issues/1431
#[test]
#[cfg_attr(windows, ignore)]
fn test_file_explorer_git_markers_clear_after_external_commit() {
    let repo = GitTestRepo::new();
    repo.setup_git_explorer_plugin();
    repo.create_file("file1.txt", "hello");
    repo.create_file("file2.txt", "world");
    repo.git_add_all();
    repo.git_commit("Initial commit");

    // Create working-copy modifications so "M" markers appear
    fs::write(repo.path.join("file1.txt"), "modified").unwrap();
    fs::write(repo.path.join("file2.txt"), "also modified").unwrap();

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

    harness.editor_mut().toggle_file_explorer();
    harness
        .wait_until(|h| h.screen_to_string().contains("File Explorer"))
        .unwrap();

    // Wait for "M" markers to appear on modified files
    harness
        .wait_until(|h| {
            let screen = h.screen_to_string();
            screen
                .lines()
                .any(|line| line.contains("file1.txt") && line.contains("M"))
        })
        .unwrap();

    // Ensure the current .git/index mtime is recorded by the file tree poller.
    harness.advance_time(std::time::Duration::from_secs(5));
    harness.editor_mut().poll_file_tree_changes();

    // Snapshot the seeded .git/index mtime. This is the value the poller just
    // recorded; the external commit below must yield a *different* mtime for
    // the change to be detected.
    let index_path = repo.path.join(".git").join("index");
    let seeded_mtime = fs::metadata(&index_path).unwrap().modified().unwrap();

    // Commit all changes externally (simulating a user running git in another
    // terminal). Use the isolated test git wrapper, not a raw `git` process.
    repo.git_add_all();
    repo.git_commit("commit changes");

    // Force .git/index to a deterministically newer mtime. Relying on the real
    // post-commit mtime is flaky: on filesystems with coarse mtime granularity
    // the commit can land in the same tick as the seeded value, so the poller
    // never observes a change and the test hangs (the TIMEOUT this regression
    // test was failing with). Setting the mtime explicitly removes any
    // dependence on wall-clock timing or filesystem resolution — no
    // `thread::sleep` and no real-time waiting.
    let bumped = seeded_mtime + std::time::Duration::from_secs(10);
    std::fs::File::options()
        .write(true)
        .open(&index_path)
        .unwrap()
        .set_modified(bumped)
        .unwrap();

    // Advance time past poll interval — poll_file_tree_changes now also
    // checks .git/index mtime and fires the plugin hook on change.
    harness.advance_time(std::time::Duration::from_secs(5));

    // wait_until drives the editor tick loop; the integrated git index
    // check inside poll_file_tree_changes detects the mtime change.
    harness
        .wait_until(|h| {
            let screen = h.screen_to_string();
            !screen
                .lines()
                .any(|line| line.contains("file1.txt") && line.contains("M"))
        })
        .unwrap();

    // Also verify file2 has no marker
    let screen = harness.screen_to_string();
    assert!(
        !screen
            .lines()
            .any(|line| line.contains("file2.txt") && line.contains("M")),
        "file2.txt should not have an M marker after external commit"
    );
}

/// The rendered file explorer panel has a hard minimum width of 5 columns,
/// independent of whatever the config or the restored workspace tries to
/// set it to. A 0/1/2-column explorer has no room for a readable tree and
/// leaves the drag border stuck against the screen edge; clamping here
/// keeps the UI recoverable even if a user hand-edits their config to a
/// near-zero value.
#[test]
fn test_file_explorer_minimum_render_width_is_5_cols() {
    use fresh::config::{Config, ExplorerWidth};
    let mut config = Config::default();
    config.file_explorer.width = ExplorerWidth::Columns(1);

    let mut harness = EditorTestHarness::with_config(100, 40, config).unwrap();

    harness
        .send_key(KeyCode::Char('e'), KeyModifiers::CONTROL)
        .unwrap();
    harness.wait_for_file_explorer().unwrap();
    harness.render().unwrap();
    assert!(harness.editor().file_explorer_visible());

    let cols = find_explorer_border_col(&harness) + 1;
    assert_eq!(
        cols, 5,
        "explorer must never render narrower than 5 cols regardless of config; got {}.\nScreen:\n{}",
        cols,
        harness.screen_to_string()
    );
}

/// Test that FileExplorerSide::Left renders explorer on the left (default behavior)
#[test]
fn test_file_explorer_side_left() {
    use fresh::config::{Config, ExplorerWidth, FileExplorerSide};

    let mut config = Config::default();
    config.file_explorer.side = FileExplorerSide::Left;
    config.file_explorer.width = ExplorerWidth::Columns(30); // Fixed width for predictable test

    let mut harness = EditorTestHarness::with_temp_project_and_config(120, 40, config).unwrap();
    let project_root = harness.project_dir().unwrap();
    fs::write(project_root.join("test.txt"), "test").unwrap();

    harness.editor_mut().focus_file_explorer();
    harness.wait_for_file_explorer().unwrap();
    harness.render().unwrap();

    let border_col = find_explorer_border_col(&harness);

    // File explorer should be on the left side (~30% of 120 = ~36 cols is default width)
    // Border should be around column 35-40 (well before the middle of screen at 60)
    assert!(
        border_col < 60,
        "File explorer should be on the left side (border at col {}). Screen:\n{}",
        border_col,
        harness.screen_to_string()
    );
}

/// Test that FileExplorerSide::Right renders explorer on the right side
#[test]
fn test_file_explorer_side_right() {
    use fresh::config::{Config, ExplorerWidth, FileExplorerSide};

    let mut config = Config::default();
    config.file_explorer.side = FileExplorerSide::Right;
    config.file_explorer.width = ExplorerWidth::Columns(30); // Fixed width for predictable test

    let mut harness = EditorTestHarness::with_temp_project_and_config(120, 40, config).unwrap();
    let project_root = harness.project_dir().unwrap();
    fs::write(project_root.join("test.txt"), "test").unwrap();

    harness.editor_mut().focus_file_explorer();
    harness.wait_for_file_explorer().unwrap();
    harness.render().unwrap();

    let border_col = find_explorer_border_col(&harness);

    // File explorer should be on the right side (terminal width 120, so right side starts around col 80)
    assert!(
        border_col > 60,
        "File explorer should be on the right side (border at col {}). Screen:\n{}",
        border_col,
        harness.screen_to_string()
    );
}

/// Test that workspace serialization correctly persists file explorer side
#[test]
fn test_file_explorer_side_workspace_serialization() {
    use fresh::config::{Config, FileExplorerSide};

    let temp_dir = tempfile::tempdir().unwrap();
    let project_dir = temp_dir.path().to_path_buf();
    fs::write(project_dir.join("file.txt"), "content").unwrap();

    // Create config with side set to Right
    let mut config = Config::default();
    config.file_explorer.side = FileExplorerSide::Right;

    let mut harness =
        EditorTestHarness::with_config_and_working_dir(120, 40, config, project_dir.clone())
            .unwrap();

    // Show file explorer
    harness.editor_mut().toggle_file_explorer();
    harness.wait_for_file_explorer().unwrap();
    harness.render().unwrap();

    // Verify it's on the right
    let border_col = find_explorer_border_col(&harness);
    assert!(
        border_col > 60,
        "File explorer should be on right side (border at col {}). Screen:\n{}",
        border_col,
        harness.screen_to_string()
    );

    // Save the workspace and reload
    harness.editor_mut().save_workspace().unwrap();
}

// ----- Context menu: duplicate / copy path (issue #1576) -----

/// Duplicate the selected file: a sibling named "<stem> copy.<ext>" must
/// appear on disk after invoking the action.
#[test]
fn test_file_explorer_duplicate_file_creates_copy_sibling() {
    let mut harness = EditorTestHarness::with_temp_project(120, 40).unwrap();
    let project_root = harness.project_dir().unwrap();

    fs::write(project_root.join("alpha.txt"), "hello").unwrap();

    harness.editor_mut().focus_file_explorer();
    harness.wait_for_file_explorer().unwrap();
    harness.wait_for_file_explorer_item("alpha.txt").unwrap();

    // Move selection from the (auto-expanded) root onto alpha.txt.
    harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
    harness.render().unwrap();

    harness.editor_mut().file_explorer_duplicate();
    harness
        .wait_until(|h| h.editor().working_dir().join("alpha copy.txt").exists())
        .unwrap();

    let original = project_root.join("alpha.txt");
    let copy = project_root.join("alpha copy.txt");
    assert!(original.exists(), "original alpha.txt must remain");
    assert!(copy.exists(), "duplicate alpha copy.txt must exist");
    assert_eq!(
        fs::read_to_string(&copy).unwrap(),
        fs::read_to_string(&original).unwrap(),
        "duplicate's contents must match the source"
    );
}

/// Duplicating when "<stem> copy.<ext>" already exists must pick the next
/// numbered name ("<stem> copy 2.<ext>") rather than overwriting.
#[test]
fn test_file_explorer_duplicate_increments_when_copy_exists() {
    let mut harness = EditorTestHarness::with_temp_project(120, 40).unwrap();
    let project_root = harness.project_dir().unwrap();

    fs::write(project_root.join("alpha.txt"), "hello").unwrap();
    fs::write(project_root.join("alpha copy.txt"), "old copy").unwrap();

    harness.editor_mut().focus_file_explorer();
    harness.wait_for_file_explorer().unwrap();
    harness.wait_for_file_explorer_item("alpha.txt").unwrap();

    // Tree order is alphabetical, so two Downs land on "alpha.txt"
    // ("alpha copy.txt" sorts first).
    harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
    harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
    harness.render().unwrap();

    harness.editor_mut().file_explorer_duplicate();
    harness
        .wait_until(|h| h.editor().working_dir().join("alpha copy 2.txt").exists())
        .unwrap();

    assert_eq!(
        fs::read_to_string(project_root.join("alpha copy.txt")).unwrap(),
        "old copy",
        "the pre-existing 'alpha copy.txt' must NOT be overwritten"
    );
    assert_eq!(
        fs::read_to_string(project_root.join("alpha copy 2.txt")).unwrap(),
        "hello",
        "the new duplicate must contain alpha.txt's content"
    );
}

/// Duplicating a directory must recursively copy its contents under a new
/// "<name> copy" sibling.
#[test]
fn test_file_explorer_duplicate_directory_copies_recursively() {
    let mut harness = EditorTestHarness::with_temp_project(120, 40).unwrap();
    let project_root = harness.project_dir().unwrap();

    fs::create_dir(project_root.join("src")).unwrap();
    fs::write(project_root.join("src/main.rs"), "fn main() {}").unwrap();

    harness.editor_mut().focus_file_explorer();
    harness.wait_for_file_explorer().unwrap();
    harness.wait_for_file_explorer_item("src").unwrap();

    harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
    harness.render().unwrap();

    harness.editor_mut().file_explorer_duplicate();
    harness
        .wait_until(|h| {
            h.editor()
                .working_dir()
                .join("src copy")
                .join("main.rs")
                .exists()
        })
        .unwrap();

    assert_eq!(
        fs::read_to_string(project_root.join("src copy/main.rs")).unwrap(),
        "fn main() {}",
        "duplicated directory must contain a copy of main.rs"
    );
}

/// Copy Full Path puts the absolute path of the selected node on the
/// internal clipboard.
#[test]
fn test_file_explorer_copy_full_path() {
    let mut harness = EditorTestHarness::with_temp_project(120, 40).unwrap();
    let project_root = harness.project_dir().unwrap();
    fs::write(project_root.join("alpha.txt"), "hello").unwrap();

    harness.editor_mut().focus_file_explorer();
    harness.wait_for_file_explorer().unwrap();
    harness.wait_for_file_explorer_item("alpha.txt").unwrap();

    harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
    harness.render().unwrap();

    // Read the actual selected-node path from the explorer rather than
    // reconstructing it from `project_dir()`. On macOS CI the working dir
    // can be a symlinked tempdir (`/tmp` → `/private/tmp`, `/var/folders`
    // → `/private/var/folders`), so reconstruction and the tree's own
    // path representation can differ as strings even when they point to
    // the same file.
    let selected_path = harness
        .editor()
        .file_explorer()
        .and_then(|fe| fe.get_selected_entry())
        .map(|e| e.path.clone())
        .expect("explorer must have a selected entry after Down");
    assert_eq!(
        selected_path.file_name().and_then(|n| n.to_str()),
        Some("alpha.txt"),
        "Down from the auto-expanded root should land on alpha.txt; got {:?}",
        selected_path
    );

    // Force internal-only clipboard so the test doesn't read/write the
    // host's real clipboard and stays parallel-safe in CI.
    harness.editor_mut().set_clipboard_for_test(String::new());
    harness.editor_mut().file_explorer_copy_path(false);

    let copied = harness.editor().clipboard_content_for_test();
    assert_eq!(
        copied,
        selected_path.to_string_lossy(),
        "Copy Full Path must put the selected node's absolute path on the clipboard"
    );
}

/// Copy Relative Path strips the project root prefix.
#[test]
fn test_file_explorer_copy_relative_path() {
    let mut harness = EditorTestHarness::with_temp_project(120, 40).unwrap();
    let project_root = harness.project_dir().unwrap();
    fs::create_dir(project_root.join("src")).unwrap();
    fs::write(project_root.join("src/main.rs"), "fn main() {}").unwrap();

    harness.editor_mut().focus_file_explorer();
    harness.wait_for_file_explorer().unwrap();
    harness.wait_for_file_explorer_item("src").unwrap();

    // Down once to select "src", expand, Down again to land on "main.rs".
    harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
    harness
        .send_key(KeyCode::Right, KeyModifiers::NONE)
        .unwrap();
    harness.wait_for_file_explorer_item("main.rs").unwrap();
    harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
    harness.render().unwrap();

    // Sanity-check selection so the test fails loudly if explorer ordering
    // shifts under us, instead of silently asserting against the wrong path.
    let selected_path = harness
        .editor()
        .file_explorer()
        .and_then(|fe| fe.get_selected_entry())
        .map(|e| e.path.clone())
        .expect("explorer must have a selected entry");
    assert_eq!(
        selected_path.file_name().and_then(|n| n.to_str()),
        Some("main.rs"),
        "selection should be on main.rs after Down, Right, Down; got {:?}",
        selected_path
    );

    harness.editor_mut().set_clipboard_for_test(String::new());
    harness.editor_mut().file_explorer_copy_path(true);

    let copied = harness.editor().clipboard_content_for_test();
    // Compute the expected relative path the same way file_explorer_copy_path
    // does — strip the working_dir prefix from the actual selected-node path.
    // `working_dir` and `selected_path` come from the same source (the
    // editor's own state), so the prefix-strip must succeed and the result
    // is independent of any tempdir-symlink quirks.
    let working_dir = harness.editor().working_dir().to_path_buf();
    let expected = selected_path
        .strip_prefix(&working_dir)
        .expect("selected path must live under working_dir");
    assert_eq!(
        copied,
        expected.to_string_lossy(),
        "Copy Relative Path must produce the project-relative path"
    );
}

/// Wait for a hover tooltip string while scanning columns on an explorer row.
fn wait_for_hover_tooltip(harness: &mut EditorTestHarness, row: u16, needle: &str) {
    let needle = needle.to_string();
    let deadline = std::time::Instant::now() + std::time::Duration::from_secs(15);
    loop {
        // Status badges are right-aligned; scan the explorer width for the hit target.
        for col in 0..40u16 {
            harness.mouse_move(col, row).unwrap();
            if harness.screen_to_string().contains(&needle) {
                return;
            }
        }
        if std::time::Instant::now() >= deadline {
            panic!(
                "Timed out waiting for hover tooltip {needle:?} on row {row}. Screen:\n{}",
                harness.screen_to_string()
            );
        }
        harness.tick_and_render().unwrap();
        std::thread::sleep(std::time::Duration::from_millis(20));
    }
}

/// Locate the first rendered file-explorer row containing `name` and return its
/// `(row, column)` in screen cells. Columns are counted in characters (not
/// bytes) so the multi-byte tree connectors (`│`) don't skew the result.
/// Requires a tree marker on the line so we match a tree row, not a tab/status
/// echo of the same name.
fn find_explorer_row_cell(harness: &EditorTestHarness, name: &str) -> (u16, u16) {
    let screen = harness.screen_to_string();
    let (row, line) = screen
        .lines()
        .enumerate()
        .find(|(_, l)| l.contains(name) && (l.contains('│') || l.contains('>') || l.contains('▼')))
        .unwrap_or_else(|| panic!("explorer row for {name:?} not found:\n{screen}"));
    let byte_idx = line.find(name).expect("name must be on its row");
    let col = line[..byte_idx].chars().count() as u16;
    (row as u16, col)
}

/// Locate the first rendered line containing `needle` and return the
/// `(row, column)` of its first character in screen cells. Columns are counted
/// in characters (not bytes) so multi-byte glyphs don't skew the result.
fn find_screen_cell(harness: &EditorTestHarness, needle: &str) -> (u16, u16) {
    let screen = harness.screen_to_string();
    let (row, line) = screen
        .lines()
        .enumerate()
        .find(|(_, l)| l.contains(needle))
        .unwrap_or_else(|| panic!("{needle:?} not found on screen:\n{screen}"));
    let byte_idx = line.find(needle).expect("needle must be on its row");
    let col = line[..byte_idx].chars().count() as u16;
    (row as u16, col)
}

/// Regression: duplicating a tracked file inside a git repo must fire the
/// `after_file_explorer_change` plugin hook so git_explorer rescans and the
/// new file picks up its `U` (untracked) badge — without the hook, the
/// freshly-duplicated file rendered with no decoration until the user
/// triggered some other event (focus change, save, …). See PR #1841.
#[test]
#[cfg_attr(windows, ignore)] // Git plugin tests are flaky on Windows CI
fn test_file_explorer_duplicate_refreshes_git_decorations() {
    let repo = GitTestRepo::new();
    repo.setup_git_explorer_plugin();
    repo.create_file("alpha.txt", "tracked content");
    repo.git_add_all();
    repo.git_commit("seed");

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

    harness.editor_mut().toggle_file_explorer();
    harness.wait_for_screen_contains("File Explorer").unwrap();
    harness.wait_for_file_explorer_item("alpha.txt").unwrap();

    // Drive the duplicate entirely through the mouse and observe only rendered
    // output (CONTRIBUTING.md "E2E Tests Observe, Not Inspect"). Right-clicking
    // the alpha.txt row both selects that entry and opens the file-explorer
    // context menu, so there's no need to Down-walk past the auto-created
    // plugins/ directory (setup_git_explorer_plugin installs the plugin under
    // <repo>/plugins/, which sorts before files in the default Type sort) or to
    // read model state to find the selection.
    let (alpha_row, alpha_col) = find_explorer_row_cell(&harness, "alpha.txt");
    harness.mouse_right_click(alpha_col, alpha_row).unwrap();

    // The context menu must offer "Duplicate"; wait for it to render, then
    // click it — again, all through observed screen text and the mouse.
    harness.wait_for_screen_contains("Duplicate").unwrap();
    let (dup_row, dup_col) = find_screen_cell(&harness, "Duplicate");
    harness.mouse_click(dup_col, dup_row).unwrap();

    // The duplicate lands as `alpha copy.txt`. The git_explorer plugin
    // refreshes asynchronously off `after_file_explorer_change`; wait for
    // the screen to show the new file *and* its `U` badge on the same line.
    // Looking only for the file (no badge) would race the plugin's rescan
    // and pass even if the hook were silently dropped.
    harness
        .wait_until(|h| {
            h.screen_to_string()
                .lines()
                .any(|line| line.contains("alpha copy.txt") && line.contains('U'))
        })
        .unwrap();
}

/// Test that with `file_explorer.follow_active_buffer = true`, switching tabs
/// re-syncs the file explorer to the newly active buffer's path — visibly
/// expanding the directory containing that file.
///
/// Two files live in different subdirectories; both directories are
/// initially collapsed. Opening file_b (after file_a was opened in-place
/// over the empty buffer) syncs the explorer, expanding dir_b. Then
/// `prev_buffer` switches the active tab back to file_a — with the sync
/// hook in place, dir_a expands and file_a.txt becomes visible in the
/// tree. Without the hook, dir_a stays collapsed and the wait times out.
#[test]
fn test_follow_active_buffer_syncs_explorer_on_tab_switch() {
    let mut config = Config::default();
    config.file_explorer.follow_active_buffer = true;

    let mut harness = EditorTestHarness::with_temp_project_and_config(120, 40, config).unwrap();
    let project_root = harness.project_dir().unwrap();

    fs::create_dir_all(project_root.join("dir_a")).unwrap();
    fs::create_dir_all(project_root.join("dir_b")).unwrap();
    fs::write(project_root.join("dir_a/file_a.txt"), "content a").unwrap();
    fs::write(project_root.join("dir_b/file_b.txt"), "content b").unwrap();

    // Open the explorer but keep focus in the editor so the sync hook is
    // eligible (it is gated on `key_context != FileExplorer`).
    harness.editor_mut().toggle_file_explorer();
    harness.editor_mut().active_window_mut().focus_editor();
    harness
        .wait_until(|h| h.screen_to_string().contains("File Explorer"))
        .unwrap();

    // Opening file_a replaces the initial `[No Name]` buffer in place, so
    // `set_active_buffer` is a no-op and no sync fires. Opening file_b
    // creates a new buffer; the sync triggered by that switch expands
    // dir_b, leaving dir_a still collapsed.
    harness
        .editor_mut()
        .open_file(&project_root.join("dir_a/file_a.txt"))
        .unwrap();
    harness
        .editor_mut()
        .open_file(&project_root.join("dir_b/file_b.txt"))
        .unwrap();
    harness
        .wait_until(|h| explorer_tree_contains(h, "file_b.txt"))
        .unwrap();
    assert!(
        !explorer_tree_contains(&harness, "file_a.txt"),
        "Precondition: dir_a should still be collapsed (file_a.txt not in \
         the tree) before the tab switch.\nScreen:\n{}",
        harness.screen_to_string()
    );

    // Switch the active tab back to file_a. With the sync hook in place,
    // the explorer expands dir_a and file_a.txt becomes visible in the
    // tree. Without it, dir_a stays collapsed and this wait times out.
    harness.editor_mut().prev_buffer();
    harness
        .wait_until(|h| explorer_tree_contains(h, "file_a.txt"))
        .unwrap();
}

/// Returns `true` when `name` appears on a screen line that also contains a
/// tree connector, matching the existing `wait_for_file_explorer_item`
/// heuristic. Reads only rendered output.
fn explorer_tree_contains(harness: &EditorTestHarness, name: &str) -> bool {
    harness
        .screen_to_string()
        .lines()
        .any(|line| line.contains(name) && line.contains('│'))
}

/// Custom tree indicators (issue #1940) — the collapsed/expanded glyphs
/// rendered next to directories must come from
/// `file_explorer.tree_indicator_collapsed/expanded` config, falling back
/// to the historical `>` / `▼` when unset. We verify by rendering the
/// explorer with a directory at the root, then asserting on screen text.
#[test]
fn test_file_explorer_custom_tree_indicators() {
    let mut config = Config::default();
    // Use single ASCII chars so the assertion is robust against unicode
    // width quirks in the test backend.
    config.file_explorer.tree_indicator_collapsed = "+".to_string();
    config.file_explorer.tree_indicator_expanded = "-".to_string();
    // Make the explorer wide enough to comfortably render `+ subdir`.
    config.file_explorer.width = fresh::config::ExplorerWidth::Columns(30);

    let mut harness = EditorTestHarness::with_temp_project_and_config(120, 40, config).unwrap();
    let project_root = harness.project_dir().unwrap();
    // Two sibling subdirs so the root never reduces to a single child
    // (some auto-expand heuristics short-circuit on degenerate trees).
    fs::create_dir(project_root.join("subdir")).unwrap();
    fs::write(project_root.join("subdir/inner.txt"), "x").unwrap();
    fs::create_dir(project_root.join("other")).unwrap();
    fs::write(project_root.join("other/y.txt"), "y").unwrap();

    harness
        .send_key(KeyCode::Char('e'), KeyModifiers::CONTROL)
        .unwrap();
    harness
        .wait_until(|h| h.screen_to_string().contains("File Explorer"))
        .unwrap();

    // The root may or may not be auto-expanded depending on harness
    // state. If `subdir` isn't visible yet, expand by sending Right.
    if !harness.screen_to_string().contains("subdir") {
        harness
            .send_key(KeyCode::Right, KeyModifiers::NONE)
            .unwrap();
        harness
            .wait_until(|h| h.screen_to_string().contains("subdir"))
            .unwrap();
    }

    // Locate the line that contains "subdir" and confirm the configured
    // collapsed indicator ("+") precedes the name on that row.
    let screen = harness.screen_to_string();
    let subdir_line = screen
        .lines()
        .find(|l| l.contains("subdir"))
        .unwrap_or_else(|| panic!("subdir not found in:\n{screen}"));
    assert!(
        subdir_line.contains('+') && subdir_line.contains("subdir"),
        "expected '+' on the row for the collapsed directory.\n\
         Got line: {subdir_line:?}\nFull screen:\n{screen}"
    );
    // Default expanded glyph "▼" must not appear when overridden.
    assert!(
        !subdir_line.contains('▼'),
        "default expanded glyph leaked through when collapsed.\nLine: {subdir_line:?}"
    );
}

/// Locate the column of "File Explorer" in the screen output.
///
/// Returns the byte/column offset within the matching line. This is sufficient
/// to compare relative positions because the file-explorer title is rendered
/// with ASCII characters (1 byte = 1 column in the part we're matching).
fn explorer_title_column(screen: &str) -> Option<usize> {
    for line in screen.lines() {
        if let Some(idx) = line.find("File Explorer") {
            return Some(idx);
        }
    }
    None
}

/// Issue #1468: the file explorer can be moved between the left and right
/// side of the window via a single command-palette action. The action
/// flips `file_explorer.side` and the rendered title moves accordingly.
#[test]
fn test_toggle_file_explorer_side_moves_panel() {
    let mut harness = EditorTestHarness::with_temp_project(120, 40).unwrap();
    let project_root = harness.project_dir().unwrap();
    fs::write(project_root.join("a.txt"), "content").unwrap();

    // Open the file explorer (defaults to the left).
    harness.editor_mut().toggle_file_explorer();
    harness
        .wait_until(|h| h.screen_to_string().contains("File Explorer"))
        .unwrap();

    let screen_left = harness.screen_to_string();
    let col_left = explorer_title_column(&screen_left)
        .unwrap_or_else(|| panic!("File Explorer title not found:\n{screen_left}"));
    assert!(
        col_left < 30,
        "Expected file explorer on left side (col={col_left}):\n{screen_left}"
    );

    // Toggle to the right side.
    harness.editor_mut().toggle_file_explorer_side();
    harness.render().unwrap();
    let screen_right = harness.screen_to_string();
    let col_right = explorer_title_column(&screen_right)
        .unwrap_or_else(|| panic!("File Explorer title not found after toggle:\n{screen_right}"));
    assert!(
        col_right > col_left + 20,
        "Expected file explorer to move to the right (col_left={col_left}, col_right={col_right}):\n{screen_right}"
    );

    // Toggle back to the left.
    harness.editor_mut().toggle_file_explorer_side();
    harness.render().unwrap();
    let screen_left2 = harness.screen_to_string();
    let col_left2 = explorer_title_column(&screen_left2).unwrap_or_else(|| {
        panic!("File Explorer title not found after second toggle:\n{screen_left2}")
    });
    assert_eq!(
        col_left, col_left2,
        "Second toggle should return the explorer to its original left position"
    );
}

/// A configured custom ignore pattern hides matching entries from the tree.
///
/// Regression: `file_explorer.custom_ignore_patterns` was parsed from config but
/// never wired into the ignore matcher, so a file matching a configured pattern
/// still appeared in the explorer. Without that wiring this test fails — `debug.log`
/// shows up alongside `keep.rs`.
#[test]
fn test_file_explorer_applies_custom_ignore_patterns() {
    let mut config = Config::default();
    config.file_explorer.custom_ignore_patterns = vec!["*.log".to_string()];

    let mut harness =
        EditorTestHarness::with_temp_project_and_config_no_plugins(120, 40, config).unwrap();
    let project_root = harness.project_dir().unwrap();

    // A normal file (should appear) and one matching the configured pattern (should not).
    fs::write(project_root.join("keep.rs"), "fn main() {}").unwrap();
    fs::write(project_root.join("debug.log"), "noise").unwrap();

    // Open the file explorer.
    harness
        .send_key(KeyCode::Char('e'), KeyModifiers::CONTROL)
        .unwrap();

    // Wait until the directory listing has populated and settled (the non-ignored
    // file is on screen). `keep.rs` and `debug.log` are siblings loaded in the same
    // refresh, so once the tree is stable the filtering decision is final.
    harness
        .wait_until_stable(|h| h.screen_to_string().contains("keep.rs"))
        .unwrap();

    let screen = harness.screen_to_string();
    assert!(
        screen.contains("keep.rs"),
        "non-ignored file should be listed:\n{screen}"
    );
    assert!(
        !screen.contains("debug.log"),
        "file matching custom_ignore_patterns (*.log) should be hidden:\n{screen}"
    );
}

/// Regression (issue #2522): toggling the file explorer on a workspace whose
/// directory listing is slow — e.g. a remote SSH workspace — must *visibly*
/// show the explorer panel immediately, rather than leaving a blank gap until
/// the (possibly multi-second) async tree build completes.
///
/// The explorer tree is built asynchronously (`init_file_explorer` spawns a
/// task that lists the directory and only then installs the view). Before the
/// fix, the layout reserved the sidebar column only once `file_explorer` was
/// `Some`, so on a slow/remote backend the toggle appeared to do nothing until
/// the build landed. A slow `read_dir` reproduces the remote latency locally:
/// it stalls only the async build, so when we render right after the toggle the
/// tree is still `None` — yet the panel must already be on screen with its
/// final chrome ("File Explorer" title) and a "Loading…" body, and the tree
/// landing must change nothing but that body: no chrome pop, no layout shift.
#[test]
fn test_file_explorer_shows_placeholder_during_slow_build() {
    use fresh::services::fs::SlowFsConfig;
    use std::time::Duration;

    // Delay only directory listings. `is_dir`/`metadata` stay instant so the
    // synchronous root check in `init_file_explorer` doesn't block the main
    // thread; only the async `expand_node` -> `read_dir` is slow. 400ms is well
    // above the harness async-drain cap (~200ms), so the build is guaranteed to
    // still be in flight when we render right after the toggle.
    let mut slow = SlowFsConfig::none();
    slow.read_dir_delay = Duration::from_millis(400);

    let mut harness = EditorTestHarness::with_slow_fs(120, 40, slow).unwrap();
    // A file the built tree will list — proof the async build landed.
    let workdir = harness.temp_dir_path().unwrap().to_path_buf();
    fs::write(workdir.join("landed_marker.txt"), "hello\n").unwrap();

    // The explorer panel draws a bordered box whose top-left corner sits at
    // column 0; nothing else paints a `┌` at the start of a line, so it's a
    // reliable, locale-independent "the sidebar is on screen" signal.
    let panel_drawn =
        |h: &EditorTestHarness| h.screen_to_string().lines().any(|l| l.starts_with('┌'));

    // Sanity: the explorer starts hidden (no sidebar panel).
    harness.render().unwrap();
    assert!(!panel_drawn(&harness), "file explorer should start hidden");

    // Toggle the explorer on (Ctrl+B). The async build is still running when
    // `send_key`'s bounded drain returns, so `file_explorer` is still `None` —
    // yet the sidebar must already be on screen as a loading panel carrying
    // the final chrome, not a blank gap or an anonymous box.
    harness
        .send_key(KeyCode::Char('b'), KeyModifiers::CONTROL)
        .unwrap();
    harness.render().unwrap();

    let placeholder_frame = harness.screen_to_string();
    assert!(
        panel_drawn(&harness),
        "explorer panel (placeholder) must be visible while the slow build is \
         in progress, not a blank gap; screen:\n{placeholder_frame}"
    );
    assert!(
        placeholder_frame.contains("File Explorer"),
        "the loading placeholder must already carry the panel's final title; \
         screen:\n{placeholder_frame}"
    );
    assert!(
        placeholder_frame.contains("Loading"),
        "the loading placeholder must say it is loading; \
         screen:\n{placeholder_frame}"
    );
    // The chrome row (panel title + top border) is already final: record it
    // now and compare after the build lands.
    let chrome_row = |s: &str| {
        s.lines()
            .find(|l| l.starts_with('┌'))
            .map(str::to_string)
            .unwrap_or_default()
    };
    let placeholder_chrome = chrome_row(&placeholder_frame);

    // The async build must still land: the "Loading…" body is replaced by
    // the real tree...
    harness
        .wait_until(|h| h.screen_to_string().contains("landed_marker.txt"))
        .unwrap();
    // ...and that swap changed only the body — the chrome row is unchanged,
    // so the load caused no layout shift or title pop.
    assert_eq!(
        chrome_row(&harness.screen_to_string()),
        placeholder_chrome,
        "the tree landing must not change the panel chrome/layout"
    );
}

/// Test that git status decorations appear for sub-repos when the workspace root
/// is NOT a git repository (monorepo / multi-repo layout).
///
/// Layout:
///   /tmp/workspace/          (not a git repo)
///     project-a/             (git repo with modified file)
///     project-b/             (git repo with untracked file)
///
/// The git_explorer plugin should discover both sub-repos and show decorations.
#[test]
#[cfg_attr(windows, ignore)] // Git plugin tests are flaky on Windows CI
fn test_file_explorer_git_decorations_monorepo_subrepos() {
    use crate::common::git_test_helper::git_command;
    use crate::common::harness::{copy_plugin, copy_plugin_lib};
    use tempfile::TempDir;

    let workspace_dir = TempDir::new().expect("Failed to create workspace dir");
    let ws = workspace_dir.path();

    // Create project-a as a git sub-repo with a modified tracked file
    let project_a = ws.join("project-a");
    fs::create_dir_all(&project_a).unwrap();
    let output = git_command(&project_a).arg("init").output().unwrap();
    assert!(output.status.success(), "git init project-a failed");
    fs::write(project_a.join("file-a.txt"), "original").unwrap();
    git_command(&project_a).args(["add", "."]).output().unwrap();
    git_command(&project_a)
        .args(["commit", "-m", "init-a"])
        .output()
        .unwrap();
    fs::write(project_a.join("file-a.txt"), "modified").unwrap();

    // Create project-b as a git sub-repo with an untracked file
    let project_b = ws.join("project-b");
    fs::create_dir_all(&project_b).unwrap();
    let output = git_command(&project_b).arg("init").output().unwrap();
    assert!(output.status.success(), "git init project-b failed");
    fs::write(project_b.join("file-b.txt"), "tracked").unwrap();
    git_command(&project_b).args(["add", "."]).output().unwrap();
    git_command(&project_b)
        .args(["commit", "-m", "init-b"])
        .output()
        .unwrap();
    fs::write(project_b.join("new-file.txt"), "untracked content").unwrap();

    // Install the git_explorer plugin into the workspace
    let plugins_dir = ws.join("plugins");
    fs::create_dir_all(&plugins_dir).unwrap();
    copy_plugin(&plugins_dir, "git_explorer");
    copy_plugin_lib(&plugins_dir);

    let mut harness = EditorTestHarness::with_working_dir(120, 40, ws.to_path_buf()).unwrap();

    harness.editor_mut().toggle_file_explorer();
    harness
        .wait_until(|h| h.screen_to_string().contains("File Explorer"))
        .unwrap();

    // Wait for git_explorer to discover sub-repos and apply decorations.
    // project-a/file-a.txt should show M (modified), project-b/new-file.txt should show U (untracked).
    harness
        .wait_until(|h| {
            let screen = h.screen_to_string();
            screen
                .lines()
                .any(|line| line.contains("project-a") && line.contains("●"))
                || screen
                    .lines()
                    .any(|line| line.contains("file-a.txt") && line.contains("M"))
        })
        .unwrap();

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

    // Verify project-b also has a decoration (● on directory or U on new-file.txt)
    let has_project_b_decoration = screen
        .lines()
        .any(|line| line.contains("project-b") && line.contains("●"))
        || screen
            .lines()
            .any(|line| line.contains("new-file") && line.contains("U"));
    assert!(
        has_project_b_decoration,
        "project-b should show git status decorations in monorepo layout.\nScreen:\n{screen}"
    );
}

/// Regression test for #2592: git status decorations must appear for files in a
/// nested sub-repo even when the workspace root is *itself* a git repo (the
/// classic monorepo-with-vendored-repo layout). Previously the explorer only
/// ran `git status` in the outer repo, which sees the nested repo as a single
/// untracked directory and never reports the inner file's real status — so the
/// inner file rendered with no decoration while gutter/blame/Review Diff all
/// resolved the nested repo correctly.
///
/// Layout:
///   /tmp/outer/            (git repo; outer.py modified)
///     vendored/            (nested git repo; inner.py modified)
#[test]
#[cfg_attr(windows, ignore)] // Git plugin tests are flaky on Windows CI
fn test_file_explorer_git_decorations_nested_subrepo_under_repo_root() {
    use crate::common::git_test_helper::git_command;
    use crate::common::harness::{copy_plugin, copy_plugin_lib};
    use tempfile::TempDir;

    let outer_dir = TempDir::new().expect("Failed to create outer dir");
    let outer = outer_dir.path();

    // Outer repo with a committed-then-modified tracked file.
    let output = git_command(outer).arg("init").output().unwrap();
    assert!(output.status.success(), "git init outer failed");
    fs::write(outer.join("outer.py"), "outer_var = 1\n").unwrap();
    git_command(outer).args(["add", "."]).output().unwrap();
    git_command(outer)
        .args(["commit", "-m", "outer"])
        .output()
        .unwrap();
    fs::write(outer.join("outer.py"), "outer_var = 1\nouter_var2 = 2\n").unwrap();

    // Nested repo inside the outer repo, with its own modified tracked file.
    let vendored = outer.join("vendored");
    fs::create_dir_all(&vendored).unwrap();
    let output = git_command(&vendored).arg("init").output().unwrap();
    assert!(output.status.success(), "git init vendored failed");
    fs::write(vendored.join("inner.py"), "inner_a = 1\ninner_b = 2\n").unwrap();
    git_command(&vendored).args(["add", "."]).output().unwrap();
    git_command(&vendored)
        .args(["commit", "-m", "inner"])
        .output()
        .unwrap();
    fs::write(
        vendored.join("inner.py"),
        "inner_a = 1\ninner_b = 2\ninner_c = 3\n",
    )
    .unwrap();

    // Install the git_explorer plugin into the workspace.
    let plugins_dir = outer.join("plugins");
    fs::create_dir_all(&plugins_dir).unwrap();
    copy_plugin(&plugins_dir, "git_explorer");
    copy_plugin_lib(&plugins_dir);

    let mut harness = EditorTestHarness::with_working_dir(120, 40, outer.to_path_buf()).unwrap();

    // Open the nested file first, then reveal it in the explorer: Ctrl+B
    // auto-expands the tree down to the current file (issue #1569), so the
    // `inner.py` row appears without relying on ordering-sensitive keyboard
    // navigation into the `vendored` folder.
    harness.open_file(&vendored.join("inner.py")).unwrap();
    harness.render().unwrap();
    harness
        .send_key(KeyCode::Char('b'), KeyModifiers::CONTROL)
        .unwrap();

    // The `inner.py` *tree row* (a panel-body line starting with the `│`
    // border, not the tab strip whose header row starts with `┌`) must pick up
    // an `M` badge from the *nested* repo's own status. Before the fix the
    // explorer only ran `git status` in the outer repo, which reports the
    // sub-repo as a single untracked directory and never sees inner.py, so the
    // row rendered with no decoration.
    let is_inner_tree_row =
        |line: &str| line.starts_with('│') && line.contains("inner.py") && line.contains('M');
    harness
        .wait_until(|h| h.screen_to_string().lines().any(is_inner_tree_row))
        .unwrap();

    let screen = harness.screen_to_string();
    println!("Nested sub-repo under repo root:\n{screen}");
    let inner_line = screen
        .lines()
        .find(|l| l.starts_with('│') && l.contains("inner.py"))
        .unwrap_or("");
    assert!(
        inner_line.contains('M'),
        "inner.py tree row should show a modified (M) decoration from its own nested repo. Line: '{inner_line}'"
    );
}

/// `file_explorer.respect_gitignore = false` switches the `.gitignore` rules
/// off wholesale: ignored files show up without anyone touching the
/// "Show Gitignored Files" toggle. The setting was previously parsed, merged
/// and offered in the Settings UI but never read (issue #2842).
#[test]
fn test_respect_gitignore_false_shows_ignored_files() {
    let mut config = Config::default();
    config.file_explorer.respect_gitignore = false;

    let mut harness = EditorTestHarness::with_temp_project_and_config(120, 40, config).unwrap();
    let project_root = harness.project_dir().unwrap();

    fs::write(project_root.join(".gitignore"), "build_output.txt\n").unwrap();
    fs::write(project_root.join("build_output.txt"), "generated").unwrap();
    fs::write(project_root.join("visible_file.txt"), "visible").unwrap();

    harness.editor_mut().focus_file_explorer();
    harness.wait_for_file_explorer().unwrap();
    harness
        .wait_for_file_explorer_item("visible_file.txt")
        .unwrap();

    let screen = harness.screen_to_string();
    assert!(
        screen.contains("build_output.txt"),
        "a gitignored file should still be listed when respect_gitignore is \
         off, even with 'Show Gitignored Files' left off.\nScreen:\n{}",
        screen
    );
}

/// The default (`respect_gitignore = true`) keeps ignored files out, so the
/// test above is measuring the setting and not just a broken .gitignore.
#[test]
fn test_respect_gitignore_true_hides_ignored_files() {
    let mut harness = EditorTestHarness::with_temp_project(120, 40).unwrap();
    let project_root = harness.project_dir().unwrap();

    fs::write(project_root.join(".gitignore"), "build_output.txt\n").unwrap();
    fs::write(project_root.join("build_output.txt"), "generated").unwrap();
    fs::write(project_root.join("visible_file.txt"), "visible").unwrap();

    harness.editor_mut().focus_file_explorer();
    harness.wait_for_file_explorer().unwrap();
    harness
        .wait_for_file_explorer_item("visible_file.txt")
        .unwrap();

    let screen = harness.screen_to_string();
    assert!(
        !screen.contains("build_output.txt"),
        "a gitignored file must stay hidden under the default \
         respect_gitignore = true.\nScreen:\n{}",
        screen
    );
}
