//! Regressionen zu Ereigniszustellung, Hostgrenzen und Forms-Zustand.
use tb_frontend::forms::{FormCatalog, ObjectClass};
use tb_runtime::host::{CaptureHost, Ereignis, Host, MausArt, MausEreignis};
use tb_runtime::screen::TextScreen;
use tb_runtime::value::Value;
use tb_vm::interp::{RunEvent, Vm};

#[path = "../../../tests/support/prozessfrist.rs"]
mod prozessfrist;

fn vm(src: &str) -> Vm {
    let mut catalog = FormCatalog::default();
    catalog.add("Form1", ObjectClass::Form, None, false);
    catalog.add("Text1", ObjectClass::TextBox, Some("Form1"), false);
    catalog.add("Text2", ObjectClass::TextBox, Some("Form1"), false);
    catalog.add("Timer1", ObjectClass::Timer, Some("Form1"), false);
    catalog.add("Menu1", ObjectClass::Menu, Some("Form1"), false);
    catalog.add("Item1", ObjectClass::Menu, Some("Menu1"), false);
    Vm::new(
        tb_vm::compile_source_with_forms("FORM1", src, &catalog)
            .unwrap_or_else(|d| panic!("{d:?}")),
    )
}

fn stop(vm: &mut Vm, host: &mut dyn Host) {
    assert!(matches!(vm.run(host), RunEvent::Stopped { .. }));
}

fn string(vm: &Vm, name: &str) -> String {
    match vm.inspect(name) {
        Some(Value::Str(s)) => s.to_string(),
        other => panic!("{other:?}"),
    }
}

fn integer(vm: &Vm, name: &str) -> i16 {
    match vm.inspect(name) {
        Some(Value::Int(n)) => n,
        other => panic!("{other:?}"),
    }
}

fn key(s: &str) -> Ereignis {
    Ereignis::Taste(s.into(), 0)
}
fn mouse() -> Ereignis {
    Ereignis::Maus(MausEreignis {
        art: MausArt::Druck,
        taste: 1,
        shift: 0,
        zeile: 1,
        spalte: 1,
    })
}

#[test]
fn beide_signale_werden_zugestellt_und_beginnen_in_quellreihenfolge() {
    let mut vm = vm("ON SIGNAL(1) GOSUB IntHandler\nON SIGNAL(2) GOSUB TermHandler\nSIGNAL(1) ON\nSIGNAL(2) ON\nSTOP\nDOEVENTS\nEND\nIntHandler:\ns$ = s$ + \"1\"\nRETURN\nTermHandler:\ns$ = s$ + \"2\"\nRETURN");
    let mut host = CaptureHost::default();
    stop(&mut vm, &mut host);
    host.ereignis(Ereignis::Signal(2));
    host.ereignis(Ereignis::Signal(1));
    assert_eq!(vm.run(&mut host), RunEvent::Ended);
    assert_eq!(string(&vm, "s"), "12");
    for n in [0, 3] {
        assert!(
            tb_vm::compile_source("TEST", &format!("ON SIGNAL({n}) GOSUB H\nEND\nH:\nRETURN"))
                .is_err()
        );
        let mut bad = vm_for_source(&format!("n% = {n}\nON SIGNAL(n%) GOSUB H\nEND\nH:\nRETURN"));
        assert!(matches!(
            bad.run(&mut host),
            RunEvent::Error { code: 5, .. }
        ));
    }
}

fn vm_for_source(src: &str) -> Vm {
    Vm::new(tb_vm::compile_source("TEST", src).unwrap())
}

#[test]
fn key_on_off_stop_gelten_fuer_alle_konsoleneingaben() {
    for state in ["ON", "OFF", "STOP"] {
        for input in ["s$ = INKEY$", "INPUT s$", "LINE INPUT s$", "s$ = INPUT$(1)"] {
            let mut vm = vm_for_source(&format!("ON KEY(15) GOSUB H\nKEY 15, CHR$(0) + CHR$(30)\nKEY(15) {state}\nSTOP\n{input}\nKEY(15) ON\nDOEVENTS\nEND\nH:\nn% = n% + 1\nRETURN"));
            let mut host = CaptureHost::default();
            stop(&mut vm, &mut host);
            host.ereignis(key("a"));
            if state == "ON" {
                host.ereignis(key("b"));
            }
            host.ereignis(key("\r"));
            assert_eq!(vm.run(&mut host), RunEvent::Ended, "{state}: {input}");
            assert_eq!(
                string(&vm, "s"),
                if state == "ON" { "b" } else { "a" },
                "{state}: {input}"
            );
            assert_eq!(
                integer(&vm, "n"),
                if state == "OFF" { 0 } else { 1 },
                "{state}: {input}"
            );
        }
    }
}

