//! Unveränderte ursprüngliche Reviewproben und ergänzende Regressionen des Projektkompilats.
use tb_frontend::{
    forms::FormCatalog,
    source::{SourceSegment, SourceUnit},
};
use tb_runtime::{host::CaptureHost, value::Value};
use tb_vm::{
    bytecode::CompiledModule,
    compile_project,
    interp::{RunEvent, Vm},
};

fn units(files: &[(&str, &str)]) -> Vec<SourceUnit> {
    files
        .iter()
        .map(|(name, code)| SourceUnit::new(name, &format!("{name}.bas"), code))
        .collect()
}
fn compile(files: &[(&str, &str)]) -> Result<CompiledModule, Vec<tb_frontend::Diagnostic>> {
    compile_project("APP", &units(files), &FormCatalog::default(), &[])
}
fn run(module: CompiledModule) -> (RunEvent, String) {
    let bytes = module.to_tbc();
    let loaded = CompiledModule::from_tbc(&bytes).unwrap();
    assert_eq!(bytes, loaded.to_tbc());
    let mut vm = Vm::new(loaded);
    let event = vm.run(&mut CaptureHost::default());
    (event, tb_runtime::snapshot::text(&vm.rt.screen))
}

#[test]
fn v1_zweige_und_schleifen_behalten_eigene_quellorte() {
    let mut failures = vec![];
    for (name, code, line) in [
        (
            "ELSEIF",
            "IF 0 THEN\nPRINT 1\nELSEIF 1/0 THEN\nPRINT 2\nEND IF",
            3,
        ),
        ("CASE", "SELECT CASE 1\nCASE 1/0\nPRINT 2\nEND SELECT", 2),
        ("NEXT", "FOR i%=32767 TO 32767\nPRINT 1\nNEXT", 3),
        ("LOOP", "DO\nPRINT 1\nLOOP UNTIL 1/0", 3),
    ] {
        let (event, _) = run(compile(&[("MAIN", code)]).unwrap());
        if !matches!(event, RunEvent::Error { line: found, .. } if found == line) {
            failures.push(format!("{name}: erwartet Zeile {line}, erhalten {event:?}"));
        }
    }
    let source = SourceUnit {
        name: "MAIN".into(),
        segments: vec![
            SourceSegment {
                file: "main.bas".into(),
                first_line: 1,
                text: "IF 0 THEN\nPRINT 1\n".into(),
            },
            SourceSegment {
                file: "cond.bi".into(),
                first_line: 1,
                text: "ELSEIF 1/0 THEN\nPRINT 2\n".into(),
            },
            SourceSegment {
                file: "main.bas".into(),
                first_line: 4,
                text: "END IF\n".into(),
            },
        ],
    };
    let mut vm = Vm::new(compile_project("APP", &[source], &FormCatalog::default(), &[]).unwrap());
    let event = vm.run(&mut CaptureHost::default());
    if vm.current_file() != "cond.bi" {
        failures.push(format!(
            "Include: {event:?} aus {} statt cond.bi",
            vm.current_file()
        ));
    }
    // Das gleiche Problem darf nicht nur durch Umbenennen der Fehlermeldung behoben werden.
    let mut vm = Vm::new(
        compile(&[("MAIN", "IF 0 THEN\nPRINT 1\nELSEIF 1 THEN\nPRINT 2\nEND IF")]).unwrap(),
    );
    vm.add_module_breakpoint(0, 3);
    let event = vm.run(&mut CaptureHost::default());
    if event != (RunEvent::Breakpoint { line: 3 }) {
        failures.push(format!("Breakpoint ELSEIF: {event:?}"));
    }
    assert!(failures.is_empty(), "{}", failures.join("\n"));
}

#[test]
fn v2_importierte_deklarationen_behalten_kontext_und_abhaengigkeiten() {
    let cases = [
        (
            "DEFINT",
            "PRINT Doppelt(3)\nEND",
            "DEFINT A-Z\nFUNCTION Doppelt(x)\nDoppelt=x*2\nEND FUNCTION",
            " 6 \n",
        ),
        (
            "CONST",
            "CONST A=3\nPRINT B\nEND",
            "CONST A=3\nCONST B=A+1",
            " 4 \n",
        ),
        (
            "TYPE",
            "TYPE Inner\nx AS INTEGER\nEND TYPE\nDIM a AS Outer\na.i.x=3\nPRINT a.i.x\nEND",
            "TYPE Inner\nx AS INTEGER\nEND TYPE\nTYPE Outer\ni AS Inner\nEND TYPE",
            " 3 \n",
        ),
    ];
    let mut failures = vec![];
    for (name, main, lib, output) in cases {
        match compile(&[("MAIN", main), ("LIB", lib)]) {
            Err(e) => failures.push(format!("{name}: {e:?}")),
            Ok(module) => {
                let actual = run(module);
                if actual != (RunEvent::Ended, output.into()) {
                    failures.push(format!("{name}: {actual:?}"));
                }
            }
        }
    }
    assert!(failures.is_empty(), "{}", failures.join("\n"));
}

