use std::{
    path::Path,
    process::{Command, Output, Stdio},
    time::{Duration, Instant},
};

fn tbc(dir: &Path, command: &str, file: &str) -> Output {
    let mut child = Command::new(env!("CARGO_BIN_EXE_tbc"))
        .current_dir(dir)
        .args([command, file])
        .stdin(Stdio::null())
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .spawn()
        .unwrap();
    let start = Instant::now();
    while child.try_wait().unwrap().is_none() {
        if start.elapsed() > Duration::from_secs(10) {
            child.kill().unwrap();
            child.wait().unwrap();
            panic!("tbc {command} {file}: Frist überschritten");
        }
        std::thread::sleep(Duration::from_millis(10));
    }
    child.wait_with_output().unwrap()
}

#[test]
fn projekt_und_formular_laufen_nach_entfernen_saemtlicher_quellen() {
    let dir = std::env::temp_dir().join(format!("tb-tbc-v4-{}", std::process::id()));
    std::fs::create_dir_all(&dir).unwrap();
    std::fs::write(dir.join("main.bas"), "a$=Form1!Text1.Text\nb$=Form1!Text1(2).Text\nForm1.Hide\nCLS\nPRINT a$\nPRINT b$\nCALL Ausgabe\nEND\n").unwrap();
    std::fs::write(
        dir.join("lib.bas"),
        "'$INCLUDE: 'outer.bi'\nSUB Ausgabe\nPRINT N\nEND SUB\n",
    )
    .unwrap();
    std::fs::write(dir.join("outer.bi"), "'$INCLUDE: 'inner.bi'\n").unwrap();
    std::fs::write(dir.join("inner.bi"), "CONST N=7\n").unwrap();
    std::fs::write(dir.join("form.frm"), "VERSION 1.00\nBEGIN Form Form1\n BEGIN TextBox Text1\n  Index = 0\n  Text = \"hello\"\n END\n BEGIN TextBox Text1\n  Index = 2\n  Text = \"world\"\n END\nEND\n").unwrap();
    std::fs::write(
        dir.join("app.mak"),
        "' Projekt\nMAIN.BAS\nLIB.BAS\nFORM.FRM\n",
    )
    .unwrap();
    let before = tbc(&dir, "run", "app.mak");
    assert!(before.status.success(), "{before:?}");
    assert_eq!(
        String::from_utf8_lossy(&before.stdout),
        "hello\nworld\n 7 \n"
    );
    let build = tbc(&dir, "build", "app.mak");
    assert!(build.status.success(), "{build:?}");
    let bytes = std::fs::read(dir.join("app.tbc")).unwrap();
    assert_eq!(&bytes[..6], b"TBC\0\x04\0");
    for file in [
        "main.bas", "lib.bas", "outer.bi", "inner.bi", "form.frm", "app.mak",
    ] {
        std::fs::remove_file(dir.join(file)).unwrap();
    }
    let after = tbc(&dir, "run", "app.tbc");
    assert!(after.status.success(), "{after:?}");
    assert_eq!(after.stdout, before.stdout);
    for version in [1u16, 2, 3, 5, 32767] {
        let mut invalid = bytes.clone();
        invalid[4..6].copy_from_slice(&version.to_le_bytes());
        std::fs::write(dir.join("old.tbc"), invalid).unwrap();
        let out = tbc(&dir, "run", "old.tbc");
        assert!(!out.status.success());
        assert!(
            String::from_utf8_lossy(&out.stderr).contains(&format!("Formatversion {version}")),
            "{out:?}"
        );
    }
    std::fs::remove_dir_all(dir).unwrap();
}