#[test]
fn taste_maus_und_trap_behalten_die_annahmereihenfolge() {
    for (events, expected) in [
        (vec![key("a"), mouse()], "aM"),
        (vec![mouse(), key("a")], "Ma"),
        (
            vec![key("a"), mouse(), key("b"), key("t"), mouse()],
            "aMbTM",
        ),
    ] {
        let mut vm = vm("DIM SHARED s$\nON KEY(15) GOSUB H\nKEY 15, CHR$(0) + CHR$(20)\nKEY(15) ON\nForm1.Show\nText1.SetFocus\nSTOP\nn% = DOEVENTS + LEN(s$)\nDOEVENTS\nDOEVENTS\nDOEVENTS\nDOEVENTS\nEND\nH:\ns$ = s$ + \"T\"\nRETURN\nSUB Text1_KeyPress(KeyAscii AS INTEGER)\nSHARED s$\ns$ = s$ + CHR$(KeyAscii)\nEND SUB\nSUB Form_MouseDown(Button AS INTEGER, Shift AS INTEGER, X AS SINGLE, Y AS SINGLE)\nSHARED s$\ns$ = s$ + \"M\"\nEND SUB");
        let mut host = CaptureHost::default();
        stop(&mut vm, &mut host);
        for event in events {
            host.ereignis(event);
        }
        assert_eq!(vm.run(&mut host), RunEvent::Ended);
        assert_eq!(string(&vm, "s"), expected);
        assert!(
            matches!(vm.inspect("n"), Some(Value::Int(n)) if n > 0),
            "DOEVENTS muss vor der Auswertung von LEN zustellen"
        );
    }
}

#[test]
fn fokus_handler_laufen_geordnet_bis_zur_rueckkehr() {
    let mut vm = vm("DIM SHARED s$\nText1.TabIndex = 0\nText2.TabIndex = 1\nForm1.Show\nText1.SetFocus\nSTOP\nDOEVENTS\nEND\nSUB Text1_LostFocus()\nSHARED s$\ns$ = s$ + \"L\"\ns$ = s$ + \"l\"\nEND SUB\nSUB Text2_GotFocus()\nSHARED s$\ns$ = s$ + \"G\"\ns$ = s$ + \"g\"\nEND SUB");
    let mut host = CaptureHost::default();
    stop(&mut vm, &mut host);
    host.ereignis(key("\t"));
    assert_eq!(vm.run(&mut host), RunEvent::Ended);
    assert_eq!(string(&vm, "s"), "LlGg");
}

#[test]
fn doevents_verhindert_rekursion_derselben_forms_prozedur() {
    let mut vm = vm("DIM SHARED s$\nForm1.Show\nText1.SetFocus\nSTOP\nDOEVENTS\nEND\nSUB Text1_KeyPress(KeyAscii AS INTEGER)\nSHARED s$\ns$ = s$ + CHR$(KeyAscii)\nDOEVENTS\ns$ = s$ + \"r\"\nEND SUB");
    let mut host = CaptureHost::default();
    stop(&mut vm, &mut host);
    host.ereignis(key("a"));
    host.ereignis(key("b"));
    assert_eq!(vm.run(&mut host), RunEvent::Ended);
    assert_eq!(string(&vm, "s"), "arbr");
}

#[test]
fn offenes_menu_sperrt_traps_und_wartende_forms_timer_an_allen_zustellpunkten() {
    let mut vm = vm("DIM SHARED n%\nON UEVENT GOSUB H\nUEVENT ON\nMenu1.Caption = \"&Datei\"\nItem1.Caption = \"&Ende\"\nForm1.Show\nSTOP\nCALL SetUEvent\nn% = n% + 1\nDOEVENTS\nSLEEP .001\nLINE INPUT s$\nSTOP\nDOEVENTS\nEND\nH:\nn% = n% + 10\nRETURN\nSUB Timer1_Timer()\nSHARED n%\nn% = n% + 100\nEND SUB");
    let mut host = CaptureHost::default();
    stop(&mut vm, &mut host);
    vm.forms.handle_key("d", 4);
    assert!(vm.forms.menu_is_open());
    vm.queue_form_event(tb_ui::forms::FormEvent {
        object: 3,
        array_index: None,
        name: "TIMER".into(),
        args: vec![],
    });
    // Erst die blockierende Konsole holt Enter ab, damit das Menü offen bleibt.
    struct InputHost(CaptureHost);
    impl Host for InputHost {
        fn present(&mut self, s: &TextScreen) {
            self.0.present(s);
        }
        fn next_event(&mut self, block: bool) -> Option<Ereignis> {
            block.then(|| key("\r"))
        }
        fn jetzt_ms(&mut self) -> u64 {
            self.0.jetzt_ms()
        }
        fn warten(&mut self, deadline: Option<u64>) -> Option<Ereignis> {
            if deadline.is_none() {
                self.next_event(true)
            } else {
                self.0.warten(deadline)
            }
        }
    }
    stop(&mut vm, &mut InputHost(CaptureHost::default()));
    assert_eq!(integer(&vm, "n"), 1);
    assert!(vm.forms.menu_is_open());
    host.ereignis(key("\u{1b}"));
    assert_eq!(vm.run(&mut host), RunEvent::Ended);
    assert_eq!(integer(&vm, "n"), 111);
}

#[test]
fn ohne_zeitbedarf_keine_uhrabfrage_und_keine_mausansammlung() {
    struct OhneUhr(CaptureHost);
    impl Host for OhneUhr {
        fn present(&mut self, s: &TextScreen) {
            self.0.present(s);
        }
        fn next_event(&mut self, b: bool) -> Option<Ereignis> {
            self.0.next_event(b)
        }
        fn jetzt_ms(&mut self) -> u64 {
            panic!("unerwartete Uhrabfrage")
        }
        fn warten(&mut self, _: Option<u64>) -> Option<Ereignis> {
            panic!("unerwartetes Warten")
        }
    }
    for src in ["a% = 1\na% = 2\nEND", "Form1.Show\na% = 1\nDOEVENTS\nEND"] {
        let mut vm = vm(src);
        let mut host = OhneUhr(CaptureHost::default());
        for _ in 0..500 {
            host.0.ereignis(mouse());
        }
        assert_eq!(vm.run(&mut host), RunEvent::Ended);
        assert!(vm.rt.eingaben.is_empty());
    }
}

