//! Interpreter-Tests: Spec-Szenarien aus vm-ausfuehrung und
//! vm-fehlerbehandlung (Phase-2-Änderung).

use tb_runtime::host::CaptureHost;
use tb_runtime::value::Value;
use tb_vm::interp::{RunEvent, Vm};

fn run(src: &str) -> (RunEvent, String) {
    run_with_input(src, &[])
}

fn run_with_input(src: &str, input: &[&str]) -> (RunEvent, String) {
    let module = tb_vm::compile_source("TEST", src).unwrap_or_else(|d| {
        panic!("Compile-Fehler: {d:?}");
    });
    let mut vm = Vm::new(module);
    let mut host = CaptureHost::with_input(input);
    let ev = vm.run(&mut host);
    // Die Tests vergleichen Textausgabe; der Bildschirm-Snapshot ist der
    // Nachfolger des früheren Byte-Stroms (Entscheidung 2026-09-03, D3).
    (ev, tb_runtime::snapshot::text(&vm.rt.screen))
}

fn out(src: &str) -> String {
    let (ev, output) = run(src);
    assert_eq!(ev, RunEvent::Ended, "unerwartetes Ende: {ev:?}\n{output}");
    output
}

fn err_code(src: &str) -> u16 {
    match run(src).0 {
        RunEvent::Error { code, .. } => code,
        other => panic!("Fehler erwartet, war {other:?}"),
    }
}

/// Arbeitsverzeichnis für Tests mit Datei-E/A: ein frisches temporäres
/// Verzeichnis, das beim Verlassen samt Inhalt verschwindet. Programme
/// hinterlassen dadurch nichts im Projektbaum.
struct TempVerzeichnis {
    dir: std::path::PathBuf,
}

impl TempVerzeichnis {
    fn neu(name: &str) -> TempVerzeichnis {
        let dir = std::env::temp_dir().join(format!(
            "tb_vm_{name}_{}_{:?}",
            std::process::id(),
            std::thread::current().id()
        ));
        let _ = std::fs::remove_dir_all(&dir);
        std::fs::create_dir_all(&dir).unwrap();
        TempVerzeichnis { dir }
    }

    fn pfad(&self, name: &str) -> String {
        self.dir.join(name).to_string_lossy().replace('"', "\"\"")
    }
}

impl Drop for TempVerzeichnis {
    fn drop(&mut self) {
        let _ = std::fs::remove_dir_all(&self.dir);
    }
}

// ---- 5.1 Ausdrücke und Konvertierungsmatrix --------------------------------

#[test]
fn print_hallo_welt() {
    assert_eq!(out("PRINT \"Hallo, Welt!\"\nEND"), "Hallo, Welt!\n");
}

#[test]
fn grafik_line_paint_und_view_zeichnen_in_den_zellenpuffer() {
    assert_eq!(
        out("SCREEN 2\nVIEW (0,0)-(31,15),0,1\nLINE (0,0)-(15,7),1,BF\nPAINT (24,8),1\nEND"),
        "████\n████\n"
    );
}

#[test]
fn run_gibt_datei_oder_zeile_an_den_runner_weiter() {
    assert_eq!(
        run("RUN \"next\"").0,
        RunEvent::Restart {
            program: Some("next".into()),
            line: None
        }
    );
    assert_eq!(
        run("RUN 100").0,
        RunEvent::Restart {
            program: None,
            line: Some(100)
        }
    );
}

#[test]
fn common_dialog_registrierung_setzt_byref_erfolg() {
    assert_eq!(
        out("DECLARE SUB CmnDlgRegister(ok AS INTEGER)\nCmnDlgRegister ok%\nPRINT ok%\nEND"),
        "-1 \n"
    );
}

#[test]
fn len_liefert_die_feste_satzbreite_eines_udt() {
    assert_eq!(
        out("TYPE Satz\nText AS STRING * 3\nZahl AS INTEGER\nEND TYPE\nDIM Wert AS Satz\nPRINT LEN(Wert)"),
        " 14 \n"
    );
}

#[test]
fn banker_rounding_cint() {
    // Spec-Szenario: PRINT CINT(0.5); CINT(1.5); CINT(2.5) → " 0  2  2 "
    assert_eq!(out("PRINT CINT(0.5); CINT(1.5); CINT(2.5)"), " 0  2  2 \n");
}

#[test]
fn overflow_bei_zuweisung() {
    // Spec-Szenario: INTEGER-Variable = 40000 → Fehler 6
    assert_eq!(err_code("i% = 40000"), 6);
}

#[test]
fn intdiv_rundet_operanden_vor() {
    // Spec-Szenario: PRINT 7.5 \ 2 → 8 \ 2 = 4
    assert_eq!(out("PRINT 7.5 \\ 2"), " 4 \n");
}

#[test]
fn gemischte_arithmetik() {
    assert_eq!(out("i% = 2\nd# = i% + 1.5#\nPRINT d#"), " 3.5 \n");
    assert_eq!(out("PRINT 1 / 3"), " .3333333 \n"); // SINGLE-Division
    assert_eq!(out("PRINT 7 MOD 3; -7 MOD 3"), " 1 -1 \n"); // Vorzeichen wie Dividend
    assert_eq!(out("PRINT 2 ^ 10"), " 1024 \n");
}

#[test]
fn logik_bitweise() {
    assert_eq!(
        out("PRINT 6 AND 3; 6 OR 3; 6 XOR 3; NOT 0"),
        " 2  7  5 -1 \n"
    );
    // Operanden werden gerundet: 1.5 AND 1 → 2 AND 1 = 0
    assert_eq!(out("PRINT 1.5 AND 1"), " 0 \n");
}

#[test]
fn division_durch_null() {
    assert_eq!(err_code("PRINT 1 / 0"), 11);
    assert_eq!(err_code("PRINT 1 \\ 0"), 11);
    assert_eq!(err_code("PRINT 0 ^ -1"), 11);
}

#[test]
fn stringvergleich_und_verkettung() {
    assert_eq!(out("PRINT \"a\" + \"b\""), "ab\n");
    assert_eq!(out("PRINT (\"abc\" < \"abd\")"), "-1 \n");
}

// ---- 5.2 Kontrollfluss -------------------------------------------------------

#[test]
fn for_ohne_durchlauf() {
    // Spec-Szenario: FOR i% = 3 TO 1 → Körper wird nicht betreten
    assert_eq!(
        out("FOR i% = 3 TO 1\nPRINT i%\nNEXT\nPRINT \"ende\""),
        "ende\n"
    );
}

#[test]
fn for_mit_negativem_step() {
    assert_eq!(
        out("FOR i% = 3 TO 1 STEP -1\nPRINT i%;\nNEXT\nPRINT"),
        " 3  2  1 \n"
    );
}

#[test]
fn for_mit_dynamischem_step() {
    assert_eq!(
        out("s% = -2\nFOR i% = 5 TO 1 STEP s%\nPRINT i%;\nNEXT\nPRINT"),
        " 5  3  1 \n"
    );
}

#[test]
fn select_case_bereiche() {
    let src = "FOR i% = 1 TO 4\nSELECT CASE i%\nCASE 1: PRINT \"eins\"\nCASE 2 TO 3: PRINT \"mittel\"\nCASE ELSE: PRINT \"rest\"\nEND SELECT\nNEXT";
    assert_eq!(out(src), "eins\nmittel\nmittel\nrest\n");
}

#[test]
fn do_loop_varianten() {
    assert_eq!(
        out("n% = 3\nDO WHILE n% > 0\nPRINT n%;\nn% = n% - 1\nLOOP\nPRINT"),
        " 3  2  1 \n"
    );
    assert_eq!(
        out("n% = 0\nDO\nn% = n% + 1\nLOOP UNTIL n% >= 3\nPRINT n%"),
        " 3 \n"
    );
    assert_eq!(
        out("n% = 5\nWHILE n% > 3\nn% = n% - 1\nWEND\nPRINT n%"),
        " 3 \n"
    );
}

#[test]
fn exit_for_und_do() {
    assert_eq!(
        out("FOR i% = 1 TO 10\nIF i% = 3 THEN EXIT FOR\nNEXT\nPRINT i%"),
        " 3 \n"
    );
}

#[test]
fn kontrollfluss_korpusdatei() {
    // 5.2-Verifikation: kontrollfluss.bas byte-genau korrekt.
    let src = std::fs::read_to_string(
        std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
            .join("../../tests/compat/kontrollfluss.bas"),
    )
    .unwrap();
    let want = std::fs::read_to_string(
        std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
            .join("../../tests/compat/kontrollfluss.out"),
    )
    .unwrap();
    assert_eq!(out(&src), want);
}

#[test]
fn gosub_return_und_fehler_3() {
    assert_eq!(
        out("GOSUB U\nPRINT \"zurueck\"\nEND\nU:\nPRINT \"unten\"\nRETURN"),
        "unten\nzurueck\n"
    );
    // Spec-Szenario: RETURN ohne GOSUB → Fehler 3
    assert_eq!(err_code("RETURN"), 3);
}

#[test]
fn on_goto_berechnet() {
    let src = "FOR i% = 0 TO 3\nON i% GOTO A, B\nPRINT \"kein\";\nGOTO W\nA:\nPRINT \"a\";\nGOTO W\nB:\nPRINT \"b\";\nW:\nNEXT\nPRINT";
    assert_eq!(out(src), "keinabkein\n");
}

// ---- 5.3 Prozeduren ------------------------------------------------------------

#[test]
fn byref_wirkt_zurueck() {
    // Spec-Szenario BYREF
    assert_eq!(
        out("SUB Inc (x%)\nx% = x% + 1\nEND SUB\nn% = 1\nInc n%\nPRINT n%"),
        " 2 \n"
    );
}

