//! Tests for buffer settings commands:
//! - Set Tab Size
//! - Toggle Indentation: Spaces ↔ Tabs
//! - Toggle Tab Indicators
//! - Toggle Line Numbers
//! - Reset Buffer Settings

use crate::common::harness::{EditorTestHarness, HarnessOptions};
use crossterm::event::{KeyCode, KeyModifiers};
use fresh::config::Config;
use std::fs::File;
use std::io::Write;
use std::path::Path;
use std::time::Duration;
use tempfile::TempDir;

/// Helper to run a command from the command palette
fn run_command(harness: &mut EditorTestHarness, command_name: &str) {
    // Open command palette with Ctrl+P
    harness
        .send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
        .unwrap();
    harness.render().unwrap();

    // Type the command name
    harness.type_text(command_name).unwrap();
    harness.render().unwrap();

    // Press Enter to execute
    harness
        .send_key(KeyCode::Enter, KeyModifiers::NONE)
        .unwrap();
    harness.render().unwrap();
}

/// Test that "Toggle Indentation" command toggles between spaces and tabs
#[test]
fn test_toggle_indentation_command() {
    let temp_dir = TempDir::new().unwrap();
    let file_path = temp_dir.path().join("test.go");

    // Create an empty Go file (defaults to use_tabs=true)
    std::fs::write(&file_path, "").unwrap();

    let config = Config::default();
    let mut harness = EditorTestHarness::with_config(80, 24, config).unwrap();
    harness.open_file(&file_path).unwrap();
    harness.render().unwrap();

    // Verify initial state: Tab should insert a tab character in Go files
    harness.send_key(KeyCode::Tab, KeyModifiers::NONE).unwrap();
    let content = harness.get_buffer_content().unwrap();
    assert_eq!(content, "\t", "Initially, Go should use tabs");

    // Undo to clear the tab
    harness
        .send_key(KeyCode::Char('z'), KeyModifiers::CONTROL)
        .unwrap();
    harness.render().unwrap();

    // Run "Toggle Indentation" command to switch to spaces
    run_command(&mut harness, "Toggle Indentation: Spaces");

    // Now Tab should insert spaces
    harness.send_key(KeyCode::Tab, KeyModifiers::NONE).unwrap();
    let content = harness.get_buffer_content().unwrap();
    assert!(
        !content.contains('\t'),
        "After toggle, should insert spaces, not tabs. Got: {:?}",
        content
    );
    assert!(
        content.contains("    "),
        "After toggle, should have 4 spaces. Got: {:?}",
        content
    );

    // Undo and toggle again to switch back to tabs
    harness
        .send_key(KeyCode::Char('z'), KeyModifiers::CONTROL)
        .unwrap();
    harness.render().unwrap();

    run_command(&mut harness, "Toggle Indentation: Spaces");

    harness.send_key(KeyCode::Tab, KeyModifiers::NONE).unwrap();
    let content = harness.get_buffer_content().unwrap();
    assert_eq!(
        content, "\t",
        "After second toggle, should be back to tabs. Got: {:?}",
        content
    );
}

/// Test that "Toggle Tab Indicators" toggles visibility
#[test]
fn test_toggle_tab_indicators_command() {
    let temp_dir = TempDir::new().unwrap();
    let file_path = temp_dir.path().join("test.go");

    // Create a Go file with a tab character
    std::fs::write(&file_path, "\thello").unwrap();

    let config = Config::default();
    let mut harness = EditorTestHarness::with_config(80, 24, config).unwrap();
    harness.open_file(&file_path).unwrap();
    harness.render().unwrap();

    // Go files hide tab indicators by default
    let screen_before = harness.screen_to_string();
    assert!(
        !screen_before.contains('→'),
        "Go files should hide tab indicators by default"
    );

    // Run "Toggle Tab Indicators" command
    run_command(&mut harness, "Toggle Tab Indicators");

    // Now tab indicators should be visible
    let screen_after = harness.screen_to_string();
    assert!(
        screen_after.contains('→'),
        "After toggle, tab indicators should be visible. Screen:\n{}",
        screen_after
    );

    // Toggle again - should hide them
    run_command(&mut harness, "Toggle Tab Indicators");

    let screen_final = harness.screen_to_string();
    assert!(
        !screen_final.contains('→'),
        "After second toggle, tab indicators should be hidden again"
    );
}