#[test]
fn sleep_folgt_nur_der_hostfrist_und_forms_timer_beendet_es() {
    let _frist = prozessfrist::Prozessfrist::neu("Host-SLEEP", std::time::Duration::from_secs(5));
    struct WarteHost {
        now: u64,
        deadlines: Vec<Option<u64>>,
        timer: bool,
    }
    impl Host for WarteHost {
        fn present(&mut self, _: &TextScreen) {}
        fn next_event(&mut self, _: bool) -> Option<Ereignis> {
            None
        }
        fn jetzt_ms(&mut self) -> u64 {
            self.now
        }
        fn warten(&mut self, deadline: Option<u64>) -> Option<Ereignis> {
            self.deadlines.push(deadline);
            if self.deadlines.len() == 1 && !self.timer {
                // Reale Zeit verstreicht bei eingefrorener Hostuhr. Erst die
                // folgende Host-Wartephase darf die Deadline erreichen.
                std::thread::sleep(std::time::Duration::from_millis(10));
                return Some(Ereignis::Groesse { cols: 80, rows: 25 });
            }
            self.now = deadline.expect("Deadline fehlt");
            None
        }
    }
    let mut host = WarteHost {
        now: 0,
        deadlines: vec![],
        timer: false,
    };
    let mut plain = vm_for_source("SLEEP .001\nEND");
    assert_eq!(plain.run(&mut host), RunEvent::Ended);
    assert_eq!(host.deadlines, [Some(1), Some(1)]);
    let mut vm = vm("DIM SHARED n%\nForm1.Show\nTimer1.Interval = 100\nTimer1.Enabled = -1\nSLEEP 60\nEND\nSUB Timer1_Timer()\nSHARED n%\nn% = n% + 1\nEND SUB");
    let mut host = WarteHost {
        now: 0,
        deadlines: vec![],
        timer: true,
    };
    assert_eq!(vm.run(&mut host), RunEvent::Ended);
    assert_eq!(host.deadlines, [Some(100)]);
    assert_eq!(integer(&vm, "n"), 1);
}

#[test]
fn dialog_erhaelt_stop_markierung_unverbrauchter_tasten() {
    let mut vm = vm_for_source("ON KEY(15) GOSUB H\nKEY 15, CHR$(0) + CHR$(30)\nKEY(15) STOP\nSTOP\nx% = MSGBOX(\"Weiter\")\nKEY(15) ON\nDOEVENTS\ns$ = INKEY$\nEND\nH:\nn% = n% + 1\nRETURN");
    let mut host = CaptureHost::default();
    stop(&mut vm, &mut host);
    host.ereignis(key("\r"));
    host.ereignis(key("a"));
    vm.rt.pump(&mut host, false);
    assert_eq!(vm.run(&mut host), RunEvent::Ended);
    assert_eq!(string(&vm, "s"), "a");
    assert_eq!(integer(&vm, "n"), 1);
}

#[test]
fn unload_aus_unload_handler_ruft_dieselbe_prozedur_nicht_rekursiv_auf() {
    let mut vm = vm("DIM SHARED s$\nForm1.Show\nUNLOAD Form1\nEND\nSUB Form_Unload(Cancel AS INTEGER)\nSHARED s$\ns$ = s$ + \"u\"\nUNLOAD Form1\ns$ = s$ + \"r\"\nEND SUB");
    assert_eq!(vm.run(&mut CaptureHost::default()), RunEvent::Ended);
    assert_eq!(string(&vm, "s"), "ur");
    assert!(!vm.forms.is_loaded(0));
}

// Nachverifikation V1–V5: unveränderte Sollwerte aus den Review-Proben.
#[test]
fn review_signal_waehrend_blockierender_eingabe() {
    struct Input {
        reads: usize,
    }
    impl Host for Input {
        fn present(&mut self, _: &TextScreen) {}
        fn next_event(&mut self, block: bool) -> Option<Ereignis> {
            if !block {
                return None;
            }
            self.reads += 1;
            Some(if self.reads == 1 {
                Ereignis::Signal(2)
            } else {
                Ereignis::Ende
            })
        }
        fn jetzt_ms(&mut self) -> u64 {
            0
        }
        fn warten(&mut self, _: Option<u64>) -> Option<Ereignis> {
            self.next_event(true)
        }
    }
    let mut results = vec![];
    for statement in ["INPUT s$", "LINE INPUT s$", "s$ = INPUT$(1)"] {
        let src =
            format!("ON SIGNAL(2) GOSUB H\nSIGNAL(2) ON\n{statement}\nEND\nH:\nPRINT \"H\"\nEND");
        let mut vm = Vm::new(tb_vm::compile_source("TEST", &src).unwrap());
        let result = vm.run(&mut Input { reads: 0 });
        println!(
            "{statement}: {result:?}; Ausgabe={:?}",
            tb_runtime::snapshot::text(&vm.rt.screen)
        );
        results.push(result);
    }
    assert!(results.iter().all(|result| *result == RunEvent::Ended));
}