#[test]
fn klammern_erzwingen_byval() {
    // Spec-Szenario BYVAL
    assert_eq!(
        out("SUB Inc (x%)\nx% = x% + 1\nEND SUB\nn% = 1\nInc (n%)\nPRINT n%"),
        " 1 \n"
    );
}

#[test]
fn byref_auf_arrayelement() {
    assert_eq!(
        out("SUB Inc (x%)\nx% = x% + 1\nEND SUB\nDIM a%(5)\na%(2) = 7\nInc a%(2)\nPRINT a%(2)"),
        " 8 \n"
    );
}

#[test]
fn function_und_rekursion() {
    assert_eq!(
        out("FUNCTION Quad (x)\nQuad = x * x\nEND FUNCTION\nPRINT Quad(3)"),
        " 9 \n"
    );
    let fak = "FUNCTION Fak& (n%)\nIF n% <= 1 THEN\nFak& = 1\nELSE\nFak& = n% * Fak&(n% - 1)\nEND IF\nEND FUNCTION\nPRINT Fak&(10)";
    assert_eq!(out(fak), " 3628800 \n");
}

#[test]
fn static_behaelt_werte() {
    let src = "SUB Zaehl\nSTATIC n%\nn% = n% + 1\nPRINT n%;\nEND SUB\nZaehl\nZaehl\nZaehl\nPRINT";
    assert_eq!(out(src), " 1  2  3 \n");
}

#[test]
fn def_fn_im_modulkontext() {
    // DEF FN: Parameter lokal (BYVAL), freie Namen binden an Modulvariablen.
    let src = "faktor = 10\nDEF FNmal (x) = x * faktor\nPRINT FNmal(3)";
    assert_eq!(out(src), " 30 \n");
}

#[test]
fn ganzes_array_uebergeben() {
    let src = "SUB Summe (a%(), s%)\ns% = 0\nFOR i% = LBOUND(a%) TO UBOUND(a%)\ns% = s% + a%(i%)\nNEXT\nEND SUB\nDIM w%(3)\nFOR i% = 0 TO 3\nw%(i%) = i%\nNEXT\nSumme w%(), erg%\nPRINT erg%";
    assert_eq!(out(src), " 6 \n");
}

#[test]
fn udt_wertsemantik_und_felder() {
    let src = "TYPE Punkt\nx AS INTEGER\ny AS INTEGER\nEND TYPE\nDIM a AS Punkt, b AS Punkt\na.x = 1\nb = a\nb.x = 9\nPRINT a.x; b.x";
    assert_eq!(out(src), " 1  9 \n");
}

// ---- 5.4 DATA/READ/RESTORE -----------------------------------------------------

#[test]
fn data_read_restore() {
    // Anmerkung: unquotierte DATA-Texte verlieren derzeit die
    // Groß-/Kleinschreibung (Lexer normalisiert Bezeichner);
    // Rohtext-Erhalt ist als Aufgabe in PLAN.md Phase 3 eingeplant.
    let src =
        "DATA 1, 2.5, \"hallo\"\nREAD a%, b!, c$\nPRINT a%; b!; c$\nRESTORE\nREAD x%\nPRINT x%";
    assert_eq!(out(src), " 1  2.5 hallo\n 1 \n");
}

#[test]
fn out_of_data_fehler_4() {
    assert_eq!(err_code("DATA 1\nREAD a%, b%"), 4);
}

#[test]
fn data_typkonflikt_fehler_13() {
    assert_eq!(err_code("DATA hallo\nREAD a%"), 13);
}

#[test]
fn restore_mit_label() {
    let src = "DATA 1\nMarke:\nDATA 2\nREAD a%\nRESTORE Marke\nREAD b%\nPRINT a%; b%";
    assert_eq!(out(src), " 1  2 \n");
}

// ---- 5.5 Fehlerbehandlung -------------------------------------------------------

#[test]
fn modulweiter_handler_faengt_prozedurfehler() {
    // Spec-Szenario: Hauptprogramm setzt Handler, SUB löst Fehler 6 aus.
    let src = "SUB Knall\ni% = 40000\nEND SUB\nON ERROR GOTO Fehler\nKnall\nPRINT \"nie\"\nEND\nFehler:\nPRINT \"ERR=\"; ERR\nEND";
    assert_eq!(out(src), "ERR= 6 \n");
}

#[test]
fn lokaler_handler_verdeckt_modulweiten() {
    let src = "SUB Tu\nON LOCAL ERROR GOTO L\nERROR 5\nEXIT SUB\nL:\nPRINT \"lokal\"; ERR\nRESUME Weiter\nWeiter:\nEND SUB\nON ERROR GOTO M\nTu\nEND\nM:\nPRINT \"modul\"\nEND";
    assert_eq!(out(src), "lokal 5 \n");
}

#[test]
fn ohne_handler_bricht_ab() {
    // Spec-Szenario: Fehler 9 ohne Handler → Abbruch mit Meldung.
    let (ev, _) = run("DIM a%(3)\nPRINT a%(7)");
    match ev {
        RunEvent::Error { code, message, .. } => {
            assert_eq!(code, 9);
            assert_eq!(message, "Subscript out of range");
        }
        other => panic!("{other:?}"),
    }
}

#[test]
fn erl_liefert_zeilennummer() {
    let src = "ON ERROR GOTO H\n10 ERROR 5\nEND\nH:\nPRINT ERL\nEND";
    assert_eq!(out(src), " 10 \n");
}

#[test]
fn erl_null_ohne_zeilennummern() {
    // Spec-Szenario: keine numerischen Zeilennummern → ERL = 0
    let src = "ON ERROR GOTO H\nERROR 5\nEND\nH:\nPRINT ERL\nEND";
    assert_eq!(out(src), " 0 \n");
}

#[test]
fn error_anweisung() {
    // Spec-Szenario: ERROR 53 → Handler mit ERR = 53
    let src = "ON ERROR GOTO H\nERROR 53\nEND\nH:\nPRINT ERR\nEND";
    assert_eq!(out(src), " 53 \n");
}

#[test]
fn resume_wiederholt_anweisung() {
    let src = "ON ERROR GOTO H\nn% = 0\nversuch% = 0\n10 versuch% = versuch% + 1\nIF versuch% < 3 THEN ERROR 5\nPRINT versuch%\nEND\nH:\nRESUME";
    // RESUME wiederholt die IF-Anweisung; versuch% bleibt 1? Nein:
    // Fehler in IF-Zeile, RESUME wiederholt IF — Endlosschleife ohne
    // Zählerénderung wäre falsch. Zähler steht in Zeile 10, daher: der
    // Fehler passiert im IF, RESUME wiederholt das IF, versuch% ist noch
    // < 3 … Um Determinismus zu sichern, zählt der Handler mit.
    let _ = src;
    let src2 = "ON ERROR GOTO H\nversuch% = 0\nERROR 5\nPRINT \"nach\"; versuch%\nEND\nH:\nversuch% = versuch% + 1\nIF versuch% < 3 THEN RESUME\nRESUME NEXT";
    assert_eq!(out(src2), "nach 3 \n");
}

#[test]
fn resume_next_faehrt_fort() {
    // Spec-Szenario: RESUME NEXT nach Division durch 0.
    let src = "ON ERROR GOTO H\nx = 1 / 0\nPRINT \"weiter\"\nEND\nH:\nRESUME NEXT";
    assert_eq!(out(src), "weiter\n");
}

#[test]
fn resume_ohne_fehler_20() {
    assert_eq!(err_code("RESUME"), 20);
}

#[test]
fn fehler_im_handler_ist_fatal() {
    // Spec-Szenario: kein Kaskadieren.
    let src = "ON ERROR GOTO H\nERROR 5\nEND\nH:\ni% = 40000\nEND";
    let (ev, _) = run(src);
    match ev {
        RunEvent::Error { code, .. } => assert_eq!(code, 6),
        other => panic!("{other:?}"),
    }
}

#[test]
fn on_error_goto_0_deaktiviert() {
    let src = "ON ERROR GOTO H\nON ERROR GOTO 0\nERROR 5\nEND\nH:\nPRINT \"nie\"\nEND";
    assert_eq!(err_code(src), 5);
}

// ---- 5.6 Unterbrechbarkeit -------------------------------------------------------

#[test]
fn breakpoint_haelt_an_und_setzt_fort() {
    let module =
        tb_vm::compile_source("TEST", "a% = 1\nb% = 2\nc% = 3\nPRINT a% + b% + c%").unwrap();
    let mut vm = Vm::new(module);
    let mut host = CaptureHost::default();
    vm.add_breakpoint(3);
    // Spec-Szenario: hält VOR der Anweisung in Zeile 3.
    match vm.run(&mut host) {
        RunEvent::Breakpoint { line } => assert_eq!(line, 3),
        other => panic!("{other:?}"),
    }
    // Spec-Szenario Inspektion: b% ist gesetzt, c% noch 0.
    assert!(matches!(vm.inspect("b%"), Some(Value::Int(2))));
    assert!(matches!(vm.inspect("c%"), Some(Value::Int(0))));
    // Fortsetzen (Breakpoint entfernen, sonst hält Zeile 3 erneut).
    vm.remove_breakpoint(3);
    assert_eq!(vm.run(&mut host), RunEvent::Ended);
    assert_eq!(tb_runtime::snapshot::text(&vm.rt.screen), " 6 \n");
}

