//! Optionaler öffentlicher Bestand; kein Netzwerk und keine GUI.
use std::{
    path::{Path, PathBuf},
    process::Command,
    time::Duration,
};
use tb_runtime::host::{CaptureHost, Ereignis, Host};
use tb_vm::{
    bytecode::CompiledModule,
    interp::{RunEvent, Vm},
};
#[path = "../../../tests/support/prozessfrist.rs"]
mod prozessfrist;

#[derive(Default)]
struct PruefHost {
    inner: CaptureHost,
    dialogantwort: Option<Ereignis>,
    dialog_gesehen: bool,
    dialogtext: Option<&'static str>,
}
impl Host for PruefHost {
    fn present(&mut self, screen: &tb_runtime::screen::TextScreen) {
        self.dialog_gesehen |= self
            .dialogtext
            .is_some_and(|text| tb_runtime::snapshot::text(screen).contains(text));
        self.inner.present(screen);
    }
    fn next_event(&mut self, block: bool) -> Option<Ereignis> {
        if self.dialog_gesehen {
            if let Some(event) = self.dialogantwort.take() {
                return Some(event);
            }
        }
        self.inner.next_event(block)
    }
    fn jetzt_ms(&mut self) -> u64 {
        self.inner.jetzt_ms()
    }
    fn warten(&mut self, deadline: Option<u64>) -> Option<Ereignis> {
        self.inner.warten(deadline)
    }
}

const REVISION: &str = "1cdd2b32b829fe1721d0b6aecc433abc47a96fb6";

fn pruefe_bestand(repo: Option<&Path>) -> Result<(), String> {
    let repo = repo.ok_or_else(|| {
        format!("TB_VBDOS_REPO fehlt: cout/vbdos bei Revision {REVISION} erforderlich")
    })?;
    let rev = Command::new("git")
        .arg("-C")
        .arg(repo)
        .args(["rev-parse", "HEAD"])
        .output()
        .map_err(|e| {
            format!(
                "Fremdprogrammbestand {}: git nicht ausführbar: {e}",
                repo.display()
            )
        })?;
    if !rev.status.success() {
        return Err(format!("Fremdprogrammbestand {} fehlt oder ist kein lesbares Git-Repository; benötigt {REVISION}", repo.display()));
    }
    let found = String::from_utf8_lossy(&rev.stdout);
    if found.trim() != REVISION {
        return Err(format!(
            "Fremdprogrammbestand {}: falsche Revision, Soll {REVISION}, Ist {}",
            repo.display(),
            found.trim()
        ));
    }
    Ok(())
}

#[test]
fn fehlender_oder_falscher_fremdbestand_wird_benannt() {
    assert!(pruefe_bestand(None)
        .unwrap_err()
        .contains("TB_VBDOS_REPO fehlt"));
    let missing = Path::new(env!("CARGO_MANIFEST_DIR")).join("Cargo.toml");
    assert!(pruefe_bestand(Some(&missing))
        .unwrap_err()
        .contains("kein lesbares Git-Repository"));
    let error = pruefe_bestand(Some(Path::new(env!("CARGO_MANIFEST_DIR")))).unwrap_err();
    assert!(
        error.contains("falsche Revision") && error.contains(REVISION),
        "{error}"
    );
}

struct TempBestand {
    original: PathBuf,
    base: PathBuf,
}

impl Drop for TempBestand {
    fn drop(&mut self) {
        let _ = std::env::set_current_dir(&self.original);
        let _ = std::fs::remove_dir_all(&self.base);
    }
}