#[test]
fn review_objektargument_darf_gotfocus_nicht_vorziehen() {
    for action in [
        "CALL Mark(Text1)",
        "n% = Text1.TabIndex\nCALL Mark(Text1)",
        "Text1.Text = \"neu\"\nCALL Mark(Text1)",
        "Text1.Refresh\nCALL Mark(Text1)",
        "LOAD Form1\nCALL Mark(Text1)",
    ] {
        let mut vm = vm(&format!("DIM SHARED s$\nText1.TabIndex = 0\nText2.TabIndex = 1\nForm1.Show\nText1.SetFocus\nSTOP\nDOEVENTS\nEND\nSUB Text1_LostFocus()\nSHARED s$\n{action}\ns$ = s$ + \"l\"\nEND SUB\nSUB Mark(c AS CONTROL)\nSHARED s$\ns$ = s$ + \"L\"\nEND SUB\nSUB Text2_GotFocus()\nSHARED s$\ns$ = s$ + \"G\"\ns$ = s$ + \"g\"\nEND SUB"));
        let mut host = CaptureHost::default();
        stop(&mut vm, &mut host);
        host.ereignis(key("\t"));
        assert_eq!(vm.run(&mut host), RunEvent::Ended, "{action}");
        assert_eq!(string(&vm, "s"), "LlGg", "{action}");
    }
}

#[test]
fn review_gleichzeitig_key_und_uevent_behalten_prioritaet() {
    let mut vm = vm("ON KEY(15) GOSUB K\nKEY 15, CHR$(0) + CHR$(20)\nKEY(15) ON\nON UEVENT GOSUB U\nUEVENT ON\nSTOP\nDOEVENTS\nEND\nK:\ns$ = s$ + \"K\"\nRETURN\nU:\ns$ = s$ + \"U\"\nRETURN");
    let mut host = CaptureHost::default();
    stop(&mut vm, &mut host);
    host.ereignis(key("t"));
    vm.rt.pump(&mut host, false);
    vm.rt.traps.melden(tb_runtime::traps::Quelle::UEvent);
    assert_eq!(vm.run(&mut host), RunEvent::Ended);
    assert_eq!(string(&vm, "s"), "KU");
}

#[test]
fn review_modale_eingabe_vor_eof_wird_zugestellt() {
    let mut vm = vm("DIM SHARED s$\nForm1.Show 1\nEND\nSUB Form_MouseDown(Button AS INTEGER, Shift AS INTEGER, X AS SINGLE, Y AS SINGLE)\nSHARED s$\ns$ = \"M\"\nUNLOAD Form1\nEND SUB");
    let mut host = CaptureHost::default();
    host.ereignis_nach(2, mouse());
    host.ereignis_nach(2, Ereignis::Ende);
    assert_eq!(vm.run(&mut host), RunEvent::Ended);
    assert_eq!(string(&vm, "s"), "M");
}

#[test]
fn eof_leert_angenommene_eingaben_bei_modalen_und_modellosen_forms() {
    for modal in [false, true] {
        let show = if modal { "Form1.Show 1" } else { "Form1.Show" };
        let mut vm = vm(&format!("DIM SHARED s$\n{show}\nEND\nSUB Form_MouseDown(Button AS INTEGER, Shift AS INTEGER, X AS SINGLE, Y AS SINGLE)\nSHARED s$\ns$ = s$ + \"M\"\nEND SUB"));
        let mut host = CaptureHost::default();
        if !modal {
            assert_eq!(vm.run(&mut host), RunEvent::Ended);
        }
        host.ereignis_nach(2, mouse());
        host.ereignis_nach(2, mouse());
        host.ereignis_nach(2, Ereignis::Ende);
        let result = if modal {
            vm.run(&mut host)
        } else {
            vm.run_visible_forms(&mut host)
        };
        assert_eq!(result, RunEvent::Ended, "modal={modal}");
        assert_eq!(string(&vm, "s"), "MM", "modal={modal}");
    }
}

#[test]
fn review_menu_schliessen_stellt_pending_trap_vor_erneutem_warten_zu() {
    struct MenuHost {
        time: u64,
        waits: usize,
    }
    impl Host for MenuHost {
        fn present(&mut self, _: &TextScreen) {}
        fn next_event(&mut self, _: bool) -> Option<Ereignis> {
            None
        }
        fn jetzt_ms(&mut self) -> u64 {
            self.time
        }
        fn warten(&mut self, deadline: Option<u64>) -> Option<Ereignis> {
            self.waits += 1;
            if self.waits == 1 {
                return Some(key("\u{1b}"));
            }
            self.time = deadline.unwrap();
            None
        }
    }
    let mut vm = vm("ON UEVENT GOSUB H\nUEVENT ON\nMenu1.Caption = \"&Datei\"\nItem1.Caption = \"&Ende\"\nForm1.Show\nSTOP\nSLEEP 60\nEND\nH:\ns$ = \"U\"\nRETURN");
    let mut host = CaptureHost::default();
    stop(&mut vm, &mut host);
    vm.forms.handle_key("d", 4);
    vm.rt.traps.melden(tb_runtime::traps::Quelle::UEvent);
    let mut host = MenuHost { time: 0, waits: 0 };
    assert_eq!(vm.run(&mut host), RunEvent::Ended);
    println!(
        "Menü/SLEEP: Zeit={}, Warteaufrufe={}, Handler={}",
        host.time,
        host.waits,
        string(&vm, "s")
    );
    assert_eq!(host.waits, 1);
}

#[test]
fn review_maus_vor_getrappter_taste() {
    let mut vm = vm("DIM SHARED s$\nON KEY(15) GOSUB H\nKEY 15, CHR$(0) + CHR$(20)\nKEY(15) ON\nForm1.Show\nSTOP\nDOEVENTS\nDOEVENTS\nEND\nH:\ns$ = s$ + \"K\"\nRETURN\nSUB Form_MouseDown(Button AS INTEGER, Shift AS INTEGER, X AS SINGLE, Y AS SINGLE)\nSHARED s$\ns$ = s$ + \"M\"\nEND SUB");
    let mut host = CaptureHost::default();
    stop(&mut vm, &mut host);
    host.ereignis(mouse());
    host.ereignis(key("t"));
    assert_eq!(vm.run(&mut host), RunEvent::Ended);
    assert_eq!(string(&vm, "s"), "MK");
}