#[test]
fn einzelschritt() {
    let module = tb_vm::compile_source("TEST", "a% = 1\nb% = 2\nEND").unwrap();
    let mut vm = Vm::new(module);
    let mut host = CaptureHost::default();
    vm.set_step(true);
    let mut lines = Vec::new();
    loop {
        match vm.run(&mut host) {
            RunEvent::Stepped { line } => lines.push(line),
            RunEvent::Ended => break,
            other => panic!("{other:?}"),
        }
    }
    assert_eq!(lines, vec![1, 2, 3]);
}

// ---- 5.7 Programmende --------------------------------------------------------------

#[test]
fn stop_liefert_zeile_und_ist_fortsetzbar() {
    let module = tb_vm::compile_source("TEST", "PRINT \"a\"\nSTOP\nPRINT \"b\"").unwrap();
    let mut vm = Vm::new(module);
    let mut host = CaptureHost::default();
    match vm.run(&mut host) {
        RunEvent::Stopped { line } => assert_eq!(line, 2),
        other => panic!("{other:?}"),
    }
    // IDE-Semantik: CONT = weiterlaufen.
    assert_eq!(vm.run(&mut host), RunEvent::Ended);
    assert_eq!(tb_runtime::snapshot::text(&vm.rt.screen), "a\nb\n");
}

#[test]
fn end_und_system() {
    assert_eq!(run("PRINT \"x\"\nEND").0, RunEvent::Ended);
    assert_eq!(run("SYSTEM").0, RunEvent::Ended);
}

// ---- Eingabe -----------------------------------------------------------------------

#[test]
fn input_mit_redo() {
    let (ev, output) = run_with_input("INPUT \"Zahl\"; n%\nPRINT n% * 2", &["abc", "21"]);
    assert_eq!(ev, RunEvent::Ended);
    assert!(output.contains("Redo from start"));
    assert!(output.ends_with(" 42 \n"), "{output}");
}

#[test]
fn line_input_liest_ganze_zeile() {
    let (ev, output) = run_with_input("LINE INPUT s$\nPRINT s$", &["a, b, c"]);
    assert_eq!(ev, RunEvent::Ended);
    assert!(output.ends_with("a, b, c\n"));
}

// ---- Strings/PRINT über die VM -------------------------------------------------------

#[test]
fn mid_anweisung_mutiert() {
    assert_eq!(
        out("s$ = \"hallo\"\nMID$(s$, 2, 2) = \"EY\"\nPRINT s$"),
        "hEYlo\n"
    );
}

#[test]
fn print_zonen_und_tab() {
    assert_eq!(out("PRINT \"a\", \"b\""), "a             b\n");
    assert_eq!(out("PRINT TAB(5); \"x\""), "    x\n");
    assert_eq!(out("PRINT \"a\"; SPC(3); \"b\""), "a   b\n");
}

/// Aufgabe 2.4 des Changes `phase-3-isam`: `OPEN … FOR ISAM` senkt nicht
/// mehr auf den „nicht verfügbar"-Marker ab, sondern arbeitet. Der Lauf
/// findet in einem temporären Verzeichnis statt und lässt nichts zurück.
#[test]
fn open_for_isam_endet_nicht_mehr_mit_fehler_73() {
    let dir = TempVerzeichnis::neu("open_isam");
    let (ev, ausgabe) = run(&format!(
        "TYPE T\n f AS INTEGER\nEND TYPE\n\
         OPEN \"{}\" FOR ISAM T \"Tab\" AS #1\n\
         PRINT \"offen\"\n\
         CLOSE #1",
        dir.pfad("db.isam")
    ));
    assert_eq!(ev, RunEvent::Ended, "unerwartetes Ende: {ev:?}\n{ausgabe}");
    assert!(ausgabe.contains("offen"), "{ausgabe}");
}

// ---- Zustellpunkte des Hosts (Spec konsolen-basisbibliothek) ---------------

/// Vor blockierender Eingabe wird der Bildschirm angezeigt — sonst stünde
/// die Eingabeaufforderung unsichtbar da.
#[test]
fn ausgabe_erscheint_vor_der_eingabe() {
    use tb_runtime::host::CaptureHost;
    let module = tb_vm::compile_source("TEST", "PRINT \"Frage:\"\nINPUT a%\nPRINT a%\n").unwrap();
    let mut vm = Vm::new(module);
    let mut host = CaptureHost::with_input(&["7"]);
    assert_eq!(vm.run(&mut host), RunEvent::Ended);

    // Der Host hat den Bildschirm mehrfach gesehen, und schon beim ersten
    // Mal stand der Text da.
    assert!(host.presents > 0, "Host wurde nie zur Anzeige aufgefordert");
    let text = tb_runtime::snapshot::text(&vm.rt.screen);
    assert!(text.starts_with("Frage:\n"), "{text:?}");
    assert!(text.contains(" 7 "), "{text:?}");
}

/// Eingegangene Zeichen werden auf dem Bildschirm wiederholt (Echo), und
/// Rückschritt löscht das zuletzt getippte Zeichen.
#[test]
fn eingabe_wird_geechot_und_rueckschritt_loescht() {
    use tb_runtime::host::{taste, CaptureHost, Ereignis};
    let module = tb_vm::compile_source("TEST", "INPUT a$\nPRINT a$\n").unwrap();
    let mut vm = Vm::new(module);
    let mut host = CaptureHost::default();
    for c in ['a', 'b', 'x'] {
        host.ereignis(Ereignis::Taste(c.to_string(), 0));
    }
    host.ereignis(Ereignis::Taste(taste::BACKSPACE.to_string(), 0));
    host.ereignis(Ereignis::Taste(taste::ENTER.to_string(), 0));
    assert_eq!(vm.run(&mut host), RunEvent::Ended);
    let text = tb_runtime::snapshot::text(&vm.rt.screen);
    // Erste Zeile: das Echo ohne das gelöschte Zeichen.
    assert_eq!(text.lines().next(), Some("? ab"));
    assert!(text.contains("ab\n"), "{text:?}");
}

/// Größenänderung während des Laufs wirkt auf den Bildschirm.
#[test]
fn groessenaenderung_wirkt_zur_laufzeit() {
    use tb_runtime::host::{CaptureHost, Ereignis};
    let module = tb_vm::compile_source("TEST", "PRINT \"a\"\nFOR i% = 1 TO 50\nNEXT i%\n").unwrap();
    let mut vm = Vm::new(module);
    let mut host = CaptureHost::default();
    host.ereignis(Ereignis::Groesse {
        cols: 120,
        rows: 40,
    });
    assert_eq!(vm.run(&mut host), RunEvent::Ended);
    assert_eq!((vm.rt.screen.cols(), vm.rt.screen.rows()), (120, 40));
    // Inhalt bleibt oben links erhalten.
    assert_eq!(vm.rt.screen.cell(1, 1).ch, 'a');
}

// ---- Bildschirm und Tastatur (Spec textbildschirm) -------------------------

#[test]
fn locate_ausserhalb_loest_fehler_5_aus() {
    assert_eq!(err_code("LOCATE 26, 1"), 5);
    assert_eq!(err_code("LOCATE 1, 81"), 5);
    assert_eq!(err_code("LOCATE 0, 1"), 5);
}

#[test]
fn screen_funktion_ausserhalb_loest_fehler_5_aus() {
    assert_eq!(err_code("x% = SCREEN(30, 1)"), 5);
    assert_eq!(err_code("x% = SCREEN(1, 200)"), 5);
}

#[test]
fn color_prueft_wertebereich() {
    assert_eq!(err_code("COLOR 32, 0"), 5);
    assert_eq!(err_code("COLOR 7, 8"), 5);
}

#[test]
fn screen_anweisung_bildet_grafikmodi_auf_den_zellenpuffer_ab() {
    assert_eq!(out("SCREEN 0\nPRINT \"ok\""), "ok\n");
    assert_eq!(out("SCREEN 13\nPRINT \"ok\""), "ok\n");
    assert_eq!(err_code("m% = 14\nSCREEN m%"), 5);
}

#[test]
fn inkey_ohne_taste_ist_leer() {
    assert_eq!(out("a$ = INKEY$\nPRINT \"[\"; a$; \"]\""), "[]\n");
}

#[test]
fn inkey_liefert_zeichen_und_sondertaste() {
    use tb_runtime::host::{taste, CaptureHost, Ereignis};
    let module = tb_vm::compile_source(
        "T",
        "a$ = INKEY$\nb$ = INKEY$\nPRINT a$; LEN(b$); ASC(b$)\n",
    )
    .unwrap();
    let mut vm = Vm::new(module);
    let mut host = CaptureHost::default();
    host.ereignis(Ereignis::Taste("q".into(), 0));
    host.ereignis(Ereignis::Taste(taste::sonder(59), 0)); // F1
    assert_eq!(vm.run(&mut host), RunEvent::Ended);
    let t = tb_runtime::snapshot::text(&vm.rt.screen);
    // "q", dann Länge 2 und führendes Nullzeichen.
    assert_eq!(t, "q 2  0 \n");
}

#[test]
fn input_s_liest_genau_n_zeichen_ohne_echo() {
    use tb_runtime::host::{CaptureHost, Ereignis};
    let module = tb_vm::compile_source("T", "a$ = INPUT$(3)\nPRINT a$\n").unwrap();
    let mut vm = Vm::new(module);
    let mut host = CaptureHost::default();
    for c in ['x', 'y', 'z', 'w'] {
        host.ereignis(Ereignis::Taste(c.to_string(), 0));
    }
    assert_eq!(vm.run(&mut host), RunEvent::Ended);
    // Nur das PRINT erscheint — INPUT$ echot nicht.
    assert_eq!(tb_runtime::snapshot::text(&vm.rt.screen), "xyz\n");
}