#[test]
fn cli_meldet_physische_quellorte_auch_nach_verschachtelten_includes() {
    let dir = std::env::temp_dir().join(format!("tb-project-origins-{}", std::process::id()));
    std::fs::create_dir_all(&dir).unwrap();
    std::fs::write(dir.join("main.bas"), "CALL Fehler\nEND\n").unwrap();
    std::fs::write(dir.join("lib.bas"), "SUB Fehler\n200 ERROR 6\nEND SUB\n").unwrap();
    std::fs::write(dir.join("app.mak"), "main.bas\nlib.bas\n").unwrap();
    let out = tbc(&dir, "run", "app.mak");
    assert_eq!(out.status.code(), Some(2));
    assert!(
        String::from_utf8_lossy(&out.stderr).contains("lib.bas:2:5:"),
        "{out:?}"
    );
    std::fs::write(dir.join("lib.bas"), "SUB Fehler\ns$=42\nEND SUB\n").unwrap();
    let out = tbc(&dir, "check", "app.mak");
    assert!(!out.status.success());
    assert!(
        String::from_utf8_lossy(&out.stderr).contains("lib.bas:2:1:"),
        "{out:?}"
    );
    std::fs::write(dir.join("lib.bas"), "'$INCLUDE: 'outer.bi'\n").unwrap();
    std::fs::write(dir.join("outer.bi"), "'$INCLUDE: 'inner.bi'\n").unwrap();
    std::fs::write(dir.join("inner.bi"), "SUB Fehler\ns$=42\nEND SUB\n").unwrap();
    let out = tbc(&dir, "check", "app.mak");
    assert!(!out.status.success());
    assert!(
        String::from_utf8_lossy(&out.stderr).contains("inner.bi:2:1:"),
        "{out:?}"
    );
    std::fs::write(dir.join("inner.bi"), "SUB Fehler\n200 ERROR 6\nEND SUB\n").unwrap();
    let out = tbc(&dir, "run", "app.mak");
    assert_eq!(out.status.code(), Some(2));
    assert!(
        String::from_utf8_lossy(&out.stderr).contains("inner.bi:2:5:"),
        "{out:?}"
    );
    std::fs::write(
        dir.join("error.frm"),
        "VERSION 1.00\nBEGIN Form F\nEND\n\nSUB Form_Load\nERROR 6\nEND SUB\n",
    )
    .unwrap();
    assert!(tbc(&dir, "build", "error.frm").status.success());
    std::fs::remove_file(dir.join("error.frm")).unwrap();
    let out = tbc(&dir, "run", "error.tbc");
    assert_eq!(out.status.code(), Some(2));
    assert!(
        String::from_utf8_lossy(&out.stderr).contains("error.frm:6:1:"),
        "{out:?}"
    );
    std::fs::write(dir.join("empty.bas"), "").unwrap();
    assert!(tbc(&dir, "run", "empty.bas").status.success());
    std::fs::remove_dir_all(dir).unwrap();
}

#[test]
fn erlaubte_grafik_und_benannte_nonfeatures_werden_per_cli_geprueft() {
    let dir = std::env::temp_dir().join(format!("tb-graphics-check-{}", std::process::id()));
    std::fs::create_dir_all(&dir).unwrap();
    let path = dir.join("probe.bas");
    for mode in 0..=13 {
        std::fs::write(
            &path,
            format!("SCREEN {mode}\nLINE (1,1)-(4,4),1\nPAINT (2,2),1\nVIEW (0,0)-(10,10)\n"),
        )
        .unwrap();
        let out = tbc(&dir, "check", "probe.bas");
        assert!(
            out.status.success() && out.stderr.is_empty(),
            "SCREEN {mode}: {out:?}"
        );
    }
    for (name, source) in [
        ("POKE", "POKE 1,2"),
        ("PEEK", "PRINT PEEK(1)"),
        ("CIRCLE", "CIRCLE (1,1),1"),
        ("WINDOW", "WINDOW (0,0)-(10,10)"),
        ("IOCTL", "IOCTL #1, \"x\""),
        ("PSET", "PSET (1,1),2"),
        ("PSET", "PSET STEP (1,1),2"),
        ("PRESET", "PRESET (1,1),2"),
        ("PRESET", "PRESET STEP (1,1),2"),
        ("CIRCLE", "CIRCLE STEP (1,1),2"),
        ("WINDOW", "WINDOW SCREEN (0,0)-(10,10)"),
        ("WINDOW", "WINDOW"),
        ("PALETTE", "DIM a%(15)\nPALETTE USING a%(0)"),
        ("PALETTE", "PALETTE"),
        ("PALETTE", "IF 1 THEN PALETTE USING a%(0)"),
    ] {
        std::fs::write(&path, source).unwrap();
        let out = tbc(&dir, "check", "probe.bas");
        let message = String::from_utf8_lossy(&out.stderr);
        assert!(
            !out.status.success()
                && message.contains("Feature unavailable")
                && message.contains(name),
            "{out:?}"
        );
    }
    // Bibliotheksnamen sind keine reservierten Variablennamen.
    std::fs::write(
        &path,
        "DIM PSET(2), PALETTE(2)\nPSET(1)=3\nPALETTE(1)=4\nPRINT PSET(1),PALETTE(1)",
    )
    .unwrap();
    let out = tbc(&dir, "check", "probe.bas");
    assert!(out.status.success(), "{out:?}");
    std::fs::remove_dir_all(dir).unwrap();
}