/// Regression test for #2579: the whitespace master toggle could turn indicators
/// off but not visibly back on. Turning back on restored the hard-coded default
/// (tabs on / spaces off) instead of the user's configured visibility, so a user
/// who enabled space indicators saw nothing return on a space-indented file
/// (a restart re-resolved from config, which is why it "worked again").
#[test]
fn test_toggle_whitespace_indicators_restores_configured_spaces() {
    let temp_dir = TempDir::new().unwrap();
    let file_path = temp_dir.path().join("test.txt");

    // Leading spaces so the space indicator (·) has something to mark.
    std::fs::write(&file_path, "    hello\n").unwrap();

    // User configures space indicators on, tab indicators off.
    let mut config = Config::default();
    config.editor.whitespace_show = true;
    config.editor.whitespace_spaces_leading = true;
    config.editor.whitespace_spaces_inner = false;
    config.editor.whitespace_spaces_trailing = false;
    config.editor.whitespace_tabs_leading = false;
    config.editor.whitespace_tabs_inner = false;
    config.editor.whitespace_tabs_trailing = false;

    let mut harness = EditorTestHarness::with_config(80, 24, config).unwrap();
    harness.open_file(&file_path).unwrap();
    harness.render().unwrap();

    // Configured space indicators are visible on open.
    let screen_initial = harness.screen_to_string();
    assert!(
        screen_initial.contains('·'),
        "Space indicators should be visible on open. Screen:\n{}",
        screen_initial
    );

    // Toggle off — indicators disappear.
    run_command(&mut harness, "Toggle Whitespace Indicators");
    let screen_off = harness.screen_to_string();
    assert!(
        !screen_off.contains('·'),
        "After toggling off, space indicators should be hidden. Screen:\n{}",
        screen_off
    );

    // Toggle back on — the *configured* space indicators must return, not the
    // hard-coded default (which shows only tab indicators, invisible here).
    run_command(&mut harness, "Toggle Whitespace Indicators");
    let screen_on = harness.screen_to_string();
    assert!(
        screen_on.contains('·'),
        "After toggling back on, configured space indicators should return. Screen:\n{}",
        screen_on
    );
}

/// Regression test for #2580: whitespace indicators did not show in a brand-new
/// (unsaved) buffer. `new_buffer` left `buffer_settings.whitespace` at its
/// hard-coded default (tabs on / spaces off) instead of resolving it from the
/// user's editor config the way the file-open path does, so a user who enabled
/// space indicators saw nothing on a fresh buffer even though opened files
/// showed them.
#[test]
fn test_new_buffer_shows_configured_whitespace_indicators() {
    // User configures space indicators on, tab indicators off.
    let mut config = Config::default();
    config.editor.whitespace_show = true;
    config.editor.whitespace_spaces_leading = true;
    config.editor.whitespace_spaces_inner = false;
    config.editor.whitespace_spaces_trailing = false;
    config.editor.whitespace_tabs_leading = false;
    config.editor.whitespace_tabs_inner = false;
    config.editor.whitespace_tabs_trailing = false;

    let mut harness = EditorTestHarness::with_config(80, 24, config).unwrap();

    // Create a brand-new, never-saved buffer (File > New).
    harness.new_buffer().unwrap();

    // Type some leading spaces so the space indicator (·) has something to mark.
    harness.type_text("    hello").unwrap();
    harness.render().unwrap();

    let screen = harness.screen_to_string();
    assert!(
        screen.contains('·'),
        "Configured space indicators should be visible in a new buffer. Screen:\n{}",
        screen
    );
}