#[test]
fn v3_form_metabefehle_erzeugen_getrennte_formularobjekte() {
    let module = compile(&[
        (
            "A",
            "'$FORM\nCaption=\"a\"\nCALL SetupB\nPRINT Caption\nEND",
        ),
        ("B", "'$FORM\nSUB SetupB\nCaption=\"b\"\nEND SUB"),
    ])
    .unwrap();
    let names: Vec<_> = module.objects.iter().map(|o| o.name.clone()).collect();
    let actual = run(module);
    assert_eq!(
        actual,
        (RunEvent::Ended, "a\n".into()),
        "Objekte: {names:?}"
    );
}

#[test]
fn v4_common_behaelt_typidentitaet_und_initialisiert_arrays_einmal() {
    let mut failures = vec![];
    for (name, files, want) in [
        (
            "Suffixe",
            vec![(
                "MAIN",
                "COMMON SHARED c%, c$\nc%=7\nc$=\"ok\"\nPRINT c%;c$\nEND",
            )],
            " 7 ok\n",
        ),
        (
            "Array",
            vec![
                ("MAIN", "COMMON SHARED a%(2)\na%(1)=7\nCALL F\nEND"),
                ("LIB", "COMMON SHARED a%(2)\nSUB F\nPRINT a%(1)\nEND SUB"),
            ],
            " 7 \n",
        ),
    ] {
        match compile(&files) {
            Err(e) => failures.push(format!("{name}: {e:?}")),
            Ok(module) => {
                let actual = run(module);
                if actual != (RunEvent::Ended, want.into()) {
                    failures.push(format!("{name}: {actual:?}"));
                }
            }
        }
    }
    assert!(failures.is_empty(), "{}", failures.join("\n"));
}

#[test]
fn v5_load_und_unload_loesen_den_expliziten_container_auf() {
    use tb_frontend::forms::ObjectClass;
    let mut catalog = FormCatalog::default();
    for name in ["Form1", "Form2"] {
        catalog.add(name, ObjectClass::Form, None, false);
        catalog.add("Text1", ObjectClass::TextBox, Some(name), true);
    }
    let source = units(&[("MAIN", "LOAD Form2!Text1(3)\nForm2!Text1(3).Text=\"b\"\nPRINT Form2!Text1(3).Text\nUNLOAD Form2!Text1(3)\nEND")]);
    let module = compile_project("APP", &source, &catalog, &[]).unwrap();
    assert_eq!(run(module), (RunEvent::Ended, "b\n".into()));
}

#[test]
fn v6_linkerdiagnose_nennt_den_urspruenglichen_dateiort() {
    let errors = compile(&[
        ("MAIN", "DECLARE SUB F(x%)\nCALL F(1)\nEND"),
        ("LIB", "SUB F(x$)\nEND SUB"),
    ])
    .unwrap_err();
    assert!(
        errors
            .iter()
            .any(|d| d.message.contains("Parameter type mismatch")),
        "{errors:?}"
    );
    assert!(
        errors
            .iter()
            .all(|d| d.file.is_some() && d.pos.line > 0 && d.pos.column > 0),
        "{errors:?}"
    );
}

