use crossterm::event::{Event, KeyCode as K, KeyEvent, KeyModifiers as M};
use std::{
    fs,
    path::PathBuf,
    sync::atomic::{AtomicUsize, Ordering},
};
use tb_ide::{
    app::{App, DialogKind, WindowKind},
    commands::Command,
    documents::{Destination, Project},
    editor::{column, offset},
};
struct Temp(PathBuf);
impl Temp {
    fn new() -> Self {
        static N: AtomicUsize = AtomicUsize::new(0);
        let p = std::env::temp_dir().join(format!(
            "tb-editor-{}-{}",
            std::process::id(),
            N.fetch_add(1, Ordering::Relaxed)
        ));
        fs::create_dir_all(&p).unwrap();
        Self(p.canonicalize().unwrap())
    }
    fn app(&self) -> App {
        let mut a = App::new(&self.0, self.0.join("options.ini"), (100, 30)).unwrap();
        a.options.syntax_checking = false;
        a
    }
}
impl Drop for Temp {
    fn drop(&mut self) {
        let _ = fs::remove_dir_all(&self.0);
    }
}
fn key(a: &mut App, k: K, m: M) {
    a.handle(Event::Key(KeyEvent::new(k, m)));
}
fn plain(a: &mut App, k: K) {
    key(a, k, M::NONE);
}
fn view(a: &App) -> tb_ide::documents::ViewId {
    let WindowKind::Code(v) = a.active_window().unwrap().kind else {
        panic!()
    };
    v
}
fn code(a: &App) -> &str {
    a.project
        .document(a.active_document().unwrap())
        .unwrap()
        .code()
}
fn field(a: &mut App, n: usize, text: &str) {
    while a.dialog.as_ref().unwrap().focus != n {
        plain(a, K::Tab);
    }
    key(a, K::Char('a'), M::CONTROL);
    plain(a, K::Delete);
    for c in text.chars() {
        plain(a, K::Char(c));
    }
}
fn toggle(a: &mut App, n: usize) {
    while a.dialog.as_ref().unwrap().focus != n {
        plain(a, K::Tab);
    }
    plain(a, K::Char(' '));
}
#[test]
fn cua_unicode_overwrite_wordstar_and_shared_undo() {
    let t = Temp::new();
    let mut a = t.app();
    a.handle(Event::Paste("α\t界\nzweite Zeile\n".into()));
    assert_eq!(code(&a), "α\t界\nzweite Zeile\n");
    key(&mut a, K::Home, M::CONTROL);
    key(&mut a, K::Char('k'), M::CONTROL);
    plain(&mut a, K::Char('0'));
    plain(&mut a, K::Right);
    plain(&mut a, K::Right);
    assert_eq!(a.project.view(view(&a)).unwrap().cursor, 3);
    plain(&mut a, K::Down);
    assert_eq!(
        a.project.view(view(&a)).unwrap().cursor,
        "α\t界\n".len() + 8
    );
    key(&mut a, K::Char('q'), M::CONTROL);
    plain(&mut a, K::Char('0'));
    assert_eq!(a.project.view(view(&a)).unwrap().cursor, 0);
    let original = code(&a).to_owned();
    key(&mut a, K::Char('q'), M::CONTROL);
    plain(&mut a, K::Esc);
    plain(&mut a, K::F(3));
    assert_eq!(code(&a), original);
    key(&mut a, K::Char('a'), M::CONTROL);
    key(&mut a, K::Char('x'), M::CONTROL);
    assert_eq!(code(&a), "");
    key(&mut a, K::Char('z'), M::CONTROL);
    assert_eq!(code(&a), original);
    assert_eq!(
        a.project.view(view(&a)).unwrap().selection(),
        Some(0..original.len())
    );
    key(&mut a, K::Insert, M::CONTROL);
    a.execute(Command::NewWindow);
    key(&mut a, K::End, M::CONTROL);
    key(&mut a, K::Insert, M::SHIFT);
    assert_eq!(code(&a), original.repeat(2));
    key(&mut a, K::Char('z'), M::CONTROL);
    assert_eq!(code(&a), original);
    key(&mut a, K::Home, M::CONTROL);
    plain(&mut a, K::Insert);
    plain(&mut a, K::Char('é'));
    assert!(code(&a).starts_with("é\t界"));
    key(&mut a, K::Char('y'), M::CONTROL);
    assert_eq!(code(&a), "zweite Zeile\n");
    key(&mut a, K::Char('z'), M::CONTROL);
    assert!(code(&a).starts_with("é\t界"));
    assert_eq!(column("α\t界", 8), 10);
    assert_eq!(offset("α\t界", 8, 8), 3);
    key(&mut a, K::Home, M::CONTROL);
    key(&mut a, K::Right, M::SHIFT);
    let mut terminal = ratatui::Terminal::new(ratatui::backend::TestBackend::new(100, 30)).unwrap();
    terminal.draw(|f| a.render(f)).unwrap();
    assert!(terminal
        .backend()
        .buffer()
        .content
        .iter()
        .any(|c| c.modifier.contains(ratatui::style::Modifier::REVERSED)));
}
#[test]
fn search_scope_direction_wrap_and_shortening_replace_all() {
    let t = Temp::new();
    let mut a = t.app();
    a.handle(Event::Paste("one One stone one\n".into()));
    key(&mut a, K::Home, M::CONTROL);
    a.execute(Command::Replace);
    field(&mut a, 0, "one");
    field(&mut a, 1, "");
    toggle(&mut a, 4);
    while a.dialog.as_ref().unwrap().focus != 6 {
        plain(&mut a, K::Tab);
    }
    plain(&mut a, K::Right);
    plain(&mut a, K::Enter);
    assert_eq!(code(&a), "  stone \n");
    key(&mut a, K::Char('z'), M::CONTROL);
    assert_eq!(code(&a), "one One stone one\n");
    a.execute(Command::Find);
    field(&mut a, 0, "one");
    toggle(&mut a, 3);
    plain(&mut a, K::Enter);
    assert_eq!(a.project.view(view(&a)).unwrap().selection(), Some(0..3));
    plain(&mut a, K::F(3));
    assert_eq!(a.project.view(view(&a)).unwrap().selection(), Some(14..17));
    plain(&mut a, K::F(3));
    assert!(a.message.contains("Rücksprung"));
    plain(&mut a, K::F(3));
    assert_eq!(a.project.view(view(&a)).unwrap().selection(), Some(0..3));
    key(&mut a, K::Char('\\'), M::CONTROL);
    assert_eq!(a.project.view(view(&a)).unwrap().selection(), Some(14..17));
    a.execute(Command::Find);
    toggle(&mut a, 2);
    plain(&mut a, K::Enter);
    assert_eq!(a.project.view(view(&a)).unwrap().selection(), Some(0..3));
    a.execute(Command::Replace);
    field(&mut a, 1, "x");
    toggle(&mut a, 5);
    while a.dialog.as_ref().unwrap().focus != 6 {
        plain(&mut a, K::Tab);
    }
    plain(&mut a, K::Right);
    plain(&mut a, K::Enter);
    assert_eq!(code(&a), "x One stone one\n");
    key(&mut a, K::Char('z'), M::CONTROL);
    assert_eq!(code(&a), "one One stone one\n");
    assert_eq!(a.project.view(view(&a)).unwrap().selection(), Some(0..3));
}
#[test]
fn line_leave_incomplete_invalid_disabled_and_current_revisions() {
    let t = Temp::new();
    let mut a = t.app();
    a.options.syntax_checking = true;
    for c in "print   \"MiX\"".chars() {
        plain(&mut a, K::Char(c));
    }
    plain(&mut a, K::Enter);
    assert_eq!(code(&a), "PRINT \"MiX\"\n");
    assert!(a.current_compilation().is_some());
    a.handle(Event::Paste("SUB Fresh\n".into()));
    assert!(a.dialog.is_none());
    assert!(a.message.contains("unvollständig"), "{}", a.message);
    assert!(a.current_compilation().is_none());
    a.handle(Event::Paste("END SUB\n".into()));
    assert!(a.current_compilation().is_some(), "{}", a.message);
    a.handle(Event::Paste("PRINT )\n".into()));
    assert!(matches!(
        a.dialog.as_ref().map(|d| &d.kind),
        Some(DialogKind::Diagnostics)
    ));
    assert!(a.current_compilation().is_none());
    plain(&mut a, K::Esc);
    a.execute(Command::Undo);
    assert!(a.compile_current().is_ok());
    a.options.syntax_checking = false;
    a.handle(Event::Paste("print )\n".into()));
    assert!(a.dialog.is_none());
    assert!(code(&a).ends_with("print )\n"));
    assert!(a.current_compilation().is_none());
    assert!(a.compile_current().is_err());
}
#[test]
fn procedures_and_shared_save_hook_preserve_drafts_and_frm_code() {
    let t = Temp::new();
    let mut a = t.app();
    a.handle(Event::Paste("DEFINT A-Z\n".into()));
    a.execute(Command::NewSub);
    field(&mut a, 0, "Fresh");
    plain(&mut a, K::Enter);
    assert!(code(&a).contains("SUB Fresh"));
    assert!(a.compile_current().is_ok());
    key(&mut a, K::Home, M::CONTROL);
    key(&mut a, K::F(2), M::SHIFT);
    plain(&mut a, K::Right);
    plain(&mut a, K::Enter);
    assert!(a.project.view(view(&a)).unwrap().cursor > 0);
    key(&mut a, K::F(2), M::CONTROL);
    assert_eq!(a.project.view(view(&a)).unwrap().cursor, 0);
    let id = a.active_document().unwrap();
    let v = view(&a);
    let cursor = code(&a).find("SUB Fresh").unwrap() + 4;
    a.project.view_mut(v).unwrap().cursor = cursor;
    a.project.view_mut(v).unwrap().anchor = Some(cursor + 5);
    a.project
        .save_file(id, Some(&Destination::new(t.0.join("main.bas"))))
        .unwrap();
    let saved = code(&a).to_owned();
    assert!(saved.contains("DECLARE SUB FRESH"));
    assert_eq!(
        a.project.view(v).unwrap().cursor,
        saved.find("SUB Fresh").unwrap() + 4
    );
    assert_eq!(
        a.project.view(v).unwrap().selection().map(|r| &saved[r]),
        Some("Fresh")
    );
    let revision = a.project.document(id).unwrap().revision();
    a.project.save_file(id, None).unwrap();
    assert_eq!(a.project.document(id).unwrap().revision(), revision);
    assert_eq!(code(&a), saved);
    let end = code(&a).len();
    a.project.replace_text(id, end..end, "SUB Draft\n").unwrap();
    let draft = code(&a).to_owned();
    a.project.save_file(id, None).unwrap();
    assert_eq!(fs::read_to_string(t.0.join("main.bas")).unwrap(), draft);
    assert!(a.project.save_notices[0].contains("übersprungen"));
    // Separater gültiger Projektstand für die Forms-Pflege.
    a.project.replace_text(id, 0..draft.len(), &saved).unwrap();
    let form = a.project.new_form("MainForm").unwrap();
    a.project
        .replace_text(form, 0..0, "SUB Button_Click\nEND SUB\n")
        .unwrap();
    a.project
        .save_file(form, Some(&Destination::new(t.0.join("MainForm.frm"))))
        .unwrap();
    let text = fs::read_to_string(t.0.join("MainForm.frm")).unwrap();
    assert!(text.contains("DECLARE SUB BUTTON_CLICK"), "{text}");
    let parsed = tb_ui::frm::read_text("MainForm.frm", &text).unwrap();
    assert_eq!(parsed.code, a.project.document(form).unwrap().code());
}
#[test]
fn includes_and_diagnostic_selection_use_original_physical_files() {
    let t = Temp::new();
    fs::write(t.0.join("one.bi"), "x$=1\n").unwrap();
    fs::write(t.0.join("two.bi"), "y%=\"bad\"\n").unwrap();
    fs::write(
        t.0.join("main.bas"),
        "'$INCLUDE: 'one.bi'\n'$INCLUDE: 'two.bi'\n",
    )
    .unwrap();
    let mut a = t.app();
    a.load_initial_project(t.0.join("main.bas")).unwrap();
    assert!(a.compile_current().is_err());
    let errors = a.editor.diagnostics.clone();
    assert_eq!(errors.len(), 2);
    for (i, d) in errors.iter().enumerate() {
        assert_eq!(d.pos.line, 1);
        a.goto_diagnostic(d).unwrap();
        assert_eq!(
            a.project
                .document(a.active_document().unwrap())
                .unwrap()
                .source_path(),
            t.0.join(if i == 0 { "one.bi" } else { "two.bi" })
        );
    }
    a.load_initial_project(t.0.join("main.bas")).unwrap();
    a.execute(Command::IncludedLines);
    let original = code(&a).to_owned();
    a.handle(Event::Paste("corruption".into()));
    assert_eq!(code(&a), original);
    assert!(a.editor.expansions.contains_key(&view(&a)));
    a.execute(Command::IncludedLines);
    a.execute(Command::IncludedFile);
    assert!(a
        .project
        .document(a.active_document().unwrap())
        .unwrap()
        .source_path()
        .ends_with("one.bi"));
    let id = a.active_document().unwrap();
    let len = code(&a).len();
    a.project.replace_text(id, 0..len, "x$=\"ok\"\n").unwrap();
    fs::write(t.0.join("two.bi"), "y%=2\n").unwrap(); // Open overlay remains authoritative.
    let other = a.project.open_document(&t.0.join("two.bi")).unwrap();
    let len = a.project.document(other).unwrap().code().len();
    a.project.replace_text(other, 0..len, "y%=2\n").unwrap();
    assert!(a.compile_current().is_ok());
    let mut p = Project::open(&t.0.join("main.bas"), vec![]).unwrap();
    let id = p.open_document(&t.0.join("one.bi")).unwrap();
    assert_eq!(p.document(id).unwrap().code(), "x$=1\n");
}

