//! End-to-end del primer entregable: receta `grep` real → build → hidratación → ejecución
//! dentro del rootfs Alpine. Es el test que cierra Fase 0 + Fase 1.
//!
//! Doblemente gateado para no penalizar `cargo test` por defecto:
//!   - Requiere `.dev-fs/` bootstrapeado (rootfs Alpine + zig + cache).
//!   - Requiere `HAMMER_NETWORK_TESTS=1` (descarga ~3 MB del tarball de GNU).
//!
//! Comparte la dev cache (`.dev-fs/cache`) para que la segunda corrida en una máquina dada
//! reuse los objetos de musl y termine en segundos.

use std::path::PathBuf;
use std::process::Command;

use hammer_build::{build, run_hydrate, BuildConfig};
use hammer_core::{LinkMode, Recipe, Store};

const RECIPE_TOML: &str = r#"
name    = "grep"
version = "3.12"

[source]
tarball = "https://ftp.gnu.org/gnu/grep/grep-3.12.tar.gz"
sha256  = "badda546dfc4b9d97e992e2c35f3b5c7f20522ffcbe2f01ba1e9cdcbe7644cdc"

[build]
compiler = "zig-cc"
target   = "x86_64-linux-musl"
link     = "static"
flags    = ["--disable-perl-regexp", "--disable-nls"]
"#;

fn project_root() -> PathBuf {
    PathBuf::from(env!("CARGO_MANIFEST_DIR"))
        .parent()
        .unwrap()
        .parent()
        .unwrap()
        .to_path_buf()
}

fn skip_with_reason(reason: &str) -> bool {
    eprintln!("SKIP end_to_end_grep: {reason}");
    true
}

fn should_skip(cfg: &BuildConfig) -> bool {
    if std::env::var("HAMMER_NETWORK_TESTS").ok().as_deref() != Some("1") {
        return skip_with_reason(
            "HAMMER_NETWORK_TESTS != 1 (este test descarga ~3 MB de ftp.gnu.org y compila ~30s).",
        );
    }
    if !cfg.rootfs.join("bin/busybox").is_file() {
        return skip_with_reason("rootfs Alpine no bootstrapped (corre scripts/bootstrap-devfs.sh).");
    }
    if !cfg.rootfs.join("usr/bin/autoreconf").is_file()
        || !cfg.rootfs.join("usr/bin/ld").is_file()
    {
        return skip_with_reason(
            "rootfs sin build tools (autoreconf/ld). Re-corre bootstrap-devfs.sh.",
        );
    }
    if !cfg.zig_dir.join("zig").is_file() {
        return skip_with_reason("zig no encontrado en .dev-fs/tools/zig.");
    }
    false
}

#[test]
fn grep_real_build_hydrate_run() {
    let _ = tracing_subscriber::fmt().with_test_writer().try_init();

    let project = project_root();
    // El store y la salida hidratada DEBEN vivir en el mismo filesystem (los hardlinks no
    // cruzan device boundaries). Mantenemos ambos bajo el repo, en un subdir efímero.
    let scratch = project.join("work").join("test-e2e-grep");
    let _ = std::fs::remove_dir_all(&scratch);
    std::fs::create_dir_all(&scratch).unwrap();
    let store_root = scratch.join("store");
    let work_root = scratch.join("work");
    std::fs::create_dir_all(&store_root).unwrap();
    std::fs::create_dir_all(&work_root).unwrap();

    let cfg = BuildConfig {
        rootfs: project.join(".dev-fs/alpine"),
        zig_dir: project.join(".dev-fs/tools/zig"),
        work_root,
        cache_root: Some(project.join(".dev-fs/cache")),
    };
    if should_skip(&cfg) {
        return;
    }

    // 1) Receta
    let recipe_dir = scratch.join("recipe");
    std::fs::create_dir_all(&recipe_dir).unwrap();
    let recipe_path = recipe_dir.join("grep.toml");
    std::fs::write(&recipe_path, RECIPE_TOML).unwrap();
    let recipe = Recipe::load_from_path(&recipe_path).expect("load recipe");

    // 2) Build
    let store = Store::open(&store_root).unwrap();
    let h = build(&recipe, &cfg, &store).expect("build grep");
    let sealed = store.path_of(&h, &recipe.name);
    let grep = sealed.join("usr/bin/grep");
    assert!(grep.is_file(), "esperaba {}", grep.display());

    // ELF estático: cabecera ELF + ningún DT_NEEDED (chequeo barato con `file`/`readelf` si
    // existen, fallback al magic byte).
    let header = std::fs::read(&grep).unwrap();
    assert_eq!(&header[..4], b"\x7fELF", "grep debe ser ELF");

    // 3) Hidratación al mismo filesystem
    let fhs = scratch.join("fhs");
    let report = run_hydrate(&sealed, &fhs, LinkMode::Static, None).expect("hydrate");
    assert!(report.files.iter().any(|f| f.dst.ends_with("usr/bin/grep")));

    // Compartir inode con el store: la propiedad central de hidratar por hardlink.
    use std::os::unix::fs::MetadataExt;
    let src_ino = std::fs::metadata(&grep).unwrap().ino();
    let dst_ino = std::fs::metadata(fhs.join("usr/bin/grep")).unwrap().ino();
    assert_eq!(src_ino, dst_ino, "hardlink: inode debe coincidir");

    // 4) Ejecutar dentro del rootfs Alpine bindeando el binario hidratado como /usr/bin/grep.
    //    Ejercitamos un patrón regex y un literal; v3.12 NO debe dar "memory exhausted".
    let out = Command::new("bwrap")
        .args([
            "--overlay-src",
            cfg.rootfs.to_str().unwrap(),
            "--tmp-overlay",
            "/",
            "--ro-bind",
            fhs.join("usr/bin/grep").to_str().unwrap(),
            "/usr/bin/grep",
            "--proc",
            "/proc",
            "--dev",
            "/dev",
            "--unshare-all",
            "/usr/bin/grep",
            "-oE",
            "[0-9]+",
        ])
        .stdin(std::process::Stdio::piped())
        .stdout(std::process::Stdio::piped())
        .stderr(std::process::Stdio::piped())
        .spawn()
        .expect("spawn bwrap");

    use std::io::Write;
    out.stdin
        .as_ref()
        .unwrap()
        .write_all(b"prefijo 12345 sufijo\n")
        .unwrap();
    let out = out.wait_with_output().expect("wait bwrap");
    assert!(
        out.status.success(),
        "grep en bwrap falló: stderr={}",
        String::from_utf8_lossy(&out.stderr)
    );
    assert_eq!(String::from_utf8_lossy(&out.stdout).trim(), "12345");

    // 5) Cache hit en segunda build: mismo hash.
    let h2 = build(&recipe, &cfg, &store).expect("rebuild grep");
    assert_eq!(h, h2, "segunda build debe ser cache hit");
}