#[test]
fn modulbreakpoint_rundlauf_inspektion_und_numerische_erl_bleiben_getrennt() {
    let main = format!(
        "DIM a%(2)\nTYPE T\nx AS INTEGER\nEND TYPE\nDIM r AS T\na%(1)=7\nr.x=8\n{}CALL F\nEND",
        "\n\nn%=1\n"
    );
    let lib = format!("SUB F\n{}200 ERROR 6\nEND SUB", "\n".repeat(8));
    let module = compile(&[("MAIN", &main), ("LIB", &lib)]).unwrap();
    let bytes = module.to_tbc();
    let loaded = CompiledModule::from_tbc(&bytes).unwrap();
    assert_eq!(bytes, loaded.to_tbc());
    let mut vm = Vm::new(loaded);
    vm.add_module_breakpoint(1, 10);
    let mut host = CaptureHost::default();
    assert_eq!(vm.run(&mut host), RunEvent::Breakpoint { line: 10 });
    assert_eq!(vm.current_module(), 1);
    assert_eq!(vm.current_file(), "LIB.bas");
    assert!(matches!(
        vm.inspect_element("MAIN!a%", &[1]),
        Some(Value::Int(7))
    ));
    assert!(matches!(
        vm.inspect_field("MAIN!r", &[0]),
        Some(Value::Int(8))
    ));
    vm.remove_module_breakpoint(1, 10);
    vm.set_step(true);
    assert_eq!(vm.run(&mut host), RunEvent::Stepped { line: 10 });
    assert_eq!(vm.current_source_pos().column, 5);
    vm.set_step(false);
    assert!(matches!(
        vm.run(&mut host),
        RunEvent::Error {
            code: 6,
            line: 10,
            ..
        }
    ));
    let source = format!("{}ERROR 6", "\n".repeat(41));
    assert!(matches!(
        run(compile(&[("MAIN", &source)]).unwrap()).0,
        RunEvent::Error {
            code: 6,
            line: 42,
            ..
        }
    ));
    let module = compile(&[(
        "MAIN",
        "ON ERROR RESUME NEXT\n200 ERROR 6\nPRINT ERR;ERL\nEND",
    )])
    .unwrap();
    assert_eq!(run(module), (RunEvent::Ended, " 6  200 \n".into()));
}

#[test]
fn importkontext_bleibt_auch_bei_abweichenden_lokalen_definitionen_erhalten() {
    for (files, expected) in [
        (vec![("MAIN", "CONST A=99\nPRINT B\nEND"), ("LIB", "CONST A=3\nCONST B=A+1")], " 4 \n"),
        (vec![("MAIN", "PRINT C\nEND"), ("LIB", "CONST C=B+1"), ("BASE", "CONST B=3")], " 4 \n"),
        (vec![("MAIN", "TYPE Inner\nx AS STRING * 3\nEND TYPE\nDIM a AS Outer\na.i.x=3\nPRINT a.i.x\nEND"), ("LIB", "TYPE Inner\nx AS INTEGER\nEND TYPE\nTYPE Outer\ni AS Inner\nEND TYPE")], " 3 \n"),
        (vec![("MAIN", "DEFSTR A-Z\nx%=3\nCALL Twice(x%)\nPRINT x%\nPRINT Doppelt(3)\nEND"), ("LIB", "DEFINT A-Z\nSUB Twice(x)\nx=x*2\nEND SUB\nFUNCTION Doppelt(x)\nDoppelt=x*2\nEND FUNCTION")], " 6 \n 6 \n"),
    ] {
        let actual = run(compile(&files).unwrap_or_else(|e| panic!("{files:?}: {e:?}")));
        assert_eq!(actual, (RunEvent::Ended, expected.into()), "{files:?}");
    }
}

#[test]
fn common_vertraege_und_linkfehler_haben_echte_quellorte() {
    for (left, right) in [
        ("COMMON SHARED a%(2)", "COMMON SHARED a%(3)"),
        ("COMMON SHARED a%(1 TO 2)", "COMMON SHARED a%(0 TO 2)"),
        ("COMMON SHARED a%(2)", "COMMON SHARED a%(2,2)"),
        ("COMMON SHARED a AS INTEGER", "COMMON SHARED a AS STRING"),
        ("COMMON SHARED a%(2)", "COMMON SHARED a%"),
    ] {
        let units = [
            SourceUnit::new("MAIN", "main.bas", &format!("{left}\nEND")),
            SourceUnit {
                name: "LIB".into(),
                segments: vec![SourceSegment {
                    file: "shared.bi".into(),
                    first_line: 42,
                    text: format!("  {right}\n"),
                }],
            },
        ];
        let errors = compile_project("APP", &units, &FormCatalog::default(), &[]).unwrap_err();
        assert!(
            errors.iter().any(|e| e.file.as_deref() == Some("shared.bi")
                && e.pos.line == 42
                && e.pos.column > 1
                && e.message.contains("COMMON")),
            "{errors:?}"
        );
    }
    let files = [
        (
            "MAIN",
            "COMMON SHARED a%(2),a$(2)\na%(1)=7\na$(1)=\"ok\"\nCALL F\nEND",
        ),
        (
            "LIB",
            "COMMON SHARED a%(2),a$(2)\nSUB F\nPRINT a%(1);a$(1)\nEND SUB",
        ),
    ];
    assert_eq!(
        run(compile(&files).unwrap()),
        (RunEvent::Ended, " 7 ok\n".into())
    );
    let errors =
        compile(&[("MAIN", "' header\nDECLARE SUB F(x$)\nSUB F(x%)\nEND SUB")]).unwrap_err();
    assert!(
        errors.iter().any(|e| e.pos.line == 3
            && e.file.as_deref() == Some("MAIN.bas")
            && e.message.contains("Parameter")),
        "{errors:?}"
    );
}