/// Ereignisse treffen erst beim Warten ein, auch mitten in einer Eingabe.
struct EingabeHost {
    events: std::collections::VecDeque<Ereignis>,
    now: u64,
    deadlines: Vec<Option<u64>>,
}
impl Host for EingabeHost {
    fn present(&mut self, _: &TextScreen) {}
    fn next_event(&mut self, _: bool) -> Option<Ereignis> {
        None
    }
    fn jetzt_ms(&mut self) -> u64 {
        self.now
    }
    fn warten(&mut self, deadline: Option<u64>) -> Option<Ereignis> {
        self.deadlines.push(deadline);
        if let Some(deadline) = deadline {
            self.now = deadline;
            None
        } else {
            Some(self.events.pop_front().unwrap_or(Ereignis::Ende))
        }
    }
}
fn eingabe_host(events: Vec<Ereignis>) -> EingabeHost {
    EingabeHost {
        events: events.into(),
        now: 0,
        deadlines: vec![],
    }
}

#[test]
fn input_fortsetzung_erhaelt_teiltext_ausdruck_und_byref_ziel() {
    for (statement, expected, enter) in [
        ("INPUT s$", "ab", true),
        ("LINE INPUT s$", "ab", true),
        ("s$ = \"x\" + INPUT$(2) + \"y\"", "xaby", false),
    ] {
        let src = format!("ON SIGNAL(2) GOSUB H\nSIGNAL(2) ON\nCALL Lesen(s$)\nEND\nH:\nn% = n% + 1\nRETURN\nSUB Lesen(s$)\n{statement}\nEND SUB");
        let mut vm = vm_for_source(&src);
        let mut events = vec![key("a"), Ereignis::Signal(2), key("b")];
        if enter {
            events.push(key("\r"));
        }
        let mut host = eingabe_host(events);
        assert_eq!(vm.run(&mut host), RunEvent::Ended, "{statement}");
        assert_eq!(string(&vm, "s"), expected);
        assert_eq!(integer(&vm, "n"), 1);
        assert_eq!(host.deadlines.len(), if enter { 4 } else { 3 });
    }
}

#[test]
fn handler_eingabe_und_stop_bewahren_die_aeussere_eingabe() {
    let mut vm = vm_for_source(
        "ON SIGNAL(2) GOSUB H\nSIGNAL(2) ON\nINPUT s$\nEND\nH:\nSTOP\nINPUT t$\nRETURN",
    );
    let mut host = eingabe_host(vec![
        key("a"),
        Ereignis::Signal(2),
        key("h"),
        key("\r"),
        key("b"),
        key("\r"),
    ]);
    stop(&mut vm, &mut host);
    assert_eq!(vm.run(&mut host), RunEvent::Ended);
    assert_eq!(string(&vm, "s"), "ab");
    assert_eq!(string(&vm, "t"), "h");
}

#[test]
fn timer_waehrend_input_wird_vor_erneutem_warten_zugestellt() {
    let mut vm = vm_for_source(
        "ON TIMER(1) GOSUB H\nTIMER ON\nINPUT s$\nEND\nH:\nn% = n% + 1\nTIMER OFF\nRETURN",
    );
    let mut host = eingabe_host(vec![key("a"), key("\r")]);
    assert_eq!(vm.run(&mut host), RunEvent::Ended);
    assert_eq!(string(&vm, "s"), "a");
    assert_eq!(integer(&vm, "n"), 1);
    assert_eq!(host.deadlines, [Some(1000), None, None]);
}

#[test]
fn input_redo_verliert_weder_referenzen_noch_teiltext_beim_trap() {
    let mut vm =
        vm_for_source("ON SIGNAL(2) GOSUB H\nSIGNAL(2) ON\nINPUT \"Zahl:\"; n%\nEND\nH:\nRETURN");
    let mut host = eingabe_host(vec![
        key("x"),
        key("\r"),
        key("4"),
        Ereignis::Signal(2),
        key("2"),
        key("\r"),
    ]);
    assert_eq!(vm.run(&mut host), RunEvent::Ended);
    assert_eq!(integer(&vm, "n"), 42);
    assert_eq!(
        tb_runtime::snapshot::text(&vm.rt.screen)
            .matches("Zahl:")
            .count(),
        2
    );
}

#[test]
fn lokaler_error_handler_nach_trap_verwirft_abgebrochene_teileingabe() {
    let mut vm = vm_for_source("ON SIGNAL(2) GOSUB H\nSIGNAL(2) ON\nCALL Lesen(s$)\nEND\nH:\nERROR 5\nRETURN\nSUB Lesen(s$)\nON LOCAL ERROR GOTO Fehler\nINPUT s$\nEXIT SUB\nFehler:\nINPUT s$\nRESUME NEXT\nEND SUB");
    let mut host = eingabe_host(vec![key("a"), Ereignis::Signal(2), key("b"), key("\r")]);
    assert_eq!(vm.run(&mut host), RunEvent::Ended);
    assert_eq!(string(&vm, "s"), "b");
}