#[test]
fn crlf_navigation_word_boundaries_readonly_and_normalization_keep_views() {
    let t = Temp::new();
    let mut a = t.app();
    a.handle(Event::Paste("word next\r\n界\tX\r\n".into()));
    key(&mut a, K::Home, M::CONTROL);
    key(&mut a, K::Right, M::CONTROL);
    assert_eq!(a.project.view(view(&a)).unwrap().cursor, 5);
    plain(&mut a, K::End);
    assert_eq!(a.project.view(view(&a)).unwrap().cursor, 9);
    plain(&mut a, K::Right);
    assert_eq!(a.project.view(view(&a)).unwrap().cursor, 11);
    plain(&mut a, K::Left);
    assert_eq!(a.project.view(view(&a)).unwrap().cursor, 9);
    plain(&mut a, K::Delete);
    assert_eq!(code(&a), "word next界\tX\r\n");
    a.execute(Command::Undo);
    let id = a.active_document().unwrap();
    let v = view(&a);
    let second = a.project.open_view(id).unwrap();
    a.project.view_mut(second).unwrap().cursor = 5;
    a.project
        .replace_text(id, 0..code(&a).len(), "print   1\n")
        .unwrap();
    a.project.view_mut(second).unwrap().cursor = 8;
    a.project.view_mut(v).unwrap().cursor = 8;
    a.options.syntax_checking = true;
    plain(&mut a, K::Down);
    assert_eq!(code(&a), "PRINT 1\n");
    assert_eq!(a.project.view(second).unwrap().cursor, 6);
    a.execute(Command::Undo);
    assert_eq!(code(&a), "print   1\n");
    assert_eq!(a.project.view(second).unwrap().cursor, 8);
    a.options.syntax_checking = false;
    a.execute(Command::IncludedLines);
    let before = code(&a).to_owned();
    for command in [
        Command::Cut,
        Command::Paste,
        Command::Clear,
        Command::Undo,
        Command::NewSub,
        Command::LoadText,
        Command::Replace,
    ] {
        a.execute(command);
        assert!(a.dialog.is_none());
        assert_eq!(code(&a), before);
        assert!(a.message.contains("schreibgeschützt"));
    }
    let scroll = a.project.view(view(&a)).unwrap().scroll_line;
    plain(&mut a, K::PageDown);
    assert!(a.project.view(view(&a)).unwrap().scroll_line > scroll);
}
#[test]
fn external_include_revision_and_project_context_declarations() {
    let t = Temp::new();
    fs::write(
        t.0.join("defs.bi"),
        "DEFINT A-Z\nTYPE Pair\nX AS INTEGER\nEND TYPE\nCONST N=1\n",
    )
    .unwrap();
    fs::write(
        t.0.join("main.bas"),
        "'$INCLUDE: 'defs.bi'\nSUB Work(p AS Pair, n)\np.x=n+N\nEND SUB\n",
    )
    .unwrap();
    let mut a = t.app();
    a.load_initial_project(t.0.join("main.bas")).unwrap();
    assert!(a.compile_current().is_ok());
    assert!(a.current_compilation().is_some());
    fs::write(
        t.0.join("defs.bi"),
        "DEFINT A-Z\nTYPE Pair\nX AS INTEGER\nEND TYPE\nCONST N=2\n",
    )
    .unwrap();
    // Geöffnete Include-Dokumente sind maßgeblich; externe Änderungen überschreiben keinen Puffer.
    assert!(a.current_compilation().is_some());
    let include = a.project.find_document(&t.0.join("defs.bi")).unwrap();
    let len = a.project.document(include).unwrap().code().len();
    a.project
        .replace_text(
            include,
            0..len,
            &fs::read_to_string(t.0.join("defs.bi")).unwrap(),
        )
        .unwrap();
    assert!(a.current_compilation().is_none());
    assert!(a.current_diagnostics().is_none());
    assert!(a.compile_current().is_ok());
    let id = a.active_document().unwrap();
    a.project.save_file(id, None).unwrap();
    assert!(
        code(&a).contains("DECLARE SUB WORK (P AS PAIR, N%)"),
        "{} {:?}",
        code(&a),
        a.project.save_notices
    );
    assert!(a.project.save_notices.is_empty());
    assert!(a.compile_current().is_ok());
    a.execute(Command::NewFunction);
    field(&mut a, 0, "Fresh");
    plain(&mut a, K::Enter);
    assert!(code(&a).contains("FUNCTION Fresh%"), "{}", code(&a));
    assert!(a.compile_current().is_ok());
    let other = a.project.new_module("Other").unwrap();
    a.project
        .replace_text(other, 0..0, "SUB Taken\nEND SUB\n")
        .unwrap();
    a.execute(Command::NewSub);
    field(&mut a, 0, "Taken");
    plain(&mut a, K::Enter);
    assert!(a.dialog.as_ref().unwrap().error.contains("bereits"));
    plain(&mut a, K::Esc);
    // Ein erst nach dem Öffnen eingefügtes Include wird bis zum Öffnen direkt vom Loader gelesen.
    fs::write(t.0.join("late.bi"), "CONST LATE=1\n").unwrap();
    a.project
        .replace_text(id, 0..0, "'$INCLUDE: 'late.bi'\n")
        .unwrap();
    assert!(a.compile_current().is_ok());
    fs::remove_file(t.0.join("late.bi")).unwrap();
    assert!(a.current_compilation().is_none());
    assert!(a.compile_current().is_err());
    assert!(a.current_diagnostics().is_none());
}