#[test]
#[ignore = "TB_VBDOS_REPO muss auf cout/vbdos in der dokumentierten Revision zeigen"]
fn oeffentliche_formularprogramme_sind_reproduzierbar_bedienbar() {
    let _frist =
        prozessfrist::Prozessfrist::neu("öffentlicher VBDOS-Bestand", Duration::from_secs(60));
    let repo = std::env::var_os("TB_VBDOS_REPO").map(PathBuf::from);
    pruefe_bestand(repo.as_deref()).unwrap_or_else(|e| panic!("{e}"));
    let repo = repo.unwrap();
    let original = std::env::current_dir().unwrap();
    let base = std::env::temp_dir().join(format!("tb-foreign-{}", std::process::id()));
    std::fs::create_dir_all(&base).unwrap();
    let _temp = TempBestand {
        original: original.clone(),
        base: base.clone(),
    };
    let archive = base.join("source.tar");
    assert!(Command::new("git")
        .arg("-C")
        .arg(&repo)
        .args(["archive", "--format=tar", "-o"])
        .arg(&archive)
        .arg(REVISION)
        .status()
        .unwrap()
        .success());
    for (entry, keys) in [
        ("graphics/graphics.mak", vec![("x", 4)]),
        ("microsoft/check.mak", vec![("f", 4), ("x", 0)]),
        ("microsoft/qlbview.mak", vec![("\u{1b}", 0)]),
        ("microsoft/seek.mak", vec![("x", 4)]),
        (
            "microsoft/spindemo.mak",
            vec![("\t", 0), ("\t", 0), ("\0H", 0)],
        ),
        ("microsoft/notepad.frm", vec![("f", 4), ("x", 0)]),
        ("misc/mentors/mentors.frm", vec![("f", 4), ("x", 0)]),
    ] {
        let mut results = Vec::new();
        for repeat in 0..2 {
            eprintln!("{entry}: Lauf {} kompilieren", repeat + 1);
            // Jeder Lauf startet frisch am selben Pfad: CURDIR$ erscheint
            // bei kurzen Temp-Pfaden im sichtbaren Formulartitel.
            let dir = base.join("run");
            std::fs::create_dir_all(&dir).unwrap();
            assert!(Command::new("tar")
                .arg("-xf")
                .arg(&archive)
                .arg("-C")
                .arg(&dir)
                .status()
                .unwrap()
                .success());
            let path = dir.join(entry);
            let build = Command::new(env!("CARGO_BIN_EXE_tbc"))
                .arg("build")
                .arg(&path)
                .output()
                .unwrap();
            assert!(
                build.status.success() && build.stderr.is_empty(),
                "{entry}: {build:?}"
            );
            let module =
                CompiledModule::from_tbc(&std::fs::read(path.with_extension("tbc")).unwrap())
                    .unwrap();
            let mut vm = Vm::new(module);
            vm.rt.screen.resize(100, 30);
            vm.forms.resize(100, 30);
            std::env::set_current_dir(path.parent().unwrap()).unwrap();
            let mut host = PruefHost {
                dialogtext: match entry {
                    "microsoft/check.mak" | "misc/mentors/mentors.frm" => {
                        Some("Save these records?")
                    }
                    "microsoft/notepad.frm" => Some("Save changes to"),
                    _ => None,
                },
                ..Default::default()
            };
            eprintln!("{entry}: Start und initiale Ereignisse");
            assert_eq!(vm.run(&mut host), RunEvent::Ended, "{entry}: Start");
            assert_eq!(
                vm.run_visible_forms(&mut host),
                RunEvent::Ended,
                "{entry}: initiale Ereignisse"
            );
            assert!(
                vm.forms.has_visible_forms(),
                "{entry}: kein sichtbares Startformular"
            );
            let text1 = vm
                .forms
                .objects
                .iter()
                .position(|o| o.description.name.eq_ignore_ascii_case("Text1"));
            let before = tb_runtime::snapshot::snapshot(&vm.rt.screen);
            let mut states = Vec::new();
            for (key, shift) in &keys {
                eprintln!("{entry}: Taste {key:?}, Modifikatoren {shift}");
                vm.rt.ende = false;
                if host.dialogtext.is_some() && *key == "x" {
                    host.dialogantwort = Some(Ereignis::Taste("n".into(), 0));
                }
                host.inner.ereignis(Ereignis::Taste((*key).into(), *shift));
                assert_eq!(
                    vm.run_visible_forms(&mut host),
                    RunEvent::Ended,
                    "{entry}: Taste {key:?}"
                );
                states.push((
                    vm.forms.has_visible_forms(),
                    vm.forms.menu_is_open(),
                    vm.forms.active_form(),
                    tb_runtime::snapshot::snapshot(&vm.rt.screen),
                ));
            }
            if entry.contains("spindemo") {
                let text_id =
                    tb_frontend::forms::property(tb_frontend::forms::ObjectClass::TextBox, "TEXT")
                        .unwrap()
                        .0;
                assert_eq!(
                    vm.forms.get(text1.unwrap() as u16, text_id).unwrap(),
                    tb_ui::forms::PropertyValue::String(" 1".into()),
                    "{entry}: Spin-Handler hat Text1 nicht aktualisiert"
                );
                assert!(
                    states[2].3 != states[1].3,
                    "Pfeiltaste muss den sichtbaren Wert ändern"
                );
            } else {
                assert!(
                    !vm.forms.has_visible_forms(),
                    "{entry}: Exit/Cancel hat das Formular nicht entladen"
                );
            }
            if keys.len() == 2 {
                assert!(
                    states[0].1 && !states[1].1,
                    "{entry}: Menü öffnen und Exit auswählen"
                );
            }
            if host.dialogtext.is_some() {
                assert!(
                    host.dialog_gesehen && host.dialogantwort.is_none(),
                    "{entry}: Save-Dialog mit N beantworten"
                );
            }
            assert!(
                states.iter().any(|s| !s.0 || s.3 != before),
                "{entry}: keine sichtbare Zustandsänderung"
            );
            eprintln!("{entry}: Lauf {} bestanden", repeat + 1);
            results.push(states);
            std::env::set_current_dir(&original).unwrap();
            std::fs::remove_dir_all(&dir).unwrap();
        }
        assert_eq!(results[0], results[1], "{entry}: nicht reproduzierbar");
    }
    std::fs::remove_dir_all(&base).unwrap();
    assert!(
        !base.exists(),
        "temporärer Fremdprogrammbestand blieb zurück"
    );
}
