use tb_frontend::{
    forms::FormCatalog,
    source::{SourceSegment, SourceUnit},
};
use tb_runtime::{host::CaptureHost, value::Value};
use tb_ui::frm::{self, FormFile};
use tb_vm::{
    bytecode::CompiledModule,
    compile_project,
    interp::{RunEvent, Vm},
};

fn compile(units: &[SourceUnit], forms: &[FormFile]) -> CompiledModule {
    let mut catalog = FormCatalog::default();
    for form in forms {
        catalog.append(&form.catalog());
    }
    compile_project("APP", units, &catalog, forms).unwrap_or_else(|d| panic!("{d:#?}"))
}

#[test]
fn module_includes_erl_breakpoints_und_inspektion_behalten_ihren_ursprung() {
    let a = SourceUnit::new("MAIN", "main.bas", "x%=1\nCALL Fehler\nEND\n");
    let b = SourceUnit {
        name: "LIB".into(),
        segments: vec![
            SourceSegment {
                file: "lib.bas".into(),
                first_line: 1,
                text: "SUB Fehler\n".into(),
            },
            SourceSegment {
                file: "nested.bi".into(),
                first_line: 2,
                text: "200 ERROR 6\n".into(),
            },
            SourceSegment {
                file: "lib.bas".into(),
                first_line: 3,
                text: "END SUB\n".into(),
            },
        ],
    };
    let module = compile(&[a, b], &[]);
    let bytes = module.to_tbc();
    let loaded = CompiledModule::from_tbc(&bytes).unwrap();
    assert_eq!(loaded.to_tbc(), bytes);
    let mut vm = Vm::new(loaded);
    let mut host = CaptureHost::default();
    vm.add_module_breakpoint(1, 2);
    assert_eq!(vm.run(&mut host), RunEvent::Breakpoint { line: 2 });
    assert_eq!(vm.current_module(), 1);
    assert_eq!(vm.current_file(), "nested.bi");
    assert_eq!(vm.current_source_pos().column, 1);
    assert!(matches!(vm.inspect("MAIN!x"), Some(Value::Int(1))));
    vm.remove_module_breakpoint(1, 2);
    vm.set_step(true);
    assert_eq!(vm.run(&mut host), RunEvent::Stepped { line: 2 });
    assert_eq!(vm.current_file(), "nested.bi");
    assert_eq!(vm.current_source_pos().column, 5);
    vm.set_step(false);
    assert!(matches!(
        vm.run(&mut host),
        RunEvent::Error {
            code: 6,
            line: 2,
            ..
        }
    ));
    let mut erl_vm = Vm::new(compile(
        &[SourceUnit::new(
            "NUM",
            "num.bas",
            "ON ERROR RESUME NEXT\n200 ERROR 6\nn%=ERL\nSTOP",
        )],
        &[],
    ));
    assert!(matches!(erl_vm.run(&mut host), RunEvent::Stopped { .. }));
    assert!(matches!(erl_vm.inspect("n"), Some(Value::Int(200))));
    assert_eq!(vm.current_file(), "nested.bi");
}

#[test]
fn diagnose_im_include_und_echte_duplikate_bleiben_sichtbar() {
    let source = SourceUnit {
        name: "LIB".into(),
        segments: vec![SourceSegment {
            file: "nested.bi".into(),
            first_line: 2,
            text: "Text$ = 42\n".into(),
        }],
    };
    let errors = compile_project("APP", &[source], &FormCatalog::default(), &[]).unwrap_err();
    assert!(errors
        .iter()
        .any(|e| e.file.as_deref() == Some("nested.bi") && e.pos.line == 2 && e.pos.column == 1));
    for text in [
        "SUB X\nEND SUB\nSUB X\nEND SUB",
        "CONST N=1\nCONST N=2",
        "TYPE T\nx AS INTEGER\nEND TYPE\nTYPE T\nx AS INTEGER\nEND TYPE",
    ] {
        let errors = compile_project(
            "APP",
            &[SourceUnit::new("M", "m.bas", text)],
            &FormCatalog::default(),
            &[],
        )
        .unwrap_err();
        assert!(
            errors
                .iter()
                .any(|e| e.message.contains("Duplicate definition")),
            "{errors:?}"
        );
    }
}