#[test]
fn randomize_eingabe_teilt_den_fortsetzbaren_konsolenpfad() {
    let mut vm = vm_for_source(
        "ON SIGNAL(2) GOSUB H\nSIGNAL(2) ON\nRANDOMIZE\nEND\nH:\nn% = n% + 1\nRETURN",
    );
    let mut host = eingabe_host(vec![key("4"), Ereignis::Signal(2), key("2"), key("\r")]);
    assert_eq!(vm.run(&mut host), RunEvent::Ended);
    assert_eq!(integer(&vm, "n"), 1);
    assert_eq!(
        tb_runtime::snapshot::text(&vm.rt.screen)
            .matches("Random Number Seed")
            .count(),
        1
    );
}

#[test]
fn return_label_verwirft_abgebrochene_eingabe_und_ausdruck() {
    for statement in ["INPUT s$", "s$ = \"alt\" + INPUT$(2)"] {
        let mut vm = vm_for_source(&format!("ON SIGNAL(2) GOSUB H\nSIGNAL(2) ON\n{statement}\nEND\nWeiter:\nINPUT t$\nEND\nH:\nRETURN Weiter"));
        let mut host = eingabe_host(vec![key("a"), Ereignis::Signal(2), key("b"), key("\r")]);
        assert_eq!(vm.run(&mut host), RunEvent::Ended, "{statement}");
        assert_eq!(string(&vm, "s"), "");
        assert_eq!(string(&vm, "t"), "b");
    }
}

#[test]
fn spaete_timeraktivierung_nutzt_hostzeit_auch_ueber_control_parameter() {
    for (setup, start) in [
        ("", "Timer1.Interval = 100"),
        ("", "CALL Starten(Timer1)"),
        (
            "Timer1.Enabled = 0\nTimer1.Interval = 100",
            "Timer1.Enabled = -1",
        ),
    ] {
        let mut vm = vm(&format!("DIM SHARED n%\nForm1.Show\n{setup}\nSTOP\n{start}\nSTOP\nDOEVENTS\nSTOP\nDOEVENTS\nEND\nSUB Starten(t AS CONTROL)\nt.Interval = 100\nEND SUB\nSUB Timer1_Timer()\nSHARED n%\nn% = n% + 1\nEND SUB"));
        let mut host = CaptureHost::default();
        stop(&mut vm, &mut host);
        host.uhr_vorruecken(1000);
        stop(&mut vm, &mut host);
        assert_eq!(vm.forms.next_deadline(), Some(1100));
        host.uhr_vorruecken(99);
        stop(&mut vm, &mut host);
        assert_eq!(integer(&vm, "n"), 0);
        host.uhr_vorruecken(1);
        assert_eq!(vm.run(&mut host), RunEvent::Ended);
        assert_eq!(integer(&vm, "n"), 1);
    }
}

#[test]
fn intervallaenderung_im_handler_verwirft_bereits_faellige_altintervalle() {
    let mut vm = vm("DIM SHARED n%\nForm1.Show\nTimer1.Interval = 100\nSLEEP 60\nSTOP\nDOEVENTS\nSTOP\nDOEVENTS\nEND\nSUB Timer1_Timer()\nSHARED n%\nn% = n% + 1\nIF n% = 1 THEN\nSTOP\nTimer1.Interval = 200\nELSE\nTimer1.Interval = 0\nEND IF\nEND SUB");
    let mut host = CaptureHost::default();
    stop(&mut vm, &mut host); // Im ersten Handler bei 100 ms.
    assert_eq!(host.jetzt_ms(), 100);
    host.uhr_vorruecken(350);
    stop(&mut vm, &mut host); // Alter Rückstau darf nach dem Handler nicht laufen.
    assert_eq!(integer(&vm, "n"), 1);
    assert_eq!(vm.forms.next_deadline(), Some(650));
    host.uhr_vorruecken(199);
    stop(&mut vm, &mut host);
    assert_eq!(integer(&vm, "n"), 1);
    host.uhr_vorruecken(1);
    assert_eq!(vm.run(&mut host), RunEvent::Ended);
    assert_eq!(integer(&vm, "n"), 2);
    assert_eq!(vm.forms.next_deadline(), None);
}

#[test]
fn formular_tastatur_fallback_fuehrt_alle_drei_basic_handler_aus() {
    let mut catalog = FormCatalog::default();
    catalog.add("Form1", ObjectClass::Form, None, false);
    for (input, expected) in [("a", "DPU"), ("\0;", "DU")] {
        let src = "DIM SHARED s$\nForm1.Show\nSTOP\nDOEVENTS\nEND\nSUB Form_KeyDown(KeyCode AS INTEGER, Shift AS INTEGER)\nSHARED s$\ns$ = s$ + \"D\"\nEND SUB\nSUB Form_KeyPress(KeyAscii AS INTEGER)\nSHARED s$\ns$ = s$ + \"P\"\nEND SUB\nSUB Form_KeyUp(KeyCode AS INTEGER, Shift AS INTEGER)\nSHARED s$\ns$ = s$ + \"U\"\nEND SUB";
        let mut vm = Vm::new(tb_vm::compile_source_with_forms("FORM1", src, &catalog).unwrap());
        let mut host = CaptureHost::default();
        stop(&mut vm, &mut host);
        host.ereignis(key(input));
        assert_eq!(vm.run(&mut host), RunEvent::Ended);
        assert_eq!(
            string(&vm, "s"),
            expected,
            "FORM_KEYDOWN/KEYPRESS/KEYUP: Status implementiert, Hosttaste {input:?}"
        );
    }
}

