use tb_frontend::{forms::FormCatalog, source::SourceUnit};
use tb_runtime::host::CaptureHost;
use tb_vm::{
    bytecode::CompiledModule,
    interp::{RunEvent, Vm},
    library::Library,
    project::ProjectCompiler,
};
fn unit(name: &str, text: &str) -> SourceUnit {
    SourceUnit::new(name, &format!("{name}.bas"), text)
}
fn library(units: &[SourceUnit], libs: &[Library]) -> Library {
    let l = ProjectCompiler::default()
        .compile_library(units, &FormCatalog::default(), &[], libs)
        .unwrap();
    let bytes = l.to_tbl().unwrap();
    let l = Library::from_tbl(&bytes).unwrap();
    assert_eq!(bytes, l.to_tbl().unwrap());
    l
}
fn link(main: &str, libs: &[Library]) -> Result<CompiledModule, String> {
    let mut c = ProjectCompiler::default();
    c.compile_library(&[unit("APP", main)], &FormCatalog::default(), &[], libs)
        .and_then(|l| c.link_library("APP", l, None))
        .map_err(|ds| {
            ds.iter()
                .map(ToString::to_string)
                .collect::<Vec<_>>()
                .join("\n")
        })
}
fn output(code: CompiledModule) -> String {
    let mut vm = Vm::new(code);
    assert_eq!(vm.run(&mut CaptureHost::default()), RunEvent::Ended);
    tb_runtime::snapshot::text(&vm.rt.screen)
}
#[test]
fn two_source_free_libraries_with_typed_open_imports_and_diagnostics() {
    let a = library(
        &[unit(
            "A",
            "DECLARE SUB Second(n AS INTEGER)\nSUB First(n AS INTEGER)\nSecond n\nEND SUB",
        )],
        &[],
    );
    assert!(link("CALL First(n%)\nEND", std::slice::from_ref(&a))
        .unwrap_err()
        .contains("SECOND"));
    let b = library(
        &[unit("B", "SUB Second(n AS INTEGER)\nn=n+3\nEND SUB")],
        &[],
    );
    assert_eq!(
        output(
            link(
                "n%=2\nCALL First(n%)\nPRINT n%\nEND",
                &[a.clone(), b.clone()]
            )
            .unwrap()
        ),
        " 5 \n"
    );
    let incompatible = library(&[unit("B", "SUB Second(n AS LONG)\nEND SUB")], &[]);
    assert!(link("CALL First(n%)\nEND", &[a.clone(), incompatible])
        .unwrap_err()
        .contains("Parameter type mismatch"));
    let c = library(&[unit("C", "SUB Second(n AS INTEGER)\nEND SUB")], &[]);
    let direct = link("CALL Second(n%)\nEND", &[b.clone(), c.clone()]).unwrap_err();
    assert!(
        direct.contains("Ambiguous subprogram: SECOND (B, C)"),
        "{direct}"
    );
    let error = link("CALL First(n%)\nEND", &[a.clone(), b.clone(), c]).unwrap_err();
    assert!(
        error.contains("Ambiguous") && error.contains("B") && error.contains("C"),
        "{error}"
    );
    assert!(link("END", &[b.clone(), b.clone()])
        .unwrap_err()
        .contains("Duplicate definition: module B"));
    let combined = library(&[], &[a, b]);
    assert_eq!(combined.modules.len(), 2);
    assert_eq!(
        output(link("CALL First(n%)\nPRINT n%\nEND", &[combined]).unwrap()),
        " 3 \n"
    );
}
#[test]
fn source_and_library_have_identical_types_constants_common_data_byref_and_init_order() {
    let a=unit("A","CONST N=3\nTYPE Record\nx AS INTEGER\ns AS STRING * 4\nEND TYPE\nCOMMON SHARED shared%\nDIM SHARED ready%(N)\nDATA 7\nPRINT \"A\"\nDATA 7\nSUB Work(a%(), r AS Record, n%)\nready%(N)=1\nshared%=shared%+1\na%(1)=N\nr.x=r.x+n%\nn%=9\nRESTORE\nREAD d%\nPRINT d%\nEND SUB\n");
    let b = unit("B", "PRINT \"B\"\n");
    let main="COMMON SHARED shared%\nDIM a%(N)\nDIM r AS Record\nn%=2\nCALL Work(a%(),r,n%)\nPRINT a%(1);r.x;n%;shared%\nCALL Work(a%(),r,(n%))\nPRINT r.x;n%;shared%\n";
    let source = ProjectCompiler::default()
        .compile(
            "APP",
            &[unit("APP", main), a.clone(), b.clone()],
            &FormCatalog::default(),
            &[],
        )
        .unwrap();
    let binary = link(main, &[library(&[a, b], &[])]).unwrap();
    assert_eq!(output(source), output(binary));
}
#[test]
fn library_source_locations_and_run_restart_are_preserved() {
    let l = library(&[unit("LIB", "SUB Fail\n200 ERROR 6\nEND SUB")], &[]);
    let mut vm = Vm::new(link("CALL Fail\nEND", &[l]).unwrap());
    assert!(matches!(
        vm.run(&mut CaptureHost::default()),
        RunEvent::Error {
            code: 6,
            line: 2,
            ..
        }
    ));
    assert_eq!(vm.current_file(), "LIB.bas");
    let l = library(
        &[unit(
            "LIB",
            "DIM SHARED n%\nSUB Increment\nn%=n%+1\nPRINT n%\nEND SUB",
        )],
        &[],
    );
    let code = link("CALL Increment\nEND", &[l]).unwrap();
    for _ in 0..2 {
        assert_eq!(output(code.clone()), " 1 \n");
    }
}
#[test]
fn library_container_rejects_truncation_versions_lengths_and_references() {
    let l = library(
        &[unit("LIB", "CONST N=3\nSUB Work(n%)\nn%=N\nEND SUB")],
        &[],
    );
    let bytes = l.to_tbl().unwrap();
    let seal = |mut payload: Vec<u8>| {
        payload.extend_from_slice(&tb_vm::bytecode::checksum(&payload).to_le_bytes());
        payload
    };
    for end in 0..bytes.len() - 8 {
        assert!(
            Library::from_tbl(&bytes[..end]).is_err(),
            "accepted prefix {end}"
        );
        assert!(
            Library::from_tbl(&seal(bytes[..end].to_vec())).is_err(),
            "accepted incomplete payload {end}"
        );
    }
    for offset in [0, 4, 6, 8, 12, 16] {
        let mut bad = bytes[..bytes.len() - 8].to_vec();
        bad[offset] = 255;
        assert!(
            Library::from_tbl(&seal(bad)).is_err(),
            "accepted offset {offset}"
        );
    }
    let mut bad = bytes[..bytes.len() - 8].to_vec();
    bad.push(0);
    assert!(Library::from_tbl(&seal(bad)).is_err());
    for offset in (0..bytes.len() - 8).step_by(7) {
        let mut bad = bytes[..bytes.len() - 8].to_vec();
        bad[offset] ^= 255;
        let bad = seal(bad);
        std::panic::catch_unwind(|| {
            if let Ok(l) = Library::from_tbl(&bad) {
                let _ = link("END", &[l]);
            }
        })
        .expect("beschädigte TBL darf nicht paniken");
    }
    let mut bad = l.clone();
    if let tb_frontend::ast::Stmt::ConstDecl { items, .. } = &mut bad.modules[0].metadata.exports[0]
    {
        items.clear();
    }
    assert!(bad.to_tbl().is_err());
    let mut bad = l.clone();
    bad.modules[0].metadata.defined.push("ABSENT".into());
    assert!(bad.to_tbl().is_err());
    let mut bad = l;
    bad.modules[0].code.procs[0]
        .code
        .push(tb_vm::bytecode::Instr::Call(u16::MAX, 0));
    assert!(bad.to_tbl().is_err());
}
#[test]
fn separate_form_libraries_remap_nested_objects_initials_arrays_and_events() {
    let form = |name: &str, text: &str| {
        tb_ui::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\nSUB Form_Load\nText1.Text=Text1.Text+\"!\"\nEND SUB\n")).unwrap()
    };
    let forms = [form("Form1", "a"), form("Form2", "b")];
    let mut catalog = FormCatalog::default();
    for f in &forms {
        catalog.append(&f.catalog());
    }
    let units: Vec<_> = forms.iter().map(|f| unit(&f.root.name, &f.code)).collect();
    let libs: Vec<_> = forms
        .iter()
        .zip(&units)
        .map(|(f, u)| {
            let l = ProjectCompiler::default()
                .compile_library(
                    std::slice::from_ref(u),
                    &f.catalog(),
                    std::slice::from_ref(f),
                    &[],
                )
                .unwrap();
            Library::from_tbl(&l.to_tbl().unwrap()).unwrap()
        })
        .collect();
    let main=unit("APP","Form1.Show\nForm2.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");
    let mut all = vec![main.clone()];
    all.extend(units);
    let mut c = ProjectCompiler::default();
    let mut source = c.compile("APP", &all, &catalog, &forms).unwrap();
    source.startup_form = None;
    let l = c
        .compile_library(&[main], &FormCatalog::default(), &[], &libs)
        .unwrap();
    let linked = c.link_library("APP", l, None).unwrap();
    assert_eq!(linked.event_procs.len(), 2);
    assert_eq!(output(source), output(linked));
}