/// Regression test for #2580 follow-up: changing a buffer's language via the
/// "Set Language" command must re-resolve whitespace indicators so the new
/// language's `show_whitespace_tabs` override takes effect immediately.
///
/// A Go buffer hides tab indicators (`show_whitespace_tabs: false`); switching
/// it to Plain Text (which shows them) must make the "→" reappear. Previously
/// `Set Language` only swapped the highlighter and left the stale Go visibility
/// in place, so the tab stayed unmarked until the file was reopened.
///
/// The switch is driven through the unambiguous "Plain Text" entry and the
/// result is asserted purely on rendered output (the "→" glyph), per the
/// "E2E Tests Observe, Not Inspect" guideline.
#[test]
fn test_set_language_updates_whitespace_tab_override() {
    let mut harness = EditorTestHarness::create(
        100,
        30,
        HarnessOptions::new()
            .with_config(Config::default())
            .with_project_root()
            .with_full_grammar_registry(),
    )
    .unwrap();
    let project_dir = harness.project_dir().unwrap();

    // A Go file whose first line starts with a real tab. Go's config sets
    // show_whitespace_tabs=false, so the tab is NOT marked with "→" on open.
    let go_file = project_dir.join("sample.go");
    std::fs::write(&go_file, "\thello\n").unwrap();
    harness.open_file(&go_file).unwrap();
    harness.render().unwrap();

    let before = harness.screen_to_string();
    assert!(
        !before.contains('→'),
        "Go hides tab indicators, so no → should show on open. Screen:\n{}",
        before
    );

    // Switch the language to Plain Text, which shows tab indicators by default.
    run_command(&mut harness, "Set Language");
    harness.wait_for_prompt().unwrap();
    harness.type_text("Plain Text").unwrap();
    harness.render().unwrap();
    harness
        .send_key(KeyCode::Enter, KeyModifiers::NONE)
        .unwrap();
    harness.render().unwrap();

    let after = harness.screen_to_string();
    assert!(
        after.contains('→'),
        "After switching to Plain Text, the tab indicator should reappear. \
         Screen:\n{}",
        after
    );
}

/// Test that "Set Tab Size" command changes tab rendering width
#[test]
fn test_set_tab_size_command() {
    let temp_dir = TempDir::new().unwrap();
    let file_path = temp_dir.path().join("test.txt");

    // Create a file with a tab followed by a marker
    std::fs::write(&file_path, "\tX").unwrap();

    // Start with tab_size = 4 (default)
    let config = Config::default();
    let mut harness = EditorTestHarness::with_config(80, 24, config).unwrap();
    harness.open_file(&file_path).unwrap();
    harness.render().unwrap();

    // Helper to get X visual column relative to tab indicator
    fn get_x_visual_offset(screen: &str) -> Option<usize> {
        for line in screen.lines() {
            if line.contains('X') {
                let mut indicator_col = None;
                let mut x_col = None;
                for (col, ch) in line.chars().enumerate() {
                    if ch == '→' {
                        indicator_col = Some(col);
                    }
                    if ch == 'X' {
                        x_col = Some(col);
                    }
                }
                return Some(x_col? - indicator_col?);
            }
        }
        None
    }

    // Get initial offset (tab_size=4)
    let screen_4 = harness.screen_to_string();
    let offset_4 = get_x_visual_offset(&screen_4).unwrap();
    assert_eq!(
        offset_4, 4,
        "With default tab_size=4, X should be 4 columns after indicator"
    );

    // Run "Set Tab Size" command and enter "8"
    harness
        .send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
        .unwrap();
    harness.render().unwrap();
    harness.type_text("Set Tab Size").unwrap();
    harness.render().unwrap();
    harness
        .send_key(KeyCode::Enter, KeyModifiers::NONE)
        .unwrap();
    harness.render().unwrap();

    // Clear existing text and type "8"
    harness
        .send_key(KeyCode::Char('a'), KeyModifiers::CONTROL)
        .unwrap(); // Select all
    harness.type_text("8").unwrap();
    harness
        .send_key(KeyCode::Enter, KeyModifiers::NONE)
        .unwrap();
    harness.render().unwrap();

    // Get new offset (tab_size=8)
    let screen_8 = harness.screen_to_string();
    let offset_8 = get_x_visual_offset(&screen_8).unwrap();
    assert_eq!(
        offset_8, 8,
        "After setting tab_size=8, X should be 8 columns after indicator. Screen:\n{}",
        screen_8
    );
}