#[test]
fn schleifengrenzen_tragen_breakpoint_include_und_resume() {
    for (source, line) in [
        ("FOR i%=1 TO 2\nPRINT i%\nNEXT\nEND", 3),
        ("DO\nPRINT 1\nLOOP UNTIL 1\nEND", 3),
        ("i%=1\nWHILE i%\ni%=0\nWEND\nEND", 4),
        (
            "SELECT CASE 2\nCASE 1\nPRINT 1\nCASE 2\nPRINT 2\nEND SELECT",
            4,
        ),
    ] {
        let module = compile(&[("MAIN", source)]).unwrap();
        let mut vm = Vm::new(CompiledModule::from_tbc(&module.to_tbc()).unwrap());
        vm.add_module_breakpoint(0, line);
        assert_eq!(
            vm.run(&mut CaptureHost::default()),
            RunEvent::Breakpoint { line }
        );
        assert_eq!(vm.current_file(), "MAIN.bas");
        assert_eq!(vm.current_source_pos().column, 1);
        vm.remove_module_breakpoint(0, line);
        assert_eq!(vm.run(&mut CaptureHost::default()), RunEvent::Ended);
    }
    let actual = run(compile(&[("MAIN", "x%=1\nWHILE 1/x%\nx%=0\nWEND")]).unwrap());
    assert!(
        matches!(
            actual.0,
            RunEvent::Error {
                code: 11,
                line: 2,
                ..
            }
        ),
        "{actual:?}"
    );
    let actual = run(compile(&[(
        "MAIN",
        "ON ERROR GOTO H\nDO\nn%=n%+1\nLOOP UNTIL 1/x%\nPRINT n%\nEND\nH:\nx%=1\nRESUME",
    )])
    .unwrap());
    assert_eq!(actual, (RunEvent::Ended, " 1 \n".into()));
    let actual = run(compile(&[(
        "MAIN",
        "ON ERROR RESUME NEXT\nFOR i%=32767 TO 32767\nNEXT\nPRINT ERR\nEND",
    )])
    .unwrap());
    assert_eq!(actual, (RunEvent::Ended, " 6 \n".into()));
    let unit = SourceUnit {
        name: "MAIN".into(),
        segments: vec![
            SourceSegment {
                file: "main.bas".into(),
                first_line: 1,
                text: "DO\nPRINT 1\n".into(),
            },
            SourceSegment {
                file: "end.bi".into(),
                first_line: 42,
                text: "  LOOP UNTIL 1/0\n".into(),
            },
        ],
    };
    let module = compile_project("APP", &[unit], &FormCatalog::default(), &[]).unwrap();
    let mut vm = Vm::new(CompiledModule::from_tbc(&module.to_tbc()).unwrap());
    assert!(matches!(
        vm.run(&mut CaptureHost::default()),
        RunEvent::Error {
            code: 11,
            line: 42,
            ..
        }
    ));
    assert_eq!(vm.current_file(), "end.bi");
    assert_eq!(vm.current_source_pos().column, 3);
}

#[test]
fn debugger_unterscheidet_typisierte_scalar_und_arraynamen() {
    let mut vm = Vm::new(
        compile(&[(
            "MAIN",
            "COMMON SHARED c%,c$,a%(2),a$(2)\nc%=7\nc$=\"ok\"\na%(1)=8\na$(1)=\"array\"\nSTOP",
        )])
        .unwrap(),
    );
    assert!(matches!(
        vm.run(&mut CaptureHost::default()),
        RunEvent::Stopped { .. }
    ));
    assert!(matches!(vm.inspect("c%"), Some(Value::Int(7))));
    assert!(matches!(vm.inspect("c$"),Some(Value::Str(s)) if s.as_ref()=="ok"));
    assert!(matches!(
        vm.inspect_element("a%", &[1]),
        Some(Value::Int(8))
    ));
    assert!(matches!(vm.inspect_element("a$",&[1]),Some(Value::Str(s)) if s.as_ref()=="array"));
}