#[test]
fn initialisierungen_def_fn_und_modulhandler_haben_eigene_quellorte() {
    for (source, expected_line) in [
        ("' Bibliothek\nDIM a%(1 TO 0)", 2),
        (
            "' Bibliothek\nDEF FNkaputt(x)=1/x\nSUB Fehler\nPRINT FNkaputt(0)\nEND SUB",
            2,
        ),
    ] {
        let module = compile(
            &[
                SourceUnit::new("A", "a.bas", "CALL Fehler\nEND"),
                SourceUnit::new(
                    "B",
                    "b.bas",
                    &format!(
                        "{source}\n{}",
                        if source.contains("SUB") {
                            ""
                        } else {
                            "SUB Fehler\nEND SUB"
                        }
                    ),
                ),
            ],
            &[],
        );
        let mut vm = Vm::new(CompiledModule::from_tbc(&module.to_tbc()).unwrap());
        let event = vm.run(&mut CaptureHost::default());
        assert!(
            matches!(event, RunEvent::Error { line, .. } if line == expected_line),
            "{event:?}"
        );
        assert_eq!(vm.current_file(), "b.bas");
    }
    let units = [
        SourceUnit::new(
            "A",
            "a.bas",
            "ON ERROR GOTO H\nCALL Fehler\nEND\nH:\nPRINT ERR;ERL\nEND",
        ),
        SourceUnit::new(
            "B",
            "b.bas",
            "SUB Fehler\nON ERROR GOTO 0\n200 ERROR 6\nEND SUB",
        ),
    ];
    let mut vm = Vm::new(compile(&units, &[]));
    assert_eq!(vm.run(&mut CaptureHost::default()), RunEvent::Ended);
    assert_eq!(tb_runtime::snapshot::text(&vm.rt.screen), " 6  200 \n");
}

#[test]
fn lokale_prozeduren_shared_common_und_wiederholte_includes_bleiben_getrennt() {
    let a = SourceUnit::new("A", "a.bas", "CONST N=1\nDIM SHARED x%\nCOMMON SHARED c%\nx%=10\nCALL Privat\nCALL Zweites\nPRINT x%;c%\nEND\nSUB Privat\nx%=x%+N\nc%=c%+1\nEND SUB");
    let b = SourceUnit::new("B", "b.bas", "CONST N=2\nDIM SHARED x%\nCOMMON SHARED c%\nSUB Zweites\nx%=20\nCALL Privat\nPRINT x%\nEND SUB\nSUB Privat\nx%=x%+N\nc%=c%+2\nEND SUB");
    let mut vm = Vm::new(compile(&[a, b], &[]));
    assert_eq!(vm.run(&mut CaptureHost::default()), RunEvent::Ended);
    assert_eq!(tb_runtime::snapshot::text(&vm.rt.screen), " 22 \n 11  3 \n");
    let include = SourceSegment {
        file: "shared.bi".into(),
        first_line: 1,
        text: "CONST N=7\n".into(),
    };
    let units: Vec<_> = ["A", "B"]
        .iter()
        .map(|name| SourceUnit {
            name: (*name).into(),
            segments: vec![
                include.clone(),
                SourceSegment {
                    file: format!("{name}.bas"),
                    first_line: 2,
                    text: "PRINT N\n".into(),
                },
            ],
        })
        .collect();
    let mut vm = Vm::new(compile(&units, &[]));
    assert_eq!(vm.run(&mut CaptureHost::default()), RunEvent::Ended);
    assert_eq!(tb_runtime::snapshot::text(&vm.rt.screen), " 7 \n 7 \n");
}

#[test]
fn typreferenzen_signaturen_und_option_base_ueber_modulgrenzen() {
    use tb_frontend::hir::{HProcKind, HTy, NumTy};
    use tb_vm::bytecode::Instr;
    let units = [
        SourceUnit::new("A", "a.bas", "DECLARE SUB CmnDlgRegister(ok AS INTEGER)\nTYPE Klein\nx AS INTEGER\nEND TYPE\nDIM s AS Gross\nDIM a%(2)\ns.x=4\na%(1)=3\nCmnDlgRegister ok%\nPRINT ok%\nPRINT Summe%(a%(), s, 2)\nPRINT s.x\nCALL Basis\nEND"),
        SourceUnit::new("B", "b.bas", "OPTION BASE 1\nTYPE Gross\nx AS LONG\ny AS STRING * 8\nEND TYPE\nFUNCTION Summe%(a%(), s AS Gross, n%)\nSumme%=a%(1)+s.x+n%\ns.x=s.x+1\nEND FUNCTION\nSUB Basis\nb%(1)=1\nPRINT LBOUND(b%)\nEND SUB\nSUB Datei\nOPEN \"unbenutzt.isam\" FOR ISAM Gross \"T\" AS #1\nCLOSE #1\nEND SUB"),
    ];
    let module = compile(&units, &[]);
    let bytes = module.to_tbc();
    let loaded = CompiledModule::from_tbc(&bytes).unwrap();
    assert_eq!(bytes, loaded.to_tbc());
    let signature = loaded.procs.iter().find(|p| p.name == "B!SUMME").unwrap();
    assert_eq!(signature.kind, HProcKind::Function);
    assert_eq!(signature.ret_ty, Some(HTy::Num(NumTy::Int)));
    assert!(signature.params[0].array && !signature.params[0].by_ref);
    assert!(signature.params[2].by_ref);
    assert!(signature.params[1].by_ref);
    let HTy::Udt(udt) = signature.params[1].ty else {
        panic!("UDT-Parameter fehlt")
    };
    assert_eq!(loaded.udts[udt as usize].name, "GROSS");
    let file = loaded.procs.iter().find(|p| p.name == "B!DATEI").unwrap();
    assert!(file.code.contains(&Instr::PushUdtId(udt)));
    let mut vm = Vm::new(loaded);
    assert_eq!(vm.run(&mut CaptureHost::default()), RunEvent::Ended);
    assert_eq!(
        tb_runtime::snapshot::text(&vm.rt.screen),
        "-1 \n 9 \n 5 \n 1 \n"
    );
}