#[test]
fn input_s_liest_aus_einer_binaerdatei() {
    let tmp = TempVerzeichnis::neu("input_s_datei");
    let pfad = tmp.pfad("daten.bin");
    std::fs::write(&pfad, b"abcdef").unwrap();
    assert_eq!(
        out(&format!(
            "OPEN \"{pfad}\" FOR BINARY AS #1\na$ = INPUT$(3, #1)\nPRINT a$\nCLOSE #1"
        )),
        "abc\n"
    );
}

#[test]
fn clipboard_und_printer_objekte_haben_laufzeitwirkung() {
    let module = tb_vm::compile_source(
        "T",
        "CLIPBOARD.ADDITEM \"abc\"\nPRINT CLIPBOARD.GETTEXT\nPRINTER.PRINT \"Seite\"\nPRINTER.NEWPAGE\nPRINTER.ENDDOC",
    )
    .unwrap();
    let mut vm = Vm::new(module);
    assert_eq!(vm.run(&mut CaptureHost::default()), RunEvent::Ended);
    assert_eq!(tb_runtime::snapshot::text(&vm.rt.screen), "abc\n");
    assert_eq!(vm.rt.print.drucker, "Seite\n\u{c}");
}

#[test]
fn cls_setzt_cursor_zurueck() {
    assert_eq!(out("PRINT \"weg\"\nCLS\nPRINT \"neu\""), "neu\n");
    assert_eq!(out("PRINT \"weg\"\nCLS\nPRINT CSRLIN; POS(0)"), " 1  4 \n");
}

#[test]
fn width_hebt_die_groesse_an() {
    assert_eq!(out("WIDTH 120, 40\nPRINT SCREEN(40, 120)"), " 32 \n");
}

// ---- ON ERROR-Scoping (Spec sprach-frontend) -------------------------------

/// Ein modulweiter Handler ist auch aus einer Prozedur heraus ansprechbar;
/// `RESUME NEXT` setzt dann hinter dem Prozeduraufruf fort.
#[test]
fn modulweites_on_error_aus_prozedur() {
    let src = "DECLARE SUB T ()\n\
               ON ERROR GOTO Handler\n\
               T\n\
               PRINT \"zurueck\"\n\
               END\n\
               Handler:\n\
               PRINT \"Handler\"; ERR\n\
               RESUME NEXT\n\
               SUB T\n\
               ON ERROR GOTO Handler\n\
               x = 1 / 0\n\
               END SUB\n";
    assert_eq!(out(src), "Handler 11 \nzurueck\n");
}

/// `ON LOCAL ERROR` verlangt weiterhin ein Label im eigenen Rumpf.
#[test]
fn on_local_error_bleibt_prozedurlokal() {
    let src = "DECLARE SUB T ()\n\
               ON ERROR GOTO Handler\n\
               T\n\
               END\n\
               Handler:\n\
               RESUME NEXT\n\
               SUB T\n\
               ON LOCAL ERROR GOTO Handler\n\
               END SUB\n";
    let d = tb_vm::compile_source("T", src).unwrap_err();
    assert!(
        d.iter().any(|x| x.message.contains("Label not defined")),
        "{d:?}"
    );
}

/// Ein prozedurlokaler Handler fängt vor dem modulweiten.
#[test]
fn lokaler_handler_hat_vorrang() {
    let src = "DECLARE SUB T ()\n\
               ON ERROR GOTO Modul\n\
               T\n\
               END\n\
               Modul:\n\
               PRINT \"Modul\"\n\
               RESUME NEXT\n\
               SUB T\n\
               ON LOCAL ERROR GOTO Lokal\n\
               x = 1 / 0\n\
               EXIT SUB\n\
               Lokal:\n\
               PRINT \"Lokal\"\n\
               RESUME NEXT\n\
               END SUB\n";
    assert_eq!(out(src), "Lokal\n");
}

// ---- Ortszeit (Spec konsolen-basisbibliothek) ------------------------------

use tb_runtime::datetime::Zeitzone;

/// Programm mit festgelegter Zeitzone laufen lassen.
fn out_mit_zone(src: &str, zone: Zeitzone) -> String {
    use tb_runtime::host::CaptureHost;
    let module = tb_vm::compile_source("TEST", src).unwrap_or_else(|d| panic!("{d:?}"));
    let mut vm = Vm::new(module);
    vm.rt.zeitzone = zone;
    let mut host = CaptureHost::default();
    assert_eq!(vm.run(&mut host), RunEvent::Ended);
    tb_runtime::snapshot::text(&vm.rt.screen)
}

/// `TIME$` und `HOUR(NOW)` nennen dieselbe Stunde — sie rechnen beide
/// über den Zonenzustand.
#[test]
fn time_und_now_stimmen_ueberein() {
    let t = out_mit_zone("PRINT LEFT$(TIME$, 2); HOUR(NOW)", Zeitzone::Fest(7200));
    let zeile = t.lines().next().unwrap();
    let aus_time: i32 = zeile[..2].parse().unwrap();
    let aus_now: i32 = zeile[2..].trim().parse().unwrap();
    assert_eq!(aus_time, aus_now, "{zeile:?}");
}

/// Ein fester Versatz verschiebt die Uhr um genau diesen Betrag.
#[test]
fn fester_versatz_verschiebt_die_uhr() {
    let src = "PRINT INT(TIMER)";
    let ohne: f64 = out_mit_zone(src, Zeitzone::Unbekannt)
        .trim()
        .parse()
        .unwrap();
    let mit: f64 = out_mit_zone(src, Zeitzone::Fest(3600))
        .trim()
        .parse()
        .unwrap();
    // Modulo Tageslänge, damit ein Mitternachtsübergang nichts kaputt macht.
    let diff = (mit - ohne).rem_euclid(86_400.0);
    assert!((diff - 3600.0).abs() <= 2.0, "Differenz {diff}");
}

/// `TIMER` zählt ab der lokalen Mitternacht, nicht ab der UTC-Mitternacht.
#[test]
fn timer_zaehlt_ab_lokaler_mitternacht() {
    let t = out_mit_zone(
        "PRINT INT(TIMER / 3600); VAL(LEFT$(TIME$, 2))",
        Zeitzone::Fest(-18000),
    );
    let zahlen: Vec<f64> = t
        .split_whitespace()
        .filter_map(|w| w.parse().ok())
        .collect();
    assert_eq!(zahlen.len(), 2, "{t:?}");
    assert_eq!(zahlen[0], zahlen[1], "TIMER-Stunde ≠ TIME$-Stunde: {t:?}");
}

/// `TIME$ =` wirkt auf die Ortszeit und alle davon abgeleiteten Werte.
#[test]
fn programmversatz_wirkt_auf_alle_zeitfunktionen() {
    let t = out_mit_zone(
        "TIME$ = \"10:20:30\"\nDATE$ = \"01-02-1993\"\nPRINT TIME$; \" \"; DATE$; HOUR(NOW); YEAR(NOW)",
        Zeitzone::Fest(3600),
    );
    assert!(t.starts_with("10:20:30 01-02-1993 10  1993"), "{t:?}");
}

/// Ohne ermittelbare Zone gilt UTC — ohne Fehler.
#[test]
fn rueckfall_auf_utc_laeuft_fehlerfrei() {
    let t = out_mit_zone(
        "PRINT LEN(DATE$); LEN(TIME$); TIMER >= 0; YEAR(NOW) > 2000",
        Zeitzone::Unbekannt,
    );
    assert_eq!(t.trim(), "10  8 -1 -1", "{t:?}");
}

// ---- Ereignis-Traps (Change `phase-4-ereignisschleife`) ---------------------

/// Programm mit virtueller Uhr laufen lassen: die Uhr rückt an jedem
/// Zustellpunkt um `schritt_ms` vor (design.md, D6).
fn run_mit_uhr(src: &str, schritt_ms: u64) -> (RunEvent, String) {
    struct UhrHost {
        inner: CaptureHost,
        schritt: u64,
    }
    impl tb_runtime::host::Host for UhrHost {
        fn present(&mut self, s: &tb_runtime::screen::TextScreen) {
            self.inner.present(s);
        }
        fn next_event(&mut self, b: bool) -> Option<tb_runtime::host::Ereignis> {
            self.inner.next_event(b)
        }
        fn warten(&mut self, deadline_ms: Option<u64>) -> Option<tb_runtime::host::Ereignis> {
            if let Some(event) = self.next_event(false) {
                return Some(event);
            }
            self.inner.warten(deadline_ms)
        }
        fn jetzt_ms(&mut self) -> u64 {
            let j = self.inner.jetzt_ms();
            self.inner.uhr_vorruecken(self.schritt);
            j
        }
    }
    let module = tb_vm::compile_source("TEST", src).unwrap_or_else(|d| {
        panic!("Compile-Fehler: {d:?}");
    });
    let mut vm = Vm::new(module);
    let mut host = UhrHost {
        inner: CaptureHost::default(),
        schritt: schritt_ms,
    };
    let ev = vm.run(&mut host);
    (ev, tb_runtime::snapshot::text(&vm.rt.screen))
}

#[test]
fn zeit_trap_laeuft_und_kehrt_zurueck() {
    // Die Schleife druckt x, der Trap dazwischen T. Nach RETURN zaehlt die
    // Schleife unveraendert weiter — der unterbrochene Zustand bleibt.
    let (ev, out) = run_mit_uhr(
        "ON TIMER(1) GOSUB Tick\n\
         TIMER ON\n\
         FOR i% = 1 TO 200\n\
         NEXT\n\
         PRINT \"i=\"; i%\n\
         END\n\
         Tick:\n\
         PRINT \"T\";\n\
         RETURN\n",
        600,
    );
    assert_eq!(ev, RunEvent::Ended, "{out}");
    assert!(out.contains('T'), "Zeit-Trap lief nicht: {out:?}");
    assert!(
        out.contains("i= 201"),
        "Schleife lief nicht sauber weiter: {out:?}"
    );
}