#[test]
fn explicit_startup_order_and_form_selection_survive_tbc() {
    let dir = std::env::temp_dir().join(format!("tb-startup-{}", std::process::id()));
    std::fs::create_dir_all(&dir).unwrap();
    std::fs::write(dir.join("a.bas"), "PRINT \"A\"\n").unwrap();
    std::fs::write(dir.join("b.bas"), "PRINT \"B\"\n").unwrap();
    for (file, name) in [("one.frm", "One"), ("two.frm", "Two")] {
        std::fs::write(
            dir.join(file),
            format!("VERSION 1.00\nBEGIN Form {name}\nEND\n"),
        )
        .unwrap();
    }
    for startup in [None, Some("b.bas"), Some("two.frm")] {
        std::fs::write(
            dir.join("p.mak"),
            format!(
                "a.bas\nb.bas\none.frm\ntwo.frm\n{}",
                startup
                    .map(|s| format!("' $STARTUP: \"{s}\"\n"))
                    .unwrap_or_default()
            ),
        )
        .unwrap();
        let out = tbc(&dir, "build", "p.mak");
        assert!(out.status.success(), "{out:?}");
        let m =
            tb_vm::bytecode::CompiledModule::from_tbc(&std::fs::read(dir.join("p.tbc")).unwrap())
                .unwrap();
        let chosen = m
            .startup_form
            .map(|i| m.objects[i as usize].name.to_ascii_uppercase());
        assert_eq!(
            chosen.as_deref(),
            match startup {
                None => Some("ONE"),
                Some("two.frm") => Some("TWO"),
                _ => None,
            }
        );
        if startup == Some("b.bas") {
            let direct = tbc(&dir, "run", "p.mak");
            let serialized = tbc(&dir, "run", "p.tbc");
            assert!(direct.status.success() && serialized.status.success());
            assert_eq!(direct.stdout, b"B\nA\n");
            assert_eq!(direct.stdout, serialized.stdout);
        }
    }
    std::fs::remove_dir_all(dir).unwrap();
}

#[test]
fn designer_frm_is_accepted_by_the_standalone_cli_compiler() {
    use tb_frontend::forms::ObjectClass;
    use tb_ide::{app::App, commands::Command as IdeCommand, documents::Destination};
    let dir = std::env::temp_dir().join(format!("tb-designer-cli-{}", std::process::id()));
    std::fs::create_dir_all(&dir).unwrap();
    let dir = dir.canonicalize().unwrap();
    let mut app = App::new(&dir, dir.join("options"), (100, 30)).unwrap();
    app.execute(IdeCommand::NewForm);
    let root = app.design_objects().unwrap()[0].0;
    let button = app.design_place(ObjectClass::CommandButton, 0, root, Default::default());
    assert!(button.is_err()); // ungültige Größe schreibt nichts
    let mut rect = app.area();
    rect.x = 2;
    rect.y = 2;
    rect.width = 8;
    rect.height = 2;
    let control = app
        .design_place(ObjectClass::CommandButton, 0, root, rect)
        .unwrap();
    app.design_property("INDEX", "0").unwrap();
    app.design_event(control, "CLICK").unwrap();
    let doc = app.active_document().unwrap();
    let path = dir.join("designed.frm");
    app.project
        .save_file(doc, Some(&Destination::new(&path)))
        .unwrap();
    let built = tbc(&dir, "build", "designed.frm");
    assert!(built.status.success(), "{built:?}");
    let bytes = std::fs::read(dir.join("designed.tbc")).unwrap();
    assert_eq!(&bytes[..6], b"TBC\0\x04\0");
    tb_vm::project_io::load_program(&dir.join("designed.tbc")).unwrap();
    std::fs::remove_dir_all(dir).unwrap();
}
