use crate::state::{EditorState, EditorMode, TextBuffer, YankHighlight};
use crate::action::Action;
use crate::reducer::editor::apply_editor_event;
use crate::reducer::reduce;

#[test]
fn test_edit_mode_navigation_unaffected() {
    let mut editor = EditorState::new(None);
    // Default resting mode is Edit (new(None) starts at Welcome; force Edit + content)
    editor.enter_mode(EditorMode::Edit);
    editor.buffer = TextBuffer::from_lines(vec!["main 1".to_string(), "main 2".to_string()]);
    
    apply_editor_event(&mut editor, &Action::MoveDown);
    assert_eq!(editor.cursor.y, 1);
}

#[test]
fn test_toggle_case_with_count() {
    let mut editor = EditorState::new(None);
    editor.enter_mode(EditorMode::Glide);
    editor.buffer = TextBuffer::from_lines(vec!["Hello".to_string()]);
    editor.cursor.x = 0;
    // `3~` toggles the first three chars and advances the cursor.
    editor.glide_count.push('3');
    apply_editor_event(&mut editor, &Action::ToggleCase);
    assert_eq!(editor.buffer.lines[0], "hELlo");
    assert_eq!(editor.cursor.x, 3);
    assert!(editor.modified);
}

#[test]
fn test_toggle_case_is_undoable() {
    // Regression: ~ must snapshot before mutating, or undo restores the
    // already-toggled buffer (a no-op) instead of the original.
    let mut editor = EditorState::new(None);
    editor.enter_mode(EditorMode::Glide);
    editor.buffer = TextBuffer::from_lines(vec!["Hello".to_string()]);
    editor.cursor.x = 0;
    editor.glide_count.push('3');
    reduce(&mut editor, Action::ToggleCase);
    assert_eq!(editor.buffer.lines[0], "hELlo");
    reduce(&mut editor, Action::Undo);
    assert_eq!(editor.buffer.lines[0], "Hello"); // fully reverted in one step
}

#[test]
fn test_dot_comma_repeat_last_find() {
    use crate::ui::Keymap;
    use crate::state::key::{KeyCode, KeyModifiers};
    let mut editor = EditorState::new(None);
    editor.enter_mode(EditorMode::Glide);
    editor.buffer = TextBuffer::from_lines(vec!["axbxcx".to_string()]); // x at 1,3,5
    editor.cursor.x = 0;
    // `>x`: jump onto the first 'x' (index 1), recording last_find.
    reduce(&mut editor, Action::GlideMove(crate::glide::Motion::FindChar('x')));
    assert_eq!(editor.cursor.x, 1);
    // `.` repeats forward -> next 'x' at index 3.
    let dot = Keymap::map_key_to_action(&editor, KeyCode::Char('.'), KeyModifiers::NONE).unwrap();
    reduce(&mut editor, dot);
    assert_eq!(editor.cursor.x, 3);
    // `,` repeats backward -> previous 'x' at index 1.
    let comma = Keymap::map_key_to_action(&editor, KeyCode::Char(','), KeyModifiers::NONE).unwrap();
    reduce(&mut editor, comma);
    assert_eq!(editor.cursor.x, 1);
}

#[test]
fn test_bare_till_jump_moves_cursor() {
    // Bare `t)` (no operator pending) moves the cursor to just before ')'.
    let mut editor = EditorState::new(None);
    editor.enter_mode(EditorMode::Glide);
    editor.buffer = TextBuffer::from_lines(vec!["hello)world".to_string()]);
    editor.cursor.x = 0;
    reduce(&mut editor, Action::GlideMove(crate::glide::Motion::TillChar(')')));
    assert_eq!(editor.cursor.x, 4); // one char before ')'
}

#[test]
fn test_glide_backspace_deletes_char_before_cursor() {
    // Regression: Glide `X` (Action::Backspace) must route through reduce() to
    // handle_backspace, not fall into the no-op default arm.
    let mut editor = EditorState::new(None);
    editor.enter_mode(EditorMode::Glide);
    editor.buffer = TextBuffer::from_lines(vec!["abc".to_string()]);
    editor.cursor.x = 2; // before 'c'
    reduce(&mut editor, Action::Backspace);
    assert_eq!(editor.buffer.lines[0], "ac"); // 'b' removed
    assert_eq!(editor.cursor.x, 1);
}

