#[cfg(not(all(target_os = "linux", target_arch = "x86_64")))]
fn main() {
    eprintln!("runsc_file_control_smoke only runs on Linux/x86_64");
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn main() -> Result<(), Box<dyn std::error::Error>> {
    use std::io::{Read, Write};
    use std::net::TcpStream;
    use std::time::Duration;

    use supermachine::{Image, PullPolicy, VmConfig};

    let image = Image::builder("node:24-alpine")
        .with_name(format!(
            "runsc-file-control-smoke-{}-{}",
            std::process::id(),
            unique_suffix()
        ))
        .with_pull_policy(PullPolicy::Missing)
        .with_cmd([
            "sh",
            "-lc",
            "trap 'echo term >/tmp/workload.signal; exit 0' TERM; echo ready >/tmp/workload.ready; while true; do sleep 1; done",
        ])
        .build()?;

    let vm = image.start(&VmConfig::new())?;
    let ready = vm
        .exec_builder()
        .argv([
            "sh",
            "-lc",
            "for i in $(seq 1 50); do [ -f /tmp/workload.ready ] && exit 0; sleep 0.1; done; exit 1",
        ])
        .timeout(Duration::from_secs(10))
        .output()?;
    assert!(
        ready.success(),
        "workload did not become ready: status={:?} stdout={} stderr={}",
        ready.status.code(),
        String::from_utf8_lossy(&ready.stdout),
        String::from_utf8_lossy(&ready.stderr)
    );

    let payload = b"runsc-file-control:\0:\xff\n";
    vm.write_file("/tmp/sm-io.bin", payload)?;
    let got = vm.read_file("/tmp/sm-io.bin")?;
    assert_eq!(got, payload);

    let capped = vm.read_file_with_max_bytes("/tmp/sm-io.bin", payload.len() as u64)?;
    assert_eq!(capped, payload);
    assert!(vm
        .read_file_with_max_bytes("/tmp/sm-io.bin", payload.len() as u64 - 1)
        .is_err());

    let staged = vm
        .exec_builder()
        .stage_file_mode(
            "/tmp/staged.sh",
            b"#!/bin/sh\nprintf staged:$SMOKE_ENV",
            0o755,
        )
        .env("SMOKE_ENV", "ok")
        .argv(["/tmp/staged.sh"])
        .output()?;
    assert!(
        staged.success(),
        "staged exec failed: stdout={} stderr={}",
        String::from_utf8_lossy(&staged.stdout),
        String::from_utf8_lossy(&staged.stderr)
    );
    assert_eq!(staged.stdout, b"staged:ok");

    let mut child = vm
        .exec_builder()
        .argv(["sh", "-lc", "cat; printf err >&2"])
        .spawn()?;
    let mut stdin = child.stdin().expect("spawn stdin");
    stdin.write_all(b"spawn-ok\n")?;
    stdin.close()?;
    let mut stdout = child.stdout().expect("spawn stdout");
    let mut stderr = child.stderr().expect("spawn stderr");
    let mut stdout_buf = Vec::new();
    let mut stderr_buf = Vec::new();
    stdout.read_to_end(&mut stdout_buf)?;
    stderr.read_to_end(&mut stderr_buf)?;
    let spawn_status = child.wait()?;
    assert_eq!(spawn_status.code(), Some(0));
    assert_eq!(stdout_buf, b"spawn-ok\n");
    assert_eq!(stderr_buf, b"err");

    let tty = vm
        .exec_builder()
        .tty(true)
        .winsize(81, 24)
        .argv(["sh", "-lc", "test -t 0 && test -t 1 && stty size"])
        .output()?;
    assert!(
        tty.success(),
        "tty exec failed: stdout={} stderr={}",
        String::from_utf8_lossy(&tty.stdout),
        String::from_utf8_lossy(&tty.stderr)
    );
    let tty_stdout = String::from_utf8_lossy(&tty.stdout);
    assert!(
        tty_stdout.contains("24 81"),
        "tty winsize mismatch: {tty_stdout:?}"
    );

    let httpd = vm
        .exec_builder()
        .stage_file(
            "/tmp/server.js",
            b"require('node:http').createServer((_req,res)=>res.end('runsc-http\\n')).listen(18080,'0.0.0.0')\n",
        )
        .argv(["node", "/tmp/server.js"])
        .spawn()?;
    let http_ready = vm
        .exec_builder()
        .argv([
            "sh",
            "-lc",
            "for i in $(seq 1 50); do node -e \"fetch('http://127.0.0.1:18080/').then(r=>r.text()).then(t=>process.exit(t.includes('runsc-http')?0:1)).catch(()=>process.exit(1))\" && exit 0; sleep 0.1; done; exit 1",
        ])
        .timeout(Duration::from_secs(10))
        .output()?;
    assert!(
        http_ready.success(),
        "httpd did not become ready: stdout={} stderr={}",
        String::from_utf8_lossy(&http_ready.stdout),
        String::from_utf8_lossy(&http_ready.stderr)
    );

    let mut direct = vm.connect_port(18080)?;
    direct.write_all(b"GET / HTTP/1.1\r\nHost: runsc\r\nConnection: close\r\n\r\n")?;
    let mut direct_body = String::new();
    direct.read_to_string(&mut direct_body)?;
    assert!(direct_body.contains("runsc-http"), "{direct_body}");

    let fwd = vm.expose_tcp(0, 18080)?;
    let mut tcp = TcpStream::connect(fwd.local_addr())?;
    tcp.write_all(b"GET / HTTP/1.1\r\nHost: runsc\r\nConnection: close\r\n\r\n")?;
    let mut fwd_body = String::new();
    tcp.read_to_string(&mut fwd_body)?;
    assert!(fwd_body.contains("runsc-http"), "{fwd_body}");
    drop(fwd);
    let _ = httpd.signal(libc::SIGTERM);
    let _ = httpd.wait();

    let snap_dir = std::env::temp_dir().join(format!(
        "runsc-file-control-snapshot-{}-{}",
        std::process::id(),
        unique_suffix()
    ));
    let snap = vm.snapshot(&snap_dir)?;
    let restored = snap.start(&VmConfig::new())?;
    let restored_payload = restored.read_file("/tmp/sm-io.bin")?;
    assert_eq!(restored_payload, payload);
    let restored_ready = restored
        .exec_builder()
        .argv(["sh", "-lc", "test -f /tmp/workload.ready && echo restored"])
        .timeout(Duration::from_secs(10))
        .output()?;
    assert!(
        restored_ready.success(),
        "snapshot restore marker missing: stdout={} stderr={}",
        String::from_utf8_lossy(&restored_ready.stdout),
        String::from_utf8_lossy(&restored_ready.stderr)
    );

    restored.workload_signal(libc::SIGTERM)?;
    let signaled = restored
        .exec_builder()
        .argv([
            "sh",
            "-lc",
            "for i in $(seq 1 50); do [ -f /tmp/workload.signal ] && cat /tmp/workload.signal && exit 0; sleep 0.1; done; exit 1",
        ])
        .timeout(Duration::from_secs(10))
        .output()?;
    assert!(
        signaled.success(),
        "signal marker missing: stdout={} stderr={}",
        String::from_utf8_lossy(&signaled.stdout),
        String::from_utf8_lossy(&signaled.stderr)
    );
    print!("{}", String::from_utf8_lossy(&signaled.stdout));
    let _ = std::fs::remove_dir_all(&snap_dir);
    Ok(())
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn unique_suffix() -> u128 {
    std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|d| d.as_nanos())
        .unwrap_or(0)
}