#[test]
fn beschaedigte_container_werden_vor_der_ausfuehrung_abgewiesen() {
    use tb_vm::bytecode::Instr;
    let form = frm::read_text("f.frm", "VERSION 1.00\nBEGIN Form F\n BEGIN TextBox Text1\n  Index = 2\n  Text = \"hello\"\n END\nEND\n").unwrap();
    let bytes = compile(&[SourceUnit::new("F", "f.frm", "END")], &[form]).to_tbc();
    for length in 0..bytes.len() {
        assert!(
            CompiledModule::from_tbc(&bytes[..length]).is_err(),
            "Länge {length}"
        );
    }
    for (offset, value) in [(6, 1u32), (8, u32::MAX), (16, 0), (20, u32::MAX)] {
        let mut bad = bytes.clone();
        bad[offset..offset + 4].copy_from_slice(&value.to_le_bytes());
        assert!(CompiledModule::from_tbc(&bad).is_err(), "Offset {offset}");
    }
    let mut duplicate = bytes.clone();
    duplicate[24..28].copy_from_slice(b"MODN");
    assert!(CompiledModule::from_tbc(&duplicate).is_err());
    for mutation in 0..10 {
        let mut bad = CompiledModule::from_tbc(&bytes).unwrap();
        match mutation {
            0 => bad.sources[0].module = u16::MAX,
            1 => bad.procs[0].module = u16::MAX,
            2 => bad.procs[0].code.push(Instr::Call(u16::MAX, 0)),
            3 => bad.procs[0].n_params = 1,
            4 => bad.objects[1].parent = Some(1),
            5 => bad.form_initial[1].index = 3,
            6 => {
                bad.form_initial[1].properties.insert(
                    0,
                    tb_ui::forms::PropertyValue::Object(Some((u16::MAX, None))),
                );
            }
            7 => bad.procs[0].code.push(Instr::Source(u32::MAX, 1)),
            8 | 9 => {
                let initial = &mut bad.form_initial[1];
                let parent = tb_frontend::forms::property(
                    tb_frontend::forms::ObjectClass::TextBox,
                    "PARENT",
                )
                .unwrap()
                .0;
                initial.properties.insert(
                    parent,
                    tb_ui::forms::PropertyValue::Object(Some(if mutation == 8 {
                        (initial.object, Some(initial.index))
                    } else {
                        (0, Some(99))
                    })),
                );
            }
            _ => unreachable!(),
        }
        assert!(
            CompiledModule::from_tbc(&bad.to_tbc()).is_err(),
            "Mutation {mutation}"
        );
    }
}

#[test]
fn zwei_formulare_mit_gleichen_controls_und_arrays_laufen_aus_dem_kompilat() {
    let form = |name: &str, text: &str| {
        frm::read_text(&format!("{name}.frm"), &format!(
        "VERSION 1.00\nBEGIN Form {name}\n Width = 30\n Height = 10\n BEGIN Frame Frame1\n  BEGIN TextBox Text1\n   Text = \"{text}\"\n  END\n END\n BEGIN TextBox Feld\n  Index = 0\n  Text = \"null\"\n END\n BEGIN TextBox Feld\n  Index = 2\n  Text = \"zwei\"\n END\nEND\n\nSUB Form_Load\nText1.Text = Text1.Text + \"!\"\nEND SUB\n"
    )).unwrap()
    };
    let forms = [form("Form1", "a"), form("Form2", "b")];
    let mut units = vec![SourceUnit::new("MAIN", "main.bas", "Form2.Show\na$=Form1!Text1.Text\nb$=Form2!Text1.Text\nc$=Form2!Feld(2).Text\nForm1.Hide\nForm2.Hide\nCLS\nPRINT a$\nPRINT b$\nPRINT c$\nEND")];
    units.extend(
        forms
            .iter()
            .map(|f| SourceUnit::new(&f.root.name, &format!("{}.frm", f.root.name), &f.code)),
    );
    let module = compile(&units, &forms);
    assert_eq!(module.event_procs.len(), 2);
    let bytes = module.to_tbc();
    let loaded = CompiledModule::from_tbc(&bytes).unwrap();
    assert_eq!(loaded.to_tbc(), bytes);
    let mut vm = Vm::new(loaded);
    assert_eq!(vm.run(&mut CaptureHost::default()), RunEvent::Ended);
    assert_eq!(tb_runtime::snapshot::text(&vm.rt.screen), "a!\nb!\nzwei\n");
}