#[test]
fn registrierte_form_ereignisse_erreichen_basic_aus_realen_quellen() {
    use tb_frontend::forms::{self, EventParamType};
    for name in forms::events(ObjectClass::Form) {
        let mut catalog = FormCatalog::default();
        catalog.add("Form1", ObjectClass::Form, None, false);
        catalog.add("Source", ObjectClass::TextBox, Some("Form1"), false);
        let args = forms::event_params(name)
            .unwrap()
            .iter()
            .map(|(n, t)| {
                format!(
                    "{n} AS {}",
                    match t {
                        EventParamType::Integer => "INTEGER",
                        EventParamType::Single => "SINGLE",
                        EventParamType::Control => "CONTROL",
                    }
                )
            })
            .collect::<Vec<_>>()
            .join(", ");
        let operation = match *name {
            "LOAD" | "GOTFOCUS" => "Form1.Show",
            "LOSTFOCUS" => "Form1.Hide",
            "UNLOAD" => "Form1.Unload",
            "PAINT" => "Form1.Refresh",
            "RESIZE" => "Form1.Width = 70",
            "DRAGOVER" | "DRAGDROP" => "Source.Drag 1",
            "KEYDOWN" | "KEYPRESS" | "KEYUP" | "MOUSEDOWN" | "MOUSEMOVE" | "MOUSEUP" | "CLICK"
            | "DBLCLICK" => "",
            other => panic!("FORM_{other}: Status implementiert, Auslöser fehlt"),
        };
        let setup = if matches!(*name, "LOAD" | "GOTFOCUS") {
            ""
        } else {
            "Form1.Show"
        };
        let src = format!("DIM SHARED n%\nSource.Left=2\nSource.Top=2\n{setup}\nn%=0\nSTOP\n{operation}\nDOEVENTS\nEND\nSUB Form_{name}({args})\nSHARED n%\nn%=n%+1\nEND SUB");
        let mut vm = Vm::new(
            tb_vm::compile_source_with_forms("FORM1", &src, &catalog)
                .unwrap_or_else(|d| panic!("{name}: {d:?}")),
        );
        let mut host = CaptureHost::default();
        stop(&mut vm, &mut host);
        if name.starts_with("KEY") {
            host.ereignis(key("a"));
        }
        let events: &[MausArt] = match *name {
            "MOUSEDOWN" => &[MausArt::Druck],
            "MOUSEMOVE" | "DRAGOVER" => &[MausArt::Bewegung],
            "MOUSEUP" | "DRAGDROP" => &[MausArt::Bewegung, MausArt::Loslassen],
            "CLICK" => &[MausArt::Druck, MausArt::Loslassen],
            "DBLCLICK" => &[
                MausArt::Druck,
                MausArt::Loslassen,
                MausArt::Druck,
                MausArt::Loslassen,
            ],
            _ => &[],
        };
        for art in events {
            host.ereignis(Ereignis::Maus(MausEreignis {
                art: *art,
                taste: 1,
                shift: 0,
                zeile: 1,
                spalte: 1,
            }));
        }
        assert_eq!(vm.run(&mut host), RunEvent::Ended, "FORM_{name}");
        event_nachgewiesen(&format!("FORM_{name}"), integer(&vm, "n")).unwrap();
    }
}