#[test]
fn test_save_bare_filename_uses_current_dir() {
    // Regression: a filename without a directory component has parent() == Some(""),
    // and `"".exists()` is false. That empty parent means the current directory and
    // must NOT be rejected as "Directory not found" — otherwise every bare-name save fails.
    use std::io::Read;
    let dir = std::env::temp_dir().join(format!("cozy_save_test_{}", std::process::id()));
    std::fs::create_dir_all(&dir).unwrap();
    let prev = std::env::current_dir().unwrap();
    std::env::set_current_dir(&dir).unwrap();

    let mut editor = EditorState::new(None);
    editor.enter_mode(EditorMode::Edit);
    editor.buffer = TextBuffer::from_lines(vec!["hello".to_string()]);
    editor.filename = Some(std::path::PathBuf::from("bare.txt"));

    let result = editor.save();

    // Restore CWD before asserting so a failure can't leave the test process in temp.
    std::env::set_current_dir(&prev).unwrap();

    assert!(result.is_ok(), "bare filename save must succeed, got: {:?}", result.err());
    let mut contents = String::new();
    std::fs::File::open(dir.join("bare.txt")).unwrap()
        .read_to_string(&mut contents).unwrap();
    assert_eq!(contents, "hello\n");

    let _ = std::fs::remove_dir_all(&dir);
}

#[test]
fn test_save_success_status_survives_into_edit_mode() {
    // Regression: enter_mode(Edit) clears status_message. Save set "Saved" BEFORE the
    // mode switch, so it was wiped instantly and the user saw no confirmation. The
    // success message must be set AFTER entering Edit so it persists.
    // Uses an absolute path (no CWD change) so it can't race the bare-filename test.
    let dir = std::env::temp_dir().join(format!("cozy_save_status_{}", std::process::id()));
    std::fs::create_dir_all(&dir).unwrap();
    let path = dir.join("note.txt");

    let mut editor = EditorState::new(None);
    editor.enter_mode(EditorMode::Edit);
    editor.buffer = TextBuffer::from_lines(vec!["x".to_string()]);
    editor.filename = Some(path.clone());

    reduce(&mut editor, Action::Save(path.to_string_lossy().to_string()));

    assert_eq!(editor.mode, EditorMode::Edit);
    assert!(
        editor.status_message.as_deref().unwrap_or("").contains("Saved"),
        "expected a 'Saved' status after save, got: {:?}", editor.status_message
    );

    let _ = std::fs::remove_dir_all(&dir);
}

#[test]
fn test_save_and_exit_saves_then_exits() {
    // Regression: Ctrl+X "Save and Exit" mapped Enter to Action::Quit, which exited
    // WITHOUT saving (data loss). SaveAndExit must write the file, then exit.
    use std::io::Read;
    let dir = std::env::temp_dir().join(format!("cozy_save_exit_{}", std::process::id()));
    std::fs::create_dir_all(&dir).unwrap();
    let path = dir.join("bye.txt");

    let mut editor = EditorState::new(None);
    editor.enter_mode(EditorMode::Quit);
    editor.buffer = TextBuffer::from_lines(vec!["data".to_string()]);
    editor.filename = Some(path.clone());

    let result = reduce(&mut editor, Action::SaveAndExit(path.to_string_lossy().to_string()));

    assert!(matches!(result, crate::reducer::EventResult::Exit), "must exit on successful save");
    let mut contents = String::new();
    std::fs::File::open(&path).unwrap().read_to_string(&mut contents).unwrap();
    assert_eq!(contents, "data\n");

    let _ = std::fs::remove_dir_all(&dir);
}

#[test]
fn test_save_and_exit_stays_on_failure() {
    // An empty filename with no current file can't be saved → must NOT exit (avoid
    // data loss), so the user can fix the name or discard explicitly with Ctrl+Q.
    let mut editor = EditorState::new(None);
    editor.enter_mode(EditorMode::Quit);
    editor.buffer = TextBuffer::from_lines(vec!["data".to_string()]);
    editor.filename = None;

    let result = reduce(&mut editor, Action::SaveAndExit(String::new()));

    assert!(matches!(result, crate::reducer::EventResult::Continue), "must not exit when save fails");
}

#[test]
fn test_yank_highlight_cleared_on_next_keypress() {
    let mut editor = EditorState::new(None);
    editor.enter_mode(EditorMode::Glide);
    editor.buffer = TextBuffer::from_lines(vec!["abc".to_string()]);
    // Simulate a yank having armed the flash.
    editor.yank_highlight = Some(YankHighlight { start: (0, 0), end: (0, 3), linewise: false });
    // Any subsequent action goes through reduce(), which clears the flash.
    reduce(&mut editor, Action::MoveRight);
    assert!(editor.yank_highlight.is_none());
}