#[test]
fn ohne_timer_on_kein_trap() {
    let (ev, out) = run_mit_uhr(
        "ON TIMER(1) GOSUB Tick\n\
         FOR i% = 1 TO 200\n\
         NEXT\n\
         PRINT \"fertig\"\n\
         END\n\
         Tick:\n\
         PRINT \"T\";\n\
         RETURN\n",
        600,
    );
    assert_eq!(ev, RunEvent::Ended, "{out}");
    assert!(!out.contains('T'), "Trap lief ohne TIMER ON: {out:?}");
    assert!(out.contains("fertig"));
}

#[test]
fn trap_aus_einer_sub_heraus_kehrt_dorthin_zurueck() {
    let (ev, out) = run_mit_uhr(
        "ON TIMER(1) GOSUB Tick\n\
         TIMER ON\n\
         CALL Rechne\n\
         PRINT \"ende\"\n\
         END\n\
         Tick:\n\
         PRINT \"T\";\n\
         RETURN\n\
         SUB Rechne\n\
           FOR j% = 1 TO 200\n\
           NEXT\n\
           PRINT \"sub\"; j%\n\
         END SUB\n",
        600,
    );
    assert_eq!(ev, RunEvent::Ended, "{out}");
    assert!(out.contains('T'), "Trap lief nicht: {out:?}");
    assert!(
        out.contains("sub 201"),
        "SUB lief nicht sauber weiter: {out:?}"
    );
    assert!(out.contains("ende"));
}

#[test]
fn event_off_klammert_die_zustellung() {
    let (ev, out) = run_mit_uhr(
        "ON TIMER(1) GOSUB Tick\n\
         TIMER ON\n\
         EVENT OFF\n\
         FOR i% = 1 TO 200\n\
         NEXT\n\
         PRINT \"|\";\n\
         EVENT ON\n\
         FOR i% = 1 TO 200\n\
         NEXT\n\
         END\n\
         Tick:\n\
         PRINT \"T\";\n\
         RETURN\n",
        600,
    );
    assert_eq!(ev, RunEvent::Ended, "{out}");
    let text = out.trim();
    let strich = text.find('|').expect("Trennzeichen fehlt");
    assert!(
        !text[..strich].contains('T'),
        "waehrend EVENT OFF wurde zugestellt: {text:?}"
    );
    assert!(
        text[strich..].contains('T'),
        "nach EVENT ON wurde nicht zugestellt: {text:?}"
    );
}

#[test]
fn timer_off_im_handler_ueberlebt_das_return() {
    let (ev, out) = run_mit_uhr(
        "ON TIMER(1) GOSUB Tick\n\
         TIMER ON\n\
         FOR i% = 1 TO 400\n\
         NEXT\n\
         END\n\
         Tick:\n\
         PRINT \"T\";\n\
         TIMER OFF\n\
         RETURN\n",
        600,
    );
    assert_eq!(ev, RunEvent::Ended, "{out}");
    assert_eq!(
        out.trim().matches('T').count(),
        1,
        "RETURN hat den Trap wieder eingeschaltet: {out:?}"
    );
}

#[test]
fn gosub_null_schaltet_den_trap_ab() {
    let (ev, out) = run_mit_uhr(
        "ON TIMER(1) GOSUB Tick\n\
         TIMER ON\n\
         ON TIMER(1) GOSUB 0\n\
         FOR i% = 1 TO 300\n\
         NEXT\n\
         PRINT \"fertig\"\n\
         END\n\
         Tick:\n\
         PRINT \"T\";\n\
         RETURN\n",
        600,
    );
    assert_eq!(ev, RunEvent::Ended, "{out}");
    assert!(!out.contains('T'), "abgeschalteter Trap lief: {out:?}");
}

#[test]
fn setuevent_loest_den_uevent_trap_aus() {
    let (ev, out) = run("ON UEVENT GOSUB Nutzer\n\
         UEVENT ON\n\
         CALL SetUEvent\n\
         FOR i% = 1 TO 20\n\
         NEXT\n\
         PRINT \"|\";\n\
         END\n\
         Nutzer:\n\
         PRINT \"U\";\n\
         RETURN\n");
    assert_eq!(ev, RunEvent::Ended, "{out}");
    assert!(out.contains('U'), "UEVENT-Trap lief nicht: {out:?}");
}

#[test]
fn kennung_ausserhalb_des_bereichs_ist_fehler_5() {
    // Nicht konstant, also erst zur Laufzeit pruefbar.
    let src = "n% = 99\nON KEY(n%) GOSUB M\nEND\nM:\nRETURN\n";
    assert_eq!(err_code(src), 5);
}

#[test]
fn leere_warteschleife_hat_eine_anweisungsgrenze() {
    // `FOR i = 1 TO n: NEXT` ist die klassische Warteschleife. Ohne
    // Anweisungsgrenze im Kreis würde dort nie ein Ereignis zugestellt,
    // kein Breakpoint erreicht und kein Abbruch bemerkt.
    use tb_vm::bytecode::Instr;
    for src in [
        "FOR i% = 1 TO 3\nNEXT\n",
        "WHILE i% < 3\nWEND\n",
        "DO WHILE i% < 3\nLOOP\n",
        "DO\nLOOP UNTIL i% > 3\n",
    ] {
        let m = tb_vm::compile_source("T", src).unwrap();
        let code = &m.procs[0].code;
        let mut kreise = 0;
        for (j, i) in code.iter().enumerate() {
            let ziel = match i {
                Instr::Jump(t) | Instr::JumpIfFalse(t) | Instr::JumpIfTrue(t) => *t as usize,
                _ => continue,
            };
            if ziel >= j {
                continue;
            }
            kreise += 1;
            assert!(
                code[ziel..=j].iter().any(|i| matches!(i, Instr::Stmt(_))),
                "Schleife ohne Anweisungsgrenze im Kreis: {src:?}"
            );
        }
        assert!(kreise > 0, "kein Rücksprung gefunden: {src:?}");
    }
}

#[test]
fn doevents_stellt_zu() {
    // Die Warteschleife ruft DOEVENTS; der Trap muss darin laufen.
    let (ev, out) = run_mit_uhr(
        "ON TIMER(1) GOSUB Tick\n\
         TIMER ON\n\
         FOR i% = 1 TO 5\n\
           x% = DOEVENTS\n\
         NEXT\n\
         PRINT \"|\";\n\
         END\n\
         Tick:\n\
         PRINT \"T\";\n\
         RETURN\n",
        600,
    );
    assert_eq!(ev, RunEvent::Ended, "{out}");
    let t = out.trim();
    let strich = t.find('|').expect("Trennzeichen fehlt");
    assert!(
        t[..strich].contains('T'),
        "DOEVENTS stellte nicht zu: {t:?}"
    );
}

#[test]
fn doevents_liefert_null() {
    assert_eq!(out("PRINT DOEVENTS").trim(), "0");
}

#[test]
fn sleep_endet_durch_ein_ereignis() {
    // SLEEP 60 wuerde ohne Zustellung eine Minute warten. Der faellige
    // Trap beendet es; danach laeuft das Programm hinter dem SLEEP weiter.
    let t = std::time::Instant::now();
    let (ev, out) = run_mit_uhr(
        "ON TIMER(1) GOSUB Tick\n\
         TIMER ON\n\
         SLEEP 60\n\
         PRINT \"danach\"\n\
         END\n\
         Tick:\n\
         PRINT \"T\";\n\
         RETURN\n",
        600,
    );
    assert_eq!(ev, RunEvent::Ended, "{out}");
    assert!(out.contains('T'), "Trap lief nicht: {out:?}");
    assert!(
        out.contains("danach"),
        "SLEEP kehrte nicht zurueck: {out:?}"
    );
    assert!(
        t.elapsed().as_secs() < 5,
        "SLEEP wartete die vollen 60 Sekunden ab"
    );
}

#[test]
fn fehler_im_trap_handler_erreicht_den_modulweiten_handler() {
    // `ERL` nennt die zuletzt ausgefuehrte **Zeilennummer** — deshalb ist
    // das Programm nummeriert. Der Fehler entsteht in Zeile 90 (im
    // Handler), nicht an der unterbrochenen Stelle.
    let (ev, out) = run_mit_uhr(
        "10 ON ERROR GOTO 200\n         20 ON TIMER(1) GOSUB 90\n         30 TIMER ON\n         40 FOR i% = 1 TO 300\n         50 NEXT\n         60 PRINT \"|\";\n         70 END\n         90 ERROR 7\n         100 RETURN\n         200 PRINT \"E\"; ERR; ERL;\n         210 RESUME NEXT\n",
        600,
    );
    assert_eq!(ev, RunEvent::Ended, "{out}");
    assert!(out.contains("E 7"), "Fehlerbehandlung lief nicht: {out:?}");
    assert!(
        out.contains(" 90"),
        "ERL nennt nicht die Zeile im Handler: {out:?}"
    );
}

#[test]
fn erl_nennt_die_fehlerzeile_nicht_die_handlerzeile() {
    // Regression: der Handler steht selbst auf einer nummerierten Zeile.
    // `ERL` muss trotzdem die Zeile des Fehlers melden (Spec
    // vm-fehlerbehandlung, „ERR und ERL").
    let out = out("10 ON ERROR GOTO 100\n\
         20 ERROR 7\n\
         30 END\n\
         100 PRINT ERL;\n\
         110 RESUME NEXT\n");
    assert_eq!(out.trim(), "20");
}

