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, Execution, Field, WindowKind},
    commands::Command,
};
use tb_runtime::value::Value;
struct Temp(PathBuf);
impl Temp {
    fn new() -> Self {
        static N: AtomicUsize = AtomicUsize::new(0);
        let p = std::env::temp_dir().join(format!(
            "tb-debug-{}-{}",
            std::process::id(),
            N.fetch_add(1, Ordering::Relaxed)
        ));
        fs::create_dir_all(&p).unwrap();
        Self(p.canonicalize().unwrap())
    }
    fn app(&self, source: &str) -> App {
        let p = self.0.join("main.bas");
        fs::write(&p, source).unwrap();
        let mut a = App::new(&self.0, self.0.join("config"), (100, 30)).unwrap();
        a.load_initial_project(p).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 tick(a: &mut App) {
    for _ in 0..30 {
        a.tick_execution(0);
        if !matches!(a.execution, Execution::Running | Execution::Waiting) {
            break;
        }
    }
}
fn cursor(a: &mut App, line: usize) {
    let (id, v) = a
        .windows
        .iter()
        .find_map(|w| {
            if let WindowKind::Code(v) = w.kind {
                Some((w.id, v))
            } else {
                None
            }
        })
        .unwrap();
    a.execute(Command::FocusWindow(id));
    let doc = a.project.view(v).unwrap().document();
    let code = a.project.document(doc).unwrap().code();
    let at = code
        .split_inclusive('\n')
        .take(line - 1)
        .map(str::len)
        .sum();
    a.project.view_mut(v).unwrap().cursor = at;
}
fn watch(a: &mut App, c: Command, text: &str) {
    a.execute(c);
    a.dialog.as_mut().unwrap().fields[0] = Field::text("BASIC-Ausdruck", text);
    key(a, K::Enter, M::NONE);
    assert!(a.dialog.is_none(), "{:?}", a.dialog);
}
#[test]
fn dispatcher_marks_edits_rebind_and_readonly_watches() {
    let t = Temp::new();
    let mut a = t.app("x%=1\nx%=2\nPRINT x%\nEND\n");
    cursor(&mut a, 2);
    key(&mut a, K::F(9), M::NONE);
    let doc = a.active_document().unwrap();
    a.project.replace_text(doc, 0..0, "' inserted\n").unwrap();
    assert_eq!(
        a.project.document(doc).unwrap().breakpoints[0].at,
        "' inserted\nx%=1\n".len()
    );
    key(&mut a, K::F(5), M::SHIFT);
    tick(&mut a);
    assert_eq!(a.execution, Execution::Paused, "{}", a.message);
    assert_eq!(a.session.vm.as_ref().unwrap().current_line(), 3);
    assert!(a.project.document(doc).unwrap().breakpoints[0].bound);
    watch(&mut a, Command::AddWatch, "x% + 1");
    assert!(matches!(a.debugger.watches[0].value, Ok(Value::Int(2))));
    let before = a.project.document(doc).unwrap().code().to_string();
    key(&mut a, K::Char('z'), M::NONE);
    assert_eq!(a.project.document(doc).unwrap().code(), before);
    watch(&mut a, Command::InstantWatch, "RND");
    assert!(a.debugger.instant.as_ref().unwrap().value.is_err());
    a.execute(Command::Immediate);
    for c in "x%=7".chars() {
        key(&mut a, K::Char(c), M::NONE);
    }
    key(&mut a, K::Enter, M::NONE);
    tick(&mut a);
    assert_eq!(a.execution, Execution::Paused, "{}", a.message);
    assert!(matches!(
        a.session.vm.as_ref().unwrap().inspect("x%"),
        Some(Value::Int(7))
    ));
    a.execute(Command::DeleteWatches);
    assert!(a.debugger.watches.is_empty());
    key(&mut a, K::F(8), M::NONE);
    tick(&mut a);
    assert_eq!(a.session.vm.as_ref().unwrap().current_line(), 4);
    assert!(matches!(
        a.session.vm.as_ref().unwrap().inspect("x%"),
        Some(Value::Int(2))
    ));
    a.execute(Command::ClearBreakpoints);
    key(&mut a, K::F(5), M::NONE);
    tick(&mut a);
    assert_eq!(a.execution, Execution::Ended);
    a.project.replace_text(doc, 0..0, "' again\n").unwrap();
    cursor(&mut a, 1);
    key(&mut a, K::F(9), M::NONE);
    key(&mut a, K::F(5), M::SHIFT);
    tick(&mut a);
    assert_eq!(a.execution, Execution::Ended);
    assert!(!a.project.document(doc).unwrap().breakpoints[0].bound);
}
#[test]
fn calls_procedure_step_cursor_history_and_restart() {
    let t = Temp::new();
    let mut a=t.app("CALL rec(2)\nx%=1\nx%=2\nEND\nSUB rec(n AS INTEGER)\nIF n>0 THEN CALL rec(n-1)\nPRINT n\nEND SUB\n");
    a.execute(Command::History);
    cursor(&mut a, 6);
    key(&mut a, K::F(9), M::NONE);
    key(&mut a, K::F(5), M::SHIFT);
    tick(&mut a);
    a.execute(Command::ClearBreakpoints);
    let frame = a
        .session
        .vm
        .as_ref()
        .unwrap()
        .debug_location()
        .unwrap()
        .frame;
    key(&mut a, K::F(10), M::NONE);
    tick(&mut a);
    assert_eq!(a.execution, Execution::Paused);
    assert_eq!(
        a.session
            .vm
            .as_ref()
            .unwrap()
            .debug_location()
            .unwrap()
            .frame,
        frame
    );
    cursor(&mut a, 3);
    key(&mut a, K::F(7), M::NONE);
    tick(&mut a);
    assert_eq!(a.session.vm.as_ref().unwrap().current_line(), 3);
    watch(&mut a, Command::AddWatch, "x%");
    let current = a.session.vm.as_ref().unwrap().debug_location();
    key(&mut a, K::F(8), M::SHIFT);
    key(&mut a, K::F(8), M::SHIFT);
    assert!(a.debugger.historical.is_some());
    assert!(a.message.contains("HISTORIE"));
    assert_eq!(a.session.vm.as_ref().unwrap().debug_location(), current);
    assert!(matches!(a.debugger.watches[0].value, Ok(Value::Int(1))));
    key(&mut a, K::F(10), M::SHIFT);
    a.execute(Command::NextStatement);
    assert!(a.debugger.historical.is_none());
    a.execute(Command::Restart);
    assert!(a.debugger.frame.is_none());
    assert!(a.debugger.watches[0].value.is_err());
    assert!(a.session.vm.as_ref().unwrap().debug.history.is_empty());
    assert_eq!(a.debugger.watches.len(), 1);
}
#[test]
fn includes_and_deleted_marks_do_not_bind_to_foreign_statements() {
    let t = Temp::new();
    fs::write(t.0.join("a.bi"), "'a\nx%=1\n").unwrap();
    fs::write(t.0.join("b.bi"), "'b\nx%=2\n").unwrap();
    let mut a = t.app("'$INCLUDE: 'a.bi'\n'$INCLUDE: 'b.bi'\nEND\n");
    cursor(&mut a, 2);
    a.execute(Command::IncludedFile);
    let doc = a.active_document().unwrap();
    let v = match a.active_window().unwrap().kind {
        WindowKind::Code(v) => v,
        _ => panic!(),
    };
    a.project.view_mut(v).unwrap().cursor = 3;
    key(&mut a, K::F(9), M::NONE);
    key(&mut a, K::F(5), M::SHIFT);
    tick(&mut a);
    assert_eq!(a.execution, Execution::Paused, "{}", a.message);
    let vm = a.session.vm.as_ref().unwrap();
    assert!(vm.current_file().ends_with("b.bi"));
    assert!(matches!(vm.inspect("x%"), Some(Value::Int(1))));
    a.project.replace_text(doc, 3..8, "").unwrap();
    assert!(!a.project.document(doc).unwrap().breakpoints[0].valid);
    a.execute(Command::Restart);
    tick(&mut a);
    assert_eq!(a.execution, Execution::Ended);
    assert!(!a.project.document(doc).unwrap().breakpoints[0].bound);
    a.project.undo(doc).unwrap();
    assert!(a.project.document(doc).unwrap().breakpoints[0].valid);
}
#[test]
fn calls_mouse_inspection_instant_key_delete_and_error_window() {
    let t = Temp::new();
    let mut a = t.app(
        "CALL rec(2)\nEND\nSUB rec(n AS INTEGER)\nIF n>0 THEN CALL rec(n-1)\nPRINT n\nEND SUB\n",
    );
    cursor(&mut a, 5);
    key(&mut a, K::F(9), M::NONE);
    key(&mut a, K::F(5), M::SHIFT);
    tick(&mut a);
    let current = a.session.vm.as_ref().unwrap().debug_location();
    a.execute(Command::Calls);
    let mut terminal = ratatui::Terminal::new(ratatui::backend::TestBackend::new(100, 30)).unwrap();
    terminal.draw(|f| a.render(f)).unwrap();
    let rect = a
        .hits
        .iter()
        .find_map(|(r, h)| matches!(h, tb_ide::app::Hit::DebugFrame(1)).then_some(*r))
        .unwrap();
    a.handle(Event::Mouse(crossterm::event::MouseEvent {
        kind: crossterm::event::MouseEventKind::Down(crossterm::event::MouseButton::Left),
        column: rect.x,
        row: rect.y,
        modifiers: M::NONE,
    }));
    assert_eq!(a.session.vm.as_ref().unwrap().debug_location(), current);
    key(&mut a, K::F(9), M::SHIFT);
    a.dialog.as_mut().unwrap().fields[0] = Field::text("Ausdruck", "n");
    key(&mut a, K::Enter, M::NONE);
    assert!(matches!(
        a.debugger.instant.as_ref().unwrap().value,
        Ok(Value::Int(1))
    ));
    watch(&mut a, Command::AddWatch, "n+2");
    watch(&mut a, Command::AddWatch, "1/0");
    assert!(a.debugger.watches[1].value.is_err());
    a.execute(Command::DeleteWatch);
    key(&mut a, K::Enter, M::NONE);
    assert_eq!(a.debugger.watches.len(), 1);
    terminal.draw(|f| a.render(f)).unwrap();
    let display = terminal
        .backend()
        .buffer()
        .content
        .iter()
        .map(|c| c.symbol())
        .collect::<String>();
    assert!(display.contains("FEHLER"));
    a.execute(Command::Immediate);
    for c in "ERROR 5".chars() {
        key(&mut a, K::Char(c), M::NONE);
    }
    key(&mut a, K::Enter, M::NONE);
    tick(&mut a);
    assert_eq!(a.execution, Execution::Paused);
    assert_eq!(a.session.vm.as_ref().unwrap().debug_location(), current);
    terminal.draw(|f| a.render(f)).unwrap();
    let display = terminal
        .backend()
        .buffer()
        .content
        .iter()
        .map(|c| c.symbol())
        .collect::<String>();
    assert!(display.contains("Error"));
}
#[test]
fn form_handler_keys_and_old_revision_cursor_guard() {
    let t = Temp::new();
    let p = t.0.join("window.frm");
    fs::write(&p,"VERSION 1.00\nBEGIN Form Form1\nEND\nSUB Form_Load()\nCALL work\nPRINT 2\nEND SUB\nSUB work()\nPRINT 1\nEND SUB\n").unwrap();
    let mut a = App::new(&t.0, t.0.join("config"), (100, 30)).unwrap();
    a.load_initial_project(p).unwrap();
    a.options.syntax_checking = false;
    cursor(&mut a, 2);
    key(&mut a, K::F(9), M::NONE);
    key(&mut a, K::F(5), M::SHIFT);
    tick(&mut a);
    assert_eq!(a.execution, Execution::Paused, "{}", a.message);
    assert_eq!(a.session.vm.as_ref().unwrap().current_line(), 5);
    key(&mut a, K::F(10), M::NONE);
    tick(&mut a);
    assert_eq!(a.session.vm.as_ref().unwrap().current_line(), 6);
    let before = a.session.vm.as_ref().unwrap().debug_location();
    let doc = a.active_document().unwrap();
    a.project.replace_text(doc, 0..0, "' edit\n").unwrap();
    a.execute(Command::SetStatement);
    assert!(a.message.contains("Quellstand"));
    assert_eq!(a.session.vm.as_ref().unwrap().debug_location(), before);
}
#[test]
fn immediate_wait_run_transition_watchpoint_and_trace_are_session_owned() {
    let t = Temp::new();
    let mut a=t.app("x%=1\nx%=2\nx%=3\nEND\nSUB ask(n AS INTEGER)\nINPUT n\nEND SUB\nSUB restart()\nRUN\nEND SUB\n");
    watch(&mut a, Command::Watchpoint, "x%=2");
    a.execute(Command::Trace);
    key(&mut a, K::F(5), M::SHIFT);
    tick(&mut a);
    assert_eq!(a.execution, Execution::Paused, "{}", a.message);
    assert_eq!(a.session.vm.as_ref().unwrap().current_line(), 3);
    let before = a.session.vm.as_ref().unwrap().debug_location();
    a.execute(Command::DeleteWatches);
    a.execute(Command::Immediate);
    a.submit_immediate("CALL ask(x%)").unwrap();
    tick(&mut a);
    assert_eq!(a.execution, Execution::Waiting);
    a.execute(Command::Pause);
    assert_eq!(a.execution, Execution::Paused);
    assert!(a.session.vm.as_ref().unwrap().immediate_active());
    key(&mut a, K::F(5), M::NONE);
    a.execute(Command::Output);
    key(&mut a, K::Char('9'), M::NONE);
    key(&mut a, K::Enter, M::NONE);
    tick(&mut a);
    assert_eq!(a.execution, Execution::Paused);
    assert_eq!(a.session.vm.as_ref().unwrap().debug_location(), before);
    assert!(matches!(
        a.session.vm.as_ref().unwrap().inspect("x%"),
        Some(Value::Int(9))
    ));
    a.submit_immediate("CALL restart()").unwrap();
    tick(&mut a);
    assert_eq!(a.execution, Execution::Ended);
    assert!(a.session.vm.is_none());
}