#[test]
fn contracts_reject_result_common_bounds_and_invalid_declarations() {
    let a = library(
        &[unit(
            "A",
            "DECLARE FUNCTION Value%()\nSUB Work\nPRINT Value%\nEND SUB",
        )],
        &[],
    );
    let b = library(&[unit("B", "FUNCTION Value&\nValue&=4\nEND FUNCTION")], &[]);
    assert!(link("CALL Work\nEND", &[a, b])
        .unwrap_err()
        .contains("Parameter type mismatch"));
    let a = library(&[unit("A", "COMMON SHARED n%(1 TO 3)\n")], &[]);
    let b = library(&[unit("B", "COMMON SHARED n%(1 TO 4)\n")], &[]);
    assert!(link("END", &[a, b])
        .unwrap_err()
        .contains("COMMON type or bounds mismatch"));
    for source in [
        "DECLARE SUB Bad(x AS Missing)",
        "DECLARE SUB Bad(x%)\nDECLARE SUB Bad(x&)",
        "CONST N=Missing\n",
        "TYPE X\nn AS Missing\nEND TYPE",
    ] {
        assert!(
            ProjectCompiler::default()
                .compile_library(&[unit("BAD", source)], &FormCatalog::default(), &[], &[])
                .is_err(),
            "{source}"
        );
    }
}