fn forms_catalog() -> tb_frontend::forms::FormCatalog {
    use tb_frontend::forms::ObjectClass;
    let mut c = tb_frontend::forms::FormCatalog::default();
    c.add("Form1", ObjectClass::Form, None, false);
    c.add("Text1", ObjectClass::TextBox, Some("Form1"), false);
    c.add("Command1", ObjectClass::CommandButton, Some("Form1"), true);
    c
}

fn form_vm(src: &str) -> Vm {
    let module = tb_vm::compile_source_with_forms("FORM1", src, &forms_catalog())
        .unwrap_or_else(|d| panic!("Compile-Fehler: {d:?}"));
    Vm::new(module)
}

#[test]
fn objektzugriff_laeuft_ueber_objekt_und_eigenschaftsindex() {
    let mut vm = form_vm("Form1!Text1.Text = \"hallo\"\nPRINT Form1!Text1.Text");
    let ev = vm.run(&mut CaptureHost::default());
    assert_eq!(ev, RunEvent::Ended);
    assert_eq!(tb_runtime::snapshot::text(&vm.rt.screen).trim(), "hallo");
}

#[test]
fn screen_controlpanel_hat_einen_indexierten_laufzeitwert() {
    assert_eq!(
        out("SCREEN.CONTROLPANEL(5) = 3\nPRINT SCREEN.CONTROLPANEL(5)"),
        " 3 \n"
    );
}

#[test]
fn steuerarray_eigenschaften_und_objektparameter_funktionieren() {
    let mut vm = form_vm(
        "LOAD Command1(3)\nCommand1(3).Caption = \"drei\"\nPRINT Command1(3).Caption\n\
         CALL Aus(Text1)\nPRINT Text1.Visible\nCALL FormAus(Form1)\nPRINT Form1.Caption\nEND\n\
         SUB Aus(c AS CONTROL)\nPRINT TYPEOF c IS TextBox\nc.Visible = 0\nEND SUB\n\
         SUB FormAus(f AS FORM)\nPRINT TYPEOF f IS Form\nf.Caption = \"Formular\"\nEND SUB",
    );
    assert_eq!(vm.run(&mut CaptureHost::default()), RunEvent::Ended);
    assert_eq!(
        tb_runtime::snapshot::text(&vm.rt.screen).trim(),
        "drei\n-1 \n 0 \n-1 \nFormular"
    );
}

#[test]
fn modales_show_setzt_nach_unload_fort() {
    let mut vm = form_vm(
        "DIM SHARED gesehen%\nForm1.Show 1\nPRINT gesehen%\nEND\n\
         SUB Form_MouseDown(Button AS INTEGER, Shift AS INTEGER, X AS SINGLE, Y AS SINGLE)\n\
         SHARED gesehen%\ngesehen% = 1\nUNLOAD Form1\nEND SUB",
    );
    let mut host = CaptureHost::default();
    host.ereignis_nach(
        2,
        tb_runtime::host::Ereignis::Maus(tb_runtime::host::MausEreignis {
            art: tb_runtime::host::MausArt::Druck,
            taste: 1,
            shift: 0,
            zeile: 1,
            spalte: 1,
        }),
    );
    let ev = vm.run(&mut host);
    assert_eq!(ev, RunEvent::Ended);
    assert_eq!(tb_runtime::snapshot::text(&vm.rt.screen).trim(), "1");
    assert!(host.presents >= 2);
}

#[test]
fn modelloses_formular_bleibt_nach_programmende_bedienbar() {
    let mut vm = form_vm(
        "DIM SHARED gesehen%\nEND\n\
         SUB Form_MouseDown(Button AS INTEGER, Shift AS INTEGER, X AS SINGLE, Y AS SINGLE)\n\
         SHARED gesehen%\ngesehen% = 1\nUNLOAD Form1\nEND SUB",
    );
    vm.forms.show(0, false).unwrap();
    assert_eq!(vm.run(&mut CaptureHost::default()), RunEvent::Ended);
    let mut host = CaptureHost::default();
    host.ereignis_nach(
        1,
        tb_runtime::host::Ereignis::Maus(tb_runtime::host::MausEreignis {
            art: tb_runtime::host::MausArt::Druck,
            taste: 1,
            shift: 0,
            zeile: 1,
            spalte: 1,
        }),
    );
    assert_eq!(vm.run_visible_forms(&mut host), RunEvent::Ended);
    assert!(matches!(vm.inspect("gesehen"), Some(Value::Int(1))));
    assert!(!vm.forms.has_visible_forms());
}

#[test]
fn fehlende_ereignisprozedur_verfaellt_und_typeof_prueft_klasse() {
    let mut vm = form_vm("Form1.Show\nIF TYPEOF Text1 IS TextBox THEN PRINT \"ja\"");
    vm.queue_form_event(tb_ui::forms::FormEvent {
        object: 0,
        array_index: None,
        name: "CLICK".into(),
        args: vec![],
    });
    assert_eq!(vm.run(&mut CaptureHost::default()), RunEvent::Ended);
    assert_eq!(tb_runtime::snapshot::text(&vm.rt.screen).trim(), "ja");
}

#[test]
fn mausargumente_kommen_in_zellen_an() {
    use tb_runtime::host::{Ereignis, MausArt, MausEreignis};
    let mut vm = form_vm(
        "DIM SHARED b%, s%, gx!, gy!\nForm1.Show\nSTOP\nDOEVENTS\nEND\n\
         SUB Form_MouseDown(Button AS INTEGER, Shift AS INTEGER, X AS SINGLE, Y AS SINGLE)\n\
         SHARED b%, s%, gx!, gy!\nb% = Button: s% = Shift: gx! = X: gy! = Y\nEND SUB",
    );
    let mut host = CaptureHost::default();
    assert!(matches!(vm.run(&mut host), RunEvent::Stopped { .. }));
    host.ereignis(Ereignis::Maus(MausEreignis {
        art: MausArt::Druck,
        taste: 1,
        shift: 2,
        zeile: 5,
        spalte: 7,
    }));
    assert_eq!(vm.run(&mut host), RunEvent::Ended);
    assert!(matches!(vm.inspect("b"), Some(Value::Int(1))));
    assert!(matches!(vm.inspect("s"), Some(Value::Int(2))));
    assert!(matches!(vm.inspect("gx"), Some(Value::Sng(7.0))));
    assert!(matches!(vm.inspect("gy"), Some(Value::Sng(5.0))));
}

#[test]
fn form_load_laeuft_vor_dem_sichtbarsetzen() {
    let mut vm = form_vm(
        "Form1.Show\nEND\n\
         SUB Form_Load()\nPRINT Form1.Visible\nEND SUB",
    );
    assert_eq!(vm.run(&mut CaptureHost::default()), RunEvent::Ended);
    assert_eq!(tb_runtime::snapshot::text(&vm.rt.screen).trim(), "0");
}

#[test]
fn steuerarray_ereignis_stellt_index_voran() {
    let mut vm = form_vm(
        "DIM SHARED seen%\nSTOP\nDOEVENTS\nEND\n\
         SUB Command1_Click(Index AS INTEGER)\nSHARED seen%\nseen% = Index\nEND SUB",
    );
    let mut host = CaptureHost::default();
    assert!(matches!(vm.run(&mut host), RunEvent::Stopped { .. }));
    vm.forms.load_array(2, 3).unwrap();
    vm.queue_form_event(tb_ui::forms::FormEvent {
        object: 2,
        array_index: Some(3),
        name: "CLICK".into(),
        args: vec![],
    });
    assert_eq!(vm.run(&mut host), RunEvent::Ended);
    assert!(matches!(vm.inspect("seen"), Some(Value::Int(3))));
    let active =
        tb_frontend::forms::property(tb_frontend::forms::ObjectClass::Screen, "ACTIVECONTROL")
            .unwrap()
            .0;
    assert_eq!(
        vm.forms.get(3, active).unwrap(),
        tb_ui::forms::PropertyValue::Object(Some((2, Some(3))))
    );
}

#[test]
fn form_load_laeuft_einmal_und_unload_cancel_verhindert_entladen() {
    let mut vm = form_vm(
        "DIM SHARED loads%\nx$ = Form1.Caption\nForm1.Hide\nx$ = Form1.Caption\n\
         Form1.Show\nUNLOAD Form1\nEND\n\
         SUB Form_Load()\nSHARED loads%\nloads% = loads% + 1\nEND SUB\n\
         SUB Form_Unload(Cancel AS INTEGER)\nCancel = 1\nEND SUB",
    );
    assert_eq!(vm.run(&mut CaptureHost::default()), RunEvent::Ended);
    assert!(matches!(vm.inspect("loads"), Some(Value::Int(1))));
    assert!(vm.forms.objects[0].loaded);
    assert!(vm.forms.objects[0].visible);
}

#[test]
fn objektcode_erzeugt_keine_zusaetzlichen_zustellopcodes() {
    let module = tb_vm::compile_source_with_forms(
        "FORM1",
        "Form1.Caption = \"x\"\nDOEVENTS\nx$ = Form1.Caption",
        &forms_catalog(),
    )
    .unwrap();
    let n = module.procs[0]
        .code
        .iter()
        .filter(|i| matches!(i, tb_vm::bytecode::Instr::Doevents))
        .count();
    assert_eq!(n, 1, "nur das explizite DOEVENTS ist ein Zustellopcode");
}