/// Test that "Reset Buffer Settings" restores config defaults
#[test]
fn test_reset_buffer_settings_command() {
    let temp_dir = TempDir::new().unwrap();
    let file_path = temp_dir.path().join("test.go");

    // Create a Go file with a tab character
    std::fs::write(&file_path, "\thello").unwrap();

    let config = Config::default();
    let mut harness = EditorTestHarness::with_config(80, 24, config).unwrap();
    harness.open_file(&file_path).unwrap();
    harness.render().unwrap();

    // Initial state: Go hides tab indicators and uses tabs
    let screen_initial = harness.screen_to_string();
    assert!(
        !screen_initial.contains('→'),
        "Go files should hide tab indicators initially"
    );

    // Modify settings: toggle tab indicators and toggle indentation to spaces
    run_command(&mut harness, "Toggle Tab Indicators");
    run_command(&mut harness, "Toggle Indentation: Spaces"); // Go uses tabs by default, so toggle switches to spaces

    // Verify modifications took effect
    let screen_modified = harness.screen_to_string();
    assert!(
        screen_modified.contains('→'),
        "After toggle, tab indicators should be visible"
    );

    // Type a tab to verify spaces are inserted
    harness
        .send_key(KeyCode::End, KeyModifiers::CONTROL)
        .unwrap();
    harness.send_key(KeyCode::Tab, KeyModifiers::NONE).unwrap();
    let content_modified = harness.get_buffer_content().unwrap();
    assert!(
        content_modified.contains("    "),
        "After toggling indentation, Tab should insert spaces"
    );

    // Undo the tab
    harness
        .send_key(KeyCode::Char('z'), KeyModifiers::CONTROL)
        .unwrap();
    harness.render().unwrap();

    // Reset buffer settings
    run_command(&mut harness, "Reset Buffer Settings");

    // Verify settings are restored to Go defaults
    let screen_reset = harness.screen_to_string();
    assert!(
        !screen_reset.contains('→'),
        "After reset, Go should hide tab indicators again. Screen:\n{}",
        screen_reset
    );

    // Verify tabs are restored - type a tab
    harness
        .send_key(KeyCode::End, KeyModifiers::CONTROL)
        .unwrap();
    harness.send_key(KeyCode::Tab, KeyModifiers::NONE).unwrap();
    let content_reset = harness.get_buffer_content().unwrap();
    assert!(
        content_reset.ends_with('\t'),
        "After reset, Go should use tabs again. Got: {:?}",
        content_reset
    );
}

/// Delay between file writes to ensure filesystem notifications are received.
const FILE_CHANGE_DELAY: Duration = Duration::from_millis(2100);

/// Write content to a file and sync to disk to ensure filesystem notifications fire.
fn write_and_sync(path: &Path, content: &str) {
    let mut file = File::create(path).unwrap();
    file.write_all(content.as_bytes()).unwrap();
    file.sync_all().unwrap();
    drop(file);

    // Also sync the parent directory to ensure the directory entry is flushed
    if let Some(parent) = path.parent() {
        if let Ok(dir) = File::open(parent) {
            let _ = dir.sync_all();
        }
    }
}