#[test]
fn common_arrays_behalten_dynamische_grenzen_und_kompatible_offene_deklarationen() {
    for (left, right) in [
        ("COMMON SHARED a%(2)", "COMMON SHARED a%()"),
        ("COMMON SHARED a%(2.4)", "COMMON SHARED a%(2)"),
        ("COMMON SHARED a%(n%+2)", "COMMON SHARED a%(n%+2)"),
    ] {
        let files = [
            ("MAIN", format!("{left}\na%(1)=7\nCALL F\nEND")),
            ("LIB", format!("{right}\nSUB F\nPRINT a%(1)\nEND SUB")),
        ];
        let files: Vec<_> = files.iter().map(|(n, s)| (*n, s.as_str())).collect();
        assert_eq!(
            run(compile(&files).unwrap()),
            (RunEvent::Ended, " 7 \n".into()),
            "{files:?}"
        );
    }
    let module = compile(&[
        ("MAIN", "COMMON SHARED a%(n%+2)\nEND"),
        ("LIB", "COMMON SHARED a%(n%+3)"),
    ])
    .unwrap();
    let mut vm = Vm::new(CompiledModule::from_tbc(&module.to_tbc()).unwrap());
    assert!(matches!(
        vm.run(&mut CaptureHost::default()),
        RunEvent::Error {
            code: 13,
            line: 1,
            ..
        }
    ));
    assert_eq!(vm.current_file(), "LIB.bas");
}

#[test]
fn prozedurlokaler_deftype_veraendert_keine_nachfolgende_signatur() {
    let files=[("MAIN","PRINT F(3)\nEND"),("LIB","DEFINT A-Z\nSUB Setup\nDEFSTR A-Z\nx=\"local\"\nEND SUB\nFUNCTION F(x)\nF=x*2\nEND FUNCTION")];
    assert_eq!(
        run(compile(&files).unwrap()),
        (RunEvent::Ended, " 6 \n".into())
    );
}

#[test]
fn gemischte_frm_und_form_module_haben_getrennte_ereignisbindungen() {
    let form=tb_ui::frm::read_text("A.frm","VERSION 1.00\nBEGIN Form A\n Caption = \"design\"\nEND\nSUB Form_Load\nCaption=\"a\"\nEND SUB").unwrap();
    let sources = units(&[
        (
            "MAIN",
            "B.Show\na$=A.Caption\nb$=B.Caption\nA.Hide\nB.Hide\nCLS\nPRINT a$;b$\nEND",
        ),
        ("A", &form.code),
        ("B", "'$FORM\nSUB Form_Load\nCaption=\"b\"\nEND SUB"),
    ]);
    let module = compile_project("APP", &sources, &form.catalog(), &[form]).unwrap();
    assert_eq!(module.event_procs.len(), 2);
    assert_ne!(module.event_procs[0].object, module.event_procs[1].object);
    assert_eq!(run(module), (RunEvent::Ended, "ab\n".into()));
}

#[test]
fn diagnostik_bei_literalen_im_include_hat_die_exakte_spalte() {
    let unit = SourceUnit {
        name: "MAIN".into(),
        segments: vec![
            SourceSegment {
                file: "main.bas".into(),
                first_line: 1,
                text: "DECLARE SUB F(x%)\n".into(),
            },
            SourceSegment {
                file: "args.bi".into(),
                first_line: 42,
                text: "  CALL F(\"wrong\")\n".into(),
            },
        ],
    };
    let errors = compile_project("APP", &[unit], &FormCatalog::default(), &[]).unwrap_err();
    assert!(
        errors.iter().any(|e| e.file.as_deref() == Some("args.bi")
            && e.pos.line == 42
            && e.pos.column == 10
            && e.message.contains("Parameter type mismatch")),
        "{errors:?}"
    );
}

#[test]
fn spaetere_common_konflikte_und_doppelte_module_werden_am_ursprung_gemeldet() {
    let errors = compile(&[
        ("MAIN", "COMMON a%()\nEND"),
        ("LIB", "COMMON a%(2)"),
        ("THIRD", "' head\nCOMMON a%(3)"),
    ])
    .unwrap_err();
    assert!(
        errors.iter().any(|e| e.file.as_deref() == Some("THIRD.bas")
            && e.pos.line == 2
            && e.message.contains("COMMON")),
        "{errors:?}"
    );
    let units = [
        SourceUnit::new("SAME", "first.bas", "END"),
        SourceUnit::new("SAME", "second.bas", "' head\n  END"),
    ];
    let errors = compile_project("APP", &units, &FormCatalog::default(), &[]).unwrap_err();
    assert!(
        errors
            .iter()
            .any(|e| e.file.as_deref() == Some("second.bas")
                && e.pos.line == 2
                && e.pos.column == 3
                && e.message.contains("Duplicate")),
        "{errors:?}"
    );
}