#[test]
fn listenmethoden_indexeigenschaft_und_picture_messung_laufen_in_der_vm() {
    use tb_frontend::forms::ObjectClass;
    let mut catalog = tb_frontend::forms::FormCatalog::default();
    catalog.add("Form1", ObjectClass::Form, None, false);
    catalog.add("List1", ObjectClass::ListBox, Some("Form1"), false);
    catalog.add("Picture1", ObjectClass::PictureBox, Some("Form1"), false);
    let module = tb_vm::compile_source_with_forms(
        "FORM1",
        "List1.Sorted = -1\nList1.ADDITEM \"b\"\nList1.ADDITEM \"a\"\nPRINT List1.List(0)\nPRINT List1.ListCount\nPRINT Picture1.TEXTWIDTH(\"abc\")",
        &catalog,
    )
    .unwrap_or_else(|diagnostics| panic!("Compile-Fehler: {diagnostics:?}"));
    let mut vm = Vm::new(module);
    assert_eq!(vm.run(&mut CaptureHost::default()), RunEvent::Ended);
    assert_eq!(
        tb_runtime::snapshot::text(&vm.rt.screen).trim(),
        "a\n 2 \n 3"
    );
}

#[test]
fn listenmethoden_und_indexeigenschaft_laufen_auf_control_arrays() {
    use tb_frontend::forms::ObjectClass;
    let mut catalog = tb_frontend::forms::FormCatalog::default();
    catalog.add("Form1", ObjectClass::Form, None, false);
    catalog.add("List1", ObjectClass::ListBox, Some("Form1"), true);
    let module = tb_vm::compile_source_with_forms(
        "FORM1",
        "LOAD List1(1)\nList1(1).ADDITEM \"x\"\nPRINT List1(1).List(0)",
        &catalog,
    )
    .unwrap_or_else(|diagnostics| panic!("Compile-Fehler: {diagnostics:?}"));
    let mut vm = Vm::new(module);
    assert_eq!(vm.run(&mut CaptureHost::default()), RunEvent::Ended);
    assert_eq!(tb_runtime::snapshot::text(&vm.rt.screen).trim(), "x");
}

#[test]
fn msgbox_und_inputbox_sind_keine_unsupported_opcodes_mehr() {
    use tb_runtime::host::{taste, Ereignis};
    let module = tb_vm::compile_source(
        "DIALOG",
        "r% = MSGBOX(\"Weiter?\", 4, \"Frage\")\ns$ = INPUTBOX$(\"Name\")\nPRINT r%; s$",
    )
    .unwrap_or_else(|diagnostics| panic!("Compile-Fehler: {diagnostics:?}"));
    assert!(!module.procs[0]
        .code
        .iter()
        .any(|instruction| matches!(instruction, tb_vm::bytecode::Instr::Unsupported(_))));
    let mut vm = Vm::new(module);
    let mut host = CaptureHost::default();
    for key in [taste::ENTER, "A", taste::ENTER] {
        host.ereignis(Ereignis::Taste(key.into(), 0));
    }
    assert_eq!(vm.run(&mut host), RunEvent::Ended);
    assert_eq!(tb_runtime::snapshot::text(&vm.rt.screen).trim(), "6 A");
}

#[test]
fn default_button_hat_vorrang_und_beendet_modales_formular() {
    use tb_frontend::forms::ObjectClass;
    use tb_runtime::host::{taste, Ereignis};
    let mut catalog = tb_frontend::forms::FormCatalog::default();
    catalog.add("Form1", ObjectClass::Form, None, false);
    catalog.add("Command1", ObjectClass::CommandButton, Some("Form1"), false);
    let module = tb_vm::compile_source_with_forms(
        "FORM1",
        "DIM SHARED gesehen%\nForm1.Show 1\nPRINT gesehen%\nEND\nSUB Command1_Click()\nSHARED gesehen%\ngesehen% = 1\nUNLOAD Form1\nEND SUB",
        &catalog,
    )
    .unwrap_or_else(|diagnostics| panic!("Compile-Fehler: {diagnostics:?}"));
    let mut vm = Vm::new(module);
    let default = tb_frontend::forms::property(ObjectClass::CommandButton, "DEFAULT")
        .unwrap()
        .0;
    vm.forms
        .set(1, default, tb_ui::forms::PropertyValue::Boolean(true))
        .unwrap();
    let mut host = CaptureHost::default();
    host.ereignis(Ereignis::Taste(taste::ENTER.into(), 0));
    assert_eq!(vm.run(&mut host), RunEvent::Ended);
    assert!(matches!(vm.inspect("gesehen"), Some(Value::Int(1))));
}

#[test]
fn printform_schreibt_das_textformular_in_den_druckerkanal() {
    use tb_frontend::forms::ObjectClass;
    let mut catalog = tb_frontend::forms::FormCatalog::default();
    catalog.add("Form1", ObjectClass::Form, None, false);
    let module = tb_vm::compile_source_with_forms("FORM1", "Form1.Show\nForm1.PRINTFORM", &catalog)
        .unwrap_or_else(|diagnostics| panic!("Compile-Fehler: {diagnostics:?}"));
    let mut vm = Vm::new(module);
    for (name, value) in [("WIDTH", 12), ("HEIGHT", 4)] {
        let property = tb_frontend::forms::property(ObjectClass::Form, name)
            .unwrap()
            .0;
        vm.forms
            .set_initial(0, property, tb_ui::forms::PropertyValue::Integer(value))
            .unwrap();
    }
    let caption = tb_frontend::forms::property(ObjectClass::Form, "CAPTION")
        .unwrap()
        .0;
    vm.forms
        .set_initial(
            0,
            caption,
            tb_ui::forms::PropertyValue::String("Druck".into()),
        )
        .unwrap();
    assert_eq!(vm.run(&mut CaptureHost::default()), RunEvent::Ended);
    assert!(vm.rt.print.drucker.contains("Druck"));
}

#[test]
fn klassischer_timer_trap_ruht_solange_das_menu_offen_ist() {
    use tb_frontend::forms::ObjectClass;
    use tb_runtime::host::umschalt;
    let mut catalog = tb_frontend::forms::FormCatalog::default();
    catalog.add("Form1", ObjectClass::Form, None, false);
    catalog.add("mnuDatei", ObjectClass::Menu, Some("Form1"), false);
    let module = tb_vm::compile_source_with_forms(
        "FORM1",
        "DIM SHARED n%\nON TIMER(1) GOSUB Tick\nTIMER ON\nForm1.Show\nSTOP\nDOEVENTS\nSTOP\nDOEVENTS\nPRINT n%\nEND\nTick:\nn% = n% + 1\nRETURN",
        &catalog,
    )
    .unwrap_or_else(|diagnostics| panic!("Compile-Fehler: {diagnostics:?}"));
    let mut vm = Vm::new(module);
    let caption = tb_frontend::forms::property(ObjectClass::Menu, "CAPTION")
        .unwrap()
        .0;
    vm.forms
        .set_initial(
            1,
            caption,
            tb_ui::forms::PropertyValue::String("&Datei".into()),
        )
        .unwrap();
    let mut host = CaptureHost::default();
    assert!(matches!(vm.run(&mut host), RunEvent::Stopped { .. }));
    assert!(vm.forms.handle_key("d", umschalt::ALT));
    host.uhr_vorruecken(1_500);
    assert!(matches!(vm.run(&mut host), RunEvent::Stopped { .. }));
    assert!(matches!(vm.inspect("n"), Some(Value::Int(0))));
    assert!(vm.forms.handle_key(tb_runtime::host::taste::ESC, 0));
    assert_eq!(vm.run(&mut host), RunEvent::Ended);
    assert!(matches!(vm.inspect("n"), Some(Value::Int(1))));
}

// ---- Laufzeit-Eingabe und Systemstatus ------------------------------------

#[test]
fn input_datei_bewahrt_felder_und_unterscheidet_leer_von_fehlend() {
    let tmp = TempVerzeichnis::neu("input_felder");
    let p = tmp.pfad("daten.txt");
    assert_eq!(out(&format!(
        "OPEN \"{p}\" FOR OUTPUT AS #1\nWRITE #1, \"a\", \"b\"\nWRITE #1, \"a,b\", 5\nWRITE #1, CHR$(34) + \"ä\" + CHR$(34), \"\"\nCLOSE #1\nOPEN \"{p}\" FOR INPUT AS #1\nINPUT #1, a$\nINPUT #1, b$, c$, n%, d$, e$\nPRINT a$; b$; c$; n%; d$; LEN(e$); EOF(1)\nCLOSE"
    )), "aba,b 5 \"ä\" 0 -1 \n");
    for (data, expected) in [("\"a\"\n", 62), ("a", 62), ("", 62), ("\"a", 62)] {
        std::fs::write(&p, data).unwrap();
        assert_eq!(
            err_code(&format!("OPEN \"{p}\" FOR INPUT AS #1\nINPUT #1, a$, b$")),
            expected
        );
    }
    std::fs::write(&p, "a,\r\n\r\n\"b\n,c\"\r\n").unwrap();
    assert_eq!(out(&format!("OPEN \"{p}\" FOR INPUT AS #1\nINPUT #1, a$, b$, c$, d$\nPRINT a$; LEN(b$); LEN(c$); LEN(d$); EOF(1)")), "a 0  0  4 -1 \n");
    std::fs::write(&p, "a,").unwrap();
    assert_eq!(out(&format!("OPEN \"{p}\" FOR INPUT AS #1\nINPUT #1, a$\nPRINT EOF(1);\nINPUT #1, b$\nPRINT LEN(b$); EOF(1)")), " 0  0 -1 \n");
}