/// Test that "Toggle Line Numbers" persists across external file changes and saves.
///
/// This test verifies that when line numbers are toggled off, the setting is
/// preserved through:
/// 1. External file modifications (auto-revert)
/// 2. User edits
/// 3. File saves
#[test]
#[cfg_attr(target_os = "macos", ignore)] // FSEvents coalescing can cause flaky timing
fn test_toggle_line_numbers_persists_across_file_changes() {
    let temp_dir = TempDir::new().unwrap();
    let file_path = temp_dir.path().join("test.txt");

    // Create initial file with some content
    write_and_sync(&file_path, "Initial line 1\nInitial line 2\nInitial line 3");

    let config = Config::default();
    let mut harness = EditorTestHarness::with_config(80, 24, config).unwrap();
    harness.open_file(&file_path).unwrap();
    harness.render().unwrap();

    // Step 1: Verify line numbers are enabled by default
    let screen_initial = harness.screen_to_string();
    assert!(
        screen_initial.contains("1 │"),
        "Line numbers should be visible by default. Screen:\n{}",
        screen_initial
    );

    // Step 2: Disable line numbers using command palette
    run_command(&mut harness, "Toggle Line Numbers");

    // Step 3: Verify line numbers are now hidden
    let screen_after_toggle = harness.screen_to_string();
    assert!(
        !screen_after_toggle.contains("1 │"),
        "Line numbers should be hidden after toggle. Screen:\n{}",
        screen_after_toggle
    );
    assert!(
        !screen_after_toggle.contains("2 │"),
        "Line numbers should be hidden after toggle. Screen:\n{}",
        screen_after_toggle
    );

    // Step 4: Overwrite the file externally (not using the editor)
    harness.sleep(FILE_CHANGE_DELAY);
    write_and_sync(
        &file_path,
        "Modified line 1\nModified line 2\nModified line 3\nModified line 4",
    );

    // Wait for auto-revert to process the external change
    let expected_content = "Modified line 1\nModified line 2\nModified line 3\nModified line 4";
    harness
        .wait_until(|h| h.get_buffer_content().unwrap() == expected_content)
        .expect("Auto-revert should update buffer content");

    // Step 5: Verify line numbers are still disabled after auto-revert
    harness.render().unwrap();
    let screen_after_revert = harness.screen_to_string();
    assert!(
        !screen_after_revert.contains("1 │"),
        "Line numbers should remain hidden after auto-revert. Screen:\n{}",
        screen_after_revert
    );
    assert!(
        !screen_after_revert.contains("2 │"),
        "Line numbers should remain hidden after auto-revert. Screen:\n{}",
        screen_after_revert
    );
    // Verify the new content is displayed
    harness.assert_screen_contains("Modified line 1");

    // Step 6: Make an edit in the buffer
    harness
        .send_key(KeyCode::End, KeyModifiers::CONTROL)
        .unwrap();
    harness.type_text("\nEdited line 5").unwrap();
    harness.render().unwrap();

    // Step 7: Verify line numbers are still disabled after editing
    let screen_after_edit = harness.screen_to_string();
    assert!(
        !screen_after_edit.contains("1 │"),
        "Line numbers should remain hidden after editing. Screen:\n{}",
        screen_after_edit
    );
    assert!(
        !screen_after_edit.contains("5 │"),
        "Line numbers should remain hidden after editing. Screen:\n{}",
        screen_after_edit
    );

    // Step 8: Save the file
    harness
        .send_key(KeyCode::Char('s'), KeyModifiers::CONTROL)
        .unwrap();
    harness.render().unwrap();

    // Step 9: Verify line numbers are still disabled after saving
    let screen_after_save = harness.screen_to_string();
    assert!(
        !screen_after_save.contains("1 │"),
        "Line numbers should remain hidden after save. Screen:\n{}",
        screen_after_save
    );
    assert!(
        !screen_after_save.contains("5 │"),
        "Line numbers should remain hidden after save. Screen:\n{}",
        screen_after_save
    );

    // Verify the edited content is still visible
    harness.assert_screen_contains("Edited line 5");
}

// ===========================================================================
// Line-ending indicator tests
// (`whitespace_newlines` / `whitespace_carriage_returns`)
// ===========================================================================

/// Newline indicators (↵) render at the end of every line of an LF file when
/// `whitespace_newlines` is enabled, and the master toggle hides and restores
/// them like any other whitespace indicator.
#[test]
fn test_newline_indicators_lf_file() {
    let temp_dir = TempDir::new().unwrap();
    let file_path = temp_dir.path().join("test.txt");
    std::fs::write(&file_path, "one\ntwo\n").unwrap();

    let mut config = Config::default();
    config.editor.whitespace_show = true;
    config.editor.whitespace_newlines = true;

    let mut harness = EditorTestHarness::with_config(80, 24, config).unwrap();
    harness.open_file(&file_path).unwrap();
    harness.render().unwrap();

    let screen = harness.screen_to_string();
    assert!(
        screen.contains("one↵") && screen.contains("two↵"),
        "Newline indicators should render at each line end. Screen:\n{}",
        screen
    );

    // Master toggle hides the newline indicators.
    run_command(&mut harness, "Toggle Whitespace Indicators");
    let screen_off = harness.screen_to_string();
    assert!(
        !screen_off.contains('↵'),
        "Master toggle off should hide newline indicators. Screen:\n{}",
        screen_off
    );

    // Toggling back on restores the configured newline indicators.
    run_command(&mut harness, "Toggle Whitespace Indicators");
    let screen_on = harness.screen_to_string();
    assert!(
        screen_on.contains("one↵"),
        "Master toggle on should restore newline indicators. Screen:\n{}",
        screen_on
    );
}