#[test]
fn registrierte_control_ereignisse_erreichen_basic_aus_realen_quellen() {
    use tb_frontend::forms::{self, EventParamType};
    let mut failures = Vec::new();
    for class in ObjectClass::ALL
        .iter()
        .copied()
        .filter(|c| *c != ObjectClass::Form)
    {
        for name in forms::events(class) {
            let mut catalog = FormCatalog::default();
            catalog.add("Form1", ObjectClass::Form, None, false);
            if class == ObjectClass::Menu {
                catalog.add("Bar", ObjectClass::Menu, Some("Form1"), false);
            }
            catalog.add(
                "Probe",
                class,
                Some(if class == ObjectClass::Menu {
                    "Bar"
                } else {
                    "Form1"
                }),
                false,
            );
            catalog.add("Source", ObjectClass::TextBox, Some("Form1"), false);
            let args = forms::event_params(name)
                .unwrap()
                .iter()
                .map(|(n, t)| {
                    format!(
                        "{n} AS {}",
                        match t {
                            EventParamType::Integer => "INTEGER",
                            EventParamType::Single => "SINGLE",
                            EventParamType::Control => "CONTROL",
                        }
                    )
                })
                .collect::<Vec<_>>()
                .join(", ");
            let mut setup = String::from(
                "Form1.Width=60\nForm1.Height=15\nForm1.Show\nSource.Left=40\nSource.Top=2\n",
            );
            if !matches!(class, ObjectClass::Timer | ObjectClass::Menu) {
                setup.push_str("Probe.Left=10\nProbe.Top=4\n");
            }
            let operation = match *name {
                "GOTFOCUS" => String::new(),
                "LOSTFOCUS" => "Source.SetFocus".into(),
                "PAINT" => "Probe.Refresh".into(),
                "DRAGOVER" | "DRAGDROP" => "Source.Drag 1".into(),
                "CHANGE" => match class {
                    ObjectClass::Label => "Probe.Caption=\"changed\"".into(),
                    ObjectClass::HScrollBar | ObjectClass::VScrollBar => "Probe.Value=1".into(),
                    ObjectClass::DirListBox => {
                        format!("Probe.Path=\"{}\"", std::env::temp_dir().display())
                    }
                    ObjectClass::DriveListBox => "Probe.Drive=\"/\"".into(),
                    _ => "Probe.Text=\"changed\"".into(),
                },
                "PATHCHANGE" => format!("Probe.Path=\"{}\"", std::env::temp_dir().display()),
                "PATTERNCHANGE" => "Probe.Pattern=\"*.bas\"".into(),
                "TIMER" => "Probe.Interval=1\nProbe.Enabled=-1".into(),
                "CLICK" if class == ObjectClass::Menu => {
                    setup.push_str("Probe.Caption=\"&Go\"\nProbe.Shortcut=\"F3\"\n");
                    String::new()
                }
                "CLICK" | "DBLCLICK" | "MOUSEDOWN" | "MOUSEMOVE" | "MOUSEUP" | "KEYDOWN"
                | "KEYPRESS" | "KEYUP" | "DROPDOWN" | "CUSTOM" => String::new(),
                other => panic!(
                    "{}_{other}: Status implementiert, Auslöser fehlt",
                    class.name()
                ),
            };
            let src=format!("DIM SHARED n%\n{setup}\nn%=0\nSTOP\n{operation}\nSTOP\nDOEVENTS\nEND\nSUB Probe_{name}({args})\nSHARED n%\nn%=n%+1\nEND SUB");
            let mut vm = Vm::new(
                tb_vm::compile_source_with_forms("FORM1", &src, &catalog)
                    .unwrap_or_else(|d| panic!("{}_{name}: {d:?}", class.name())),
            );
            let mut host = CaptureHost::default();
            let initial = vm.run(&mut host);
            assert!(
                matches!(initial, RunEvent::Stopped { .. }),
                "{}_{name}: {initial:?}",
                class.name()
            );
            if *name == "LOSTFOCUS" {
                vm.forms.focus((1, None)).unwrap();
            }
            let second = vm.run(&mut host);
            assert!(
                matches!(second, RunEvent::Stopped { .. }),
                "{}_{name}: {second:?}",
                class.name()
            );
            if name.starts_with("KEY") || matches!(*name, "DROPDOWN" | "CUSTOM") {
                vm.forms.focus((1, None)).unwrap();
            }
            if *name == "TIMER" {
                host.uhr_vorruecken(2);
            }
            if name.starts_with("KEY") {
                host.ereignis(key("a"));
            }
            if *name == "DROPDOWN" {
                host.ereignis(Ereignis::Taste("\0P".into(), 4));
            }
            if *name == "CUSTOM" {
                host.ereignis(key("\0H"));
            }
            if class == ObjectClass::Menu {
                host.ereignis(key("\0="));
            }
            let events: &[MausArt] = match *name {
                "MOUSEDOWN" | "GOTFOCUS" => &[MausArt::Druck],
                "MOUSEMOVE" | "DRAGOVER" => &[MausArt::Bewegung],
                "MOUSEUP" | "DRAGDROP" => &[MausArt::Bewegung, MausArt::Loslassen],
                "CLICK" if class != ObjectClass::Menu => &[MausArt::Druck, MausArt::Loslassen],
                "DBLCLICK" => &[
                    MausArt::Druck,
                    MausArt::Loslassen,
                    MausArt::Druck,
                    MausArt::Loslassen,
                ],
                _ => &[],
            };
            for art in events {
                host.ereignis(Ereignis::Maus(MausEreignis {
                    art: *art,
                    taste: 1,
                    shift: 0,
                    zeile: 5,
                    spalte: 11,
                }));
            }
            let result = vm.run(&mut host);
            if result != RunEvent::Ended || integer(&vm, "n") == 0 {
                failures.push(format!(
                    "{}_{name}: Status implementiert, Handler unerreicht: {result:?}",
                    class.name()
                ));
            }
        }
    }
    assert!(failures.is_empty(), "{}", failures.join("\n"));
}

#[test]
fn shell_und_irr_funktionssignaturen_erreichen_die_runtime() {
    let mut vm = vm_for_source(
        "DIM a(1) AS DOUBLE\na(0)=-100\na(1)=110\nr#=IRR#(a,.1)\ns&=SHELL(\"exit 7\")\nEND",
    );
    assert_eq!(vm.run(&mut CaptureHost::default()), RunEvent::Ended);
    assert!(matches!(vm.inspect("r"),Some(Value::Dbl(n)) if (n-0.1).abs()<1e-8));
    assert!(matches!(vm.inspect("s"), Some(Value::Lng(7))));
}

fn event_nachgewiesen(name: &str, count: i16) -> Result<(), String> {
    if count > 0 {
        Ok(())
    } else {
        Err(format!(
            "{name}: Status implementiert, fehlender Auslösepfad zum BASIC-Handler"
        ))
    }
}

#[test]
fn fehlender_ereignispfad_wird_namentlich_erkannt() {
    let mut catalog = FormCatalog::default();
    catalog.add("Form1", ObjectClass::Form, None, false);
    let mut module=tb_vm::compile_source_with_forms("FORM1","DIM SHARED n%\nForm1.Show\nSTOP\nDOEVENTS\nEND\nSUB Form_KeyPress(KeyAscii AS INTEGER)\nSHARED n%\nn%=n%+1\nEND SUB",&catalog).unwrap();
    module.event_procs.clear(); // künstlich fehlende Dispatch-Bindung
    let mut vm = Vm::new(module);
    let mut host = CaptureHost::default();
    stop(&mut vm, &mut host);
    host.ereignis(key("a"));
    assert_eq!(vm.run(&mut host), RunEvent::Ended);
    let error = event_nachgewiesen("FORM_KEYPRESS", integer(&vm, "n")).unwrap_err();
    assert!(
        error.contains("FORM_KEYPRESS")
            && error.contains("implementiert")
            && error.contains("Auslösepfad")
    );
}