#[test]
fn input_datei_alle_lesearten_teilen_utf8_cursor_und_seek() {
    let tmp = TempVerzeichnis::neu("input_cursor");
    let p = tmp.pfad("daten.txt");
    std::fs::write(&p, "ä😀,rest\r\n\"a,b\",Ende\r\nZ").unwrap();
    assert_eq!(out(&format!(
        "OPEN \"{p}\" FOR INPUT AS #1\nPRINT INPUT$(1, #1); INPUT$(1, #1); SEEK(1)\nLINE INPUT #1, s$\nPRINT s$\nINPUT #1, s$\nPRINT s$; INPUT$(2, #1)\nLINE INPUT #1, s$\nPRINT s$\nPRINT INPUT$(1, #1); EOF(1); LEN(INPUT$(0, #1))\nSEEK #1, 1\nINPUT #1, s$\nPRINT s$; SEEK(1)\nCLOSE"
    )), "ä😀 7 \n,rest\na,bEn\nde\nZ-1  0 \nä😀 8 \n");
    for mode in ["INPUT", "BINARY"] {
        std::fs::write(&p, "ä😀x").unwrap();
        assert_eq!(
            out(&format!(
                "OPEN \"{p}\" FOR {mode} AS #1\nPRINT INPUT$(2, #1); INPUT$(1, #1); EOF(1)"
            )),
            "ä😀x-1 \n"
        );
        assert_eq!(
            err_code(&format!(
                "OPEN \"{p}\" FOR {mode} AS #1\ns$ = INPUT$(4, #1)"
            )),
            62
        );
        for bad in [&[0xc3][..], &[0xff], &[0xe0, 0x80, 0x80], &[0xf0, 0x9f]] {
            std::fs::write(&p, bad).unwrap();
            assert_eq!(
                err_code(&format!(
                    "OPEN \"{p}\" FOR {mode} AS #1\ns$ = INPUT$(1, #1)"
                )),
                57
            );
        }
    }
    for input in ["INPUT #1, s$", "LINE INPUT #1, s$"] {
        assert_eq!(
            err_code(&format!("OPEN \"{p}\" FOR INPUT AS #1\n{input}")),
            57
        );
    }
    for mode in ["OUTPUT", "APPEND", "RANDOM"] {
        assert_eq!(
            err_code(&format!(
                "OPEN \"{p}\" FOR {mode} AS #1\ns$ = INPUT$(1, #1)"
            )),
            54
        );
    }
}

#[test]
fn input_s_teilt_sondertasten_und_bewahrt_rest_fuer_inkey() {
    use tb_runtime::host::Ereignis;
    for second in ["INPUT$(1)", "INKEY$"] {
        let src = format!("a$ = INPUT$(1)\nb$ = {second}\nc$ = INPUT$(2)\nPRINT LEN(a$); ASC(a$); LEN(b$); ASC(b$); c$; LEN(INKEY$)");
        let mut vm = Vm::new(tb_vm::compile_source("T", &src).unwrap());
        let mut host = CaptureHost::default();
        for text in ["\0;", "ä", "😀"] {
            host.ereignis(Ereignis::Taste(text.into(), 0));
        }
        assert_eq!(vm.run(&mut host), RunEvent::Ended);
        assert_eq!(
            tb_runtime::snapshot::text(&vm.rt.screen),
            " 1  0  1  59 ä😀 0 \n"
        );
    }
}

#[test]
fn alle_mk_cv_paare_verlangen_exakte_bytezeichenbreite() {
    for (mk, cv, n, breite) in [
        ("MKI$", "CVI", "-123", 2),
        ("MKL$", "CVL", "123456", 4),
        ("MKS$", "CVS", "1.5", 4),
        ("MKD$", "CVD", "-12.25", 8),
        ("MKC$", "CVC", "12.3456@", 8),
    ] {
        assert_eq!(out(&format!("PRINT {cv}({mk}({n})) = {n}")), "-1 \n");
        for arg in [
            format!("LEFT$({mk}({n}), {})", breite - 1),
            format!("{mk}({n}) + CHR$(0)"),
            format!("STRING$({breite}, \"😀\")"),
        ] {
            assert_eq!(err_code(&format!("PRINT {cv}({arg})")), 5, "{cv}({arg})");
        }
    }
}

#[test]
fn datei_und_elternpfadfehler_sind_am_sprachzugang_unterscheidbar() {
    let tmp = TempVerzeichnis::neu("pfadfehler");
    let leaf = tmp.pfad("fehlt.txt");
    let parent = tmp.pfad("fehlt/datei.txt");
    let source = tmp.pfad("quelle.txt");
    std::fs::write(&source, "erhalten").unwrap();
    for (path, code) in [(&leaf, 53), (&parent, 76)] {
        for stmt in [
            format!("OPEN \"{path}\" FOR INPUT AS #1"),
            format!("KILL \"{path}\""),
            format!("NAME \"{path}\" AS \"{source}\""),
        ] {
            assert_eq!(err_code(&stmt), code, "{stmt}");
        }
    }
    for stmt in [
        format!("MKDIR \"{parent}\""),
        format!("NAME \"{source}\" AS \"{parent}\""),
        format!("CHDIR \"{parent}\""),
        format!("RMDIR \"{parent}\""),
        format!("PRINT DIR$(\"{parent}\")"),
    ] {
        assert_eq!(err_code(&stmt), 76, "{stmt}");
    }
    for mode in ["OUTPUT", "APPEND", "BINARY", "RANDOM"] {
        assert_eq!(err_code(&format!("OPEN \"{parent}\" FOR {mode} AS #1")), 76);
    }
    assert_eq!(std::fs::read_to_string(&source).unwrap(), "erhalten");
    let sub = tmp.pfad("unter");
    let a = format!("{sub}\\a.txt");
    let b = format!("{sub}/b.txt");
    assert_eq!(out(&format!("MKDIR \"{sub}\"\nOPEN \"{a}\" FOR OUTPUT AS #1\nWRITE #1, \"ok\"\nCLOSE\nNAME \"{a}\" AS \"{b}\"\nPRINT DIR$(\"{sub}/*.txt\"); LEN(DIR$(\"\"))\nOPEN \"{b}\" FOR INPUT AS #1\nINPUT #1, s$\nPRINT s$\nCLOSE\nKILL \"{b}\"\nRMDIR \"{sub}\"")), "b.txt 0 \nok\n");
}

#[test]
fn timezoneknown_hat_integer_signatur_und_zeigt_aktuellen_status() {
    for (zone, expected) in [(Zeitzone::Fest(0), "-1 \n"), (Zeitzone::Unbekannt, " 0 \n")] {
        assert_eq!(out_mit_zone("PRINT TIMEZONEKNOWN", zone), expected);
    }
    let diagnostics = tb_vm::compile_source("T", "PRINT TIMEZONEKNOWN(1)").unwrap_err();
    assert!(
        diagnostics
            .iter()
            .any(|d| d.message.contains("TIMEZONEKNOWN")),
        "{diagnostics:?}"
    );
    let mut vm = Vm::new(
        tb_vm::compile_source(
            "T",
            "x = TIMEZONEKNOWN\nSTOP\ny = TIMEZONEKNOWN\nSTOP\nz = TIMEZONEKNOWN\nEND",
        )
        .unwrap(),
    );
    let mut host = CaptureHost::default();
    vm.rt.zonen_ermittler = |_| Some(7200);
    assert!(matches!(vm.run(&mut host), RunEvent::Stopped { .. }));
    assert!(matches!(vm.inspect("x"), Some(Value::Sng(-1.0))));
    vm.rt.zonen_ermittler = |_| None;
    assert!(matches!(vm.run(&mut host), RunEvent::Stopped { .. }));
    assert!(matches!(vm.inspect("y"), Some(Value::Sng(0.0))));
    vm.rt.zonen_ermittler = |_| Some(0);
    assert_eq!(vm.run(&mut host), RunEvent::Ended);
    assert!(matches!(vm.inspect("z"), Some(Value::Sng(-1.0))));
}

#[test]
fn unicode_puffergrenzen_und_binaere_records_bleiben_getrennt() {
    let tmp = TempVerzeichnis::neu("input_puffer");
    let p = tmp.pfad("daten.txt");
    for n in [8190, 8191, 8192] {
        std::fs::write(&p, format!("{}ä😀\r\nZ", "x".repeat(n))).unwrap();
        assert_eq!(out(&format!("OPEN \"{p}\" FOR INPUT AS #1\na$ = INPUT$({n}, #1)\nPRINT INPUT$(2, #1)\nLINE INPUT #1, b$\nINPUT #1, c$\nPRINT LEN(b$); c$; EOF(1)")), "ä😀\n 0 Z-1 \n");
    }
    let mut bytes = "ä😀".as_bytes().to_vec();
    bytes.extend_from_slice(&513i16.to_le_bytes());
    bytes.push(b'Z');
    std::fs::write(&p, bytes).unwrap();
    assert_eq!(out(&format!("OPEN \"{p}\" FOR BINARY AS #1\nPRINT INPUT$(2, #1);\nGET #1, , n%\nPRINT n%; INPUT$(1, #1); EOF(1)\nSEEK #1, 7\nn% = 1027\nPUT #1, , n%\nSEEK #1, 7\nGET #1, , n%\nPRINT n%; INPUT$(1, #1)")), "ä😀 513 Z-1 \n 1027 Z\n");
}

#[test]
fn sondertastenrest_wird_nicht_erneut_getrappt() {
    use tb_runtime::host::Ereignis;
    let src = "ON KEY(1) GOSUB H\nKEY(1) STOP\na$ = INPUT$(1)\nKEY(1) ON\nb$ = INPUT$(1)\nDOEVENTS\nPRINT ASC(a$); ASC(b$); n%\nEND\nH:\nn% = n% + 1\nRETURN";
    let mut vm = Vm::new(tb_vm::compile_source("T", src).unwrap());
    let mut host = CaptureHost::default();
    host.ereignis(Ereignis::Taste("\0;".into(), 0));
    assert_eq!(vm.run(&mut host), RunEvent::Ended);
    assert_eq!(tb_runtime::snapshot::text(&vm.rt.screen), " 0  59  1 \n");
}