/// A CRLF buffer's line break carries both halves: with carriage returns and
/// newlines enabled each line ends with `␍↵`.
#[test]
fn test_crlf_line_ending_indicators() {
    let temp_dir = TempDir::new().unwrap();
    let file_path = temp_dir.path().join("test.txt");
    std::fs::write(&file_path, "one\r\ntwo\r\n").unwrap();

    let mut config = Config::default();
    config.editor.whitespace_show = true;
    config.editor.whitespace_newlines = true;
    config.editor.whitespace_carriage_returns = true;

    let mut harness = EditorTestHarness::with_config(80, 24, config).unwrap();
    harness.open_file(&file_path).unwrap();
    harness.render().unwrap();

    let screen = harness.screen_to_string();
    assert!(
        screen.contains("one␍↵") && screen.contains("two␍↵"),
        "CRLF line endings should render both CR and newline indicators. Screen:\n{}",
        screen
    );
}

/// With only `whitespace_newlines` enabled, a CRLF buffer shows just the
/// newline half; with only `whitespace_carriage_returns`, just the CR half.
#[test]
fn test_crlf_indicator_halves_are_independent() {
    let temp_dir = TempDir::new().unwrap();
    let file_path = temp_dir.path().join("test.txt");
    std::fs::write(&file_path, "one\r\ntwo\r\n").unwrap();

    let mut config = Config::default();
    config.editor.whitespace_show = true;
    config.editor.whitespace_newlines = true;
    config.editor.whitespace_carriage_returns = false;

    let mut harness = EditorTestHarness::with_config(80, 24, config).unwrap();
    harness.open_file(&file_path).unwrap();
    harness.render().unwrap();

    let screen = harness.screen_to_string();
    assert!(
        screen.contains("one↵") && !screen.contains('␍'),
        "Newlines-only should render ↵ without ␍ on CRLF files. Screen:\n{}",
        screen
    );

    let mut config = Config::default();
    config.editor.whitespace_show = true;
    config.editor.whitespace_newlines = false;
    config.editor.whitespace_carriage_returns = true;

    let mut harness = EditorTestHarness::with_config(80, 24, config).unwrap();
    harness.open_file(&file_path).unwrap();
    harness.render().unwrap();

    let screen = harness.screen_to_string();
    assert!(
        screen.contains("one␍") && !screen.contains('↵'),
        "CR-only should render ␍ without ↵ on CRLF files. Screen:\n{}",
        screen
    );
}

/// A Classic-Mac (CR) buffer saves its line breaks as `\r`; with carriage
/// returns enabled its line ends show the CR indicator.
#[test]
fn test_cr_buffer_line_ending_indicator() {
    let temp_dir = TempDir::new().unwrap();
    let file_path = temp_dir.path().join("test.txt");
    std::fs::write(&file_path, "one\rtwo\rthree\r").unwrap();

    let mut config = Config::default();
    config.editor.whitespace_show = true;
    config.editor.whitespace_carriage_returns = true;

    let mut harness = EditorTestHarness::with_config(80, 24, config).unwrap();
    harness.open_file(&file_path).unwrap();
    harness.render().unwrap();

    let screen = harness.screen_to_string();
    assert!(
        screen.contains("one␍") && screen.contains("two␍"),
        "CR buffers should render the CR indicator at line ends. Screen:\n{}",
        screen
    );
}

/// Line-ending indicators are off by default — a default config must not
/// suddenly decorate every line end.
#[test]
fn test_line_ending_indicators_off_by_default() {
    let temp_dir = TempDir::new().unwrap();
    let file_path = temp_dir.path().join("test.txt");
    std::fs::write(&file_path, "one\r\ntwo\r\n").unwrap();

    let config = Config::default();
    let mut harness = EditorTestHarness::with_config(80, 24, config).unwrap();
    harness.open_file(&file_path).unwrap();
    harness.render().unwrap();

    let screen = harness.screen_to_string();
    assert!(
        !screen.contains('↵') && !screen.contains('␍'),
        "No line-ending indicators should render by default. Screen:\n{}",
        screen
    );
}