#[test]
fn source_debug_context_keeps_module_ids_when_library_is_between_sources() {
    let l = library(&[unit("LIB", "DIM n%\nSUB LibraryCall\nEND SUB")], &[]);
    let mut compiler = ProjectCompiler::default();
    compiler.debug_symbols = true;
    let mut product = compiler
        .compile_library(
            &[unit("APP", "PRINT 0"), unit("LAST", "x%=7\nSTOP")],
            &FormCatalog::default(),
            &[],
            &[l],
        )
        .unwrap();
    product.modules.swap(1, 2);
    let code = compiler.link_library("APP", product, None).unwrap();
    let debug = compiler.debug_compiler();
    assert!(debug.compile(1, "<main>", "x%", true).is_err());
    let mut vm = Vm::new(code);
    vm.debug.enabled = true;
    vm.debug.compiler = Some(debug);
    assert!(matches!(
        vm.run(&mut CaptureHost::default()),
        RunEvent::Stopped { .. }
    ));
    let frame = vm.debug_location().unwrap().frame;
    assert!(matches!(
        vm.evaluate_watch(frame, "x%").unwrap(),
        tb_runtime::value::Value::Int(7)
    ));
}

#[test]
fn unused_declare_is_not_an_import_but_references_in_any_body_must_resolve() {
    let l = library(
        &[unit(
            "LIB",
            "DECLARE FUNCTION Missing%(s$)\nSUB Work\nPRINT 7\nEND SUB",
        )],
        &[],
    );
    let code = link("CALL Work\nEND", &[l]).unwrap();
    assert!(!code.procs.iter().any(|p| p.name.ends_with("MISSING")));
    assert_eq!(output(code), " 7 \n");
    let l = library(
        &[unit(
            "LIB",
            "DECLARE SUB Missing\nSUB Unused\nCALL Missing\nEND SUB",
        )],
        &[],
    );
    assert!(link("END", &[l])
        .unwrap_err()
        .contains("Subprogram not defined: MISSING"));
    let mut compiler = ProjectCompiler::default();
    compiler.debug_symbols = true;
    compiler
        .compile(
            "APP",
            &[unit("APP", "DECLARE SUB Missing\nSTOP")],
            &FormCatalog::default(),
            &[],
        )
        .unwrap();
    assert!(compiler
        .debug_compiler()
        .compile(0, "<main>", "CALL Missing", false)
        .err()
        .unwrap()
        .contains("Subprogram not defined"));
}

#[test]
fn library_include_locations_keep_physical_lines_and_basic_erl() {
    use tb_frontend::source::SourceSegment;
    let source = SourceUnit {
        name: "LIB".into(),
        segments: vec![
            SourceSegment {
                file: "lib.bas".into(),
                first_line: 1,
                text: "SUB Fail\n".into(),
            },
            SourceSegment {
                file: "nested.bi".into(),
                first_line: 9,
                text: "200 ERROR 6\n".into(),
            },
            SourceSegment {
                file: "lib.bas".into(),
                first_line: 3,
                text: "END SUB\n".into(),
            },
        ],
    };
    let l = library(&[source], &[]);
    let mut vm = Vm::new(link("CALL Fail\nEND", std::slice::from_ref(&l)).unwrap());
    assert!(matches!(
        vm.run(&mut CaptureHost::default()),
        RunEvent::Error {
            code: 6,
            line: 9,
            ..
        }
    ));
    assert_eq!(vm.current_file(), "nested.bi");
    assert_eq!(
        output(
            link(
                "ON ERROR GOTO Handler\nCALL Fail\nEND\nHandler: PRINT ERR;ERL\nEND",
                &[l]
            )
            .unwrap()
        ),
        " 6  200 \n"
    );
}
