use sandlock_core::sandbox::BranchAction;
#[allow(unused_imports)]
use sandlock_core::{Sandbox};
use std::fs;
use std::os::unix::fs::PermissionsExt;
use std::path::PathBuf;

/// Path to the static rootfs-helper binary (compiled by build.rs).
fn helper_binary() -> PathBuf {
    PathBuf::from(env!("CARGO_MANIFEST_DIR"))
        .join("../../tests/rootfs-helper")
        .canonicalize()
        .expect("rootfs-helper not found — build.rs should have compiled it")
}

/// Minimal fs_readable set needed to run rootfs-helper under chroot.
fn minimal_exec_policy(rootfs: &PathBuf) -> sandlock_core::SandboxBuilder {
    Sandbox::builder()
        .chroot(rootfs)
        .fs_read("/usr")
        .fs_read("/bin")
        .fs_read("/proc")
        .fs_read("/dev")
}

fn temp_dir(name: &str) -> PathBuf {
    let dir =
        std::env::temp_dir().join(format!("sandlock-test-chroot-{}-{}", name, std::process::id()));
    let _ = fs::create_dir_all(&dir);
    dir
}

/// Build a self-contained rootfs with the static rootfs-helper binary.
///
/// Layout:
///   usr/bin/rootfs-helper   — the real binary
///   usr/bin/sh              — symlink -> rootfs-helper
///   usr/bin/cat             — symlink -> rootfs-helper
///   usr/bin/echo            — symlink -> rootfs-helper
///   usr/bin/ls              — symlink -> rootfs-helper
///   usr/bin/pwd             — symlink -> rootfs-helper
///   usr/bin/readlink        — symlink -> rootfs-helper
///   usr/bin/true            — symlink -> rootfs-helper
///   usr/bin/write           — symlink -> rootfs-helper
///   bin                     — symlink -> usr/bin  (merged /usr)
///   sbin                    — symlink -> usr/sbin (merged /usr)
///   etc/
///   proc/
///   dev/
///   tmp/                    — mode 1777
fn build_test_rootfs(name: &str) -> PathBuf {
    let rootfs = temp_dir(name);
    let helper = helper_binary();

    // Create real directories
    for dir in &["usr/bin", "usr/sbin", "etc", "proc", "dev", "tmp"] {
        let _ = fs::create_dir_all(rootfs.join(dir));
    }

    // Set /tmp sticky
    let _ = fs::set_permissions(rootfs.join("tmp"), fs::Permissions::from_mode(0o1777));

    // Hard-link the helper binary (atomic, avoids ETXTBSY races from copy).
    let dest = rootfs.join("usr/bin/rootfs-helper");
    fs::hard_link(&helper, &dest)
        .or_else(|_| fs::copy(&helper, &dest).map(|_| ()))
        .expect("failed to install rootfs-helper into rootfs");

    // Create busybox-style symlinks (relative, within rootfs)
    for cmd in &["sh", "cat", "echo", "ls", "pwd", "readlink", "true", "write"] {
        let link = rootfs.join(format!("usr/bin/{}", cmd));
        let _ = fs::remove_file(&link);
        std::os::unix::fs::symlink("rootfs-helper", &link)
            .expect("failed to create busybox symlink");
    }

    // Merged /usr symlinks (like real distros)
    let _ = std::os::unix::fs::symlink("usr/bin", rootfs.join("bin"));
    let _ = std::os::unix::fs::symlink("usr/sbin", rootfs.join("sbin"));

    rootfs
}

fn cleanup_rootfs(rootfs: &PathBuf) {
    let _ = fs::remove_dir_all(rootfs);
}

/// List / inside chroot shows rootfs contents (should see "usr", "tmp", "bin", "etc")
#[tokio::test]
async fn test_chroot_ls_root() {
    let rootfs = build_test_rootfs("ls-root");

    let policy = Sandbox::builder()
        .chroot(&rootfs)
        .fs_read("/usr")
        .fs_read("/bin")
        .fs_read("/etc")
        .fs_read("/proc")
        .fs_read("/dev")
        .fs_read("/tmp")
        .build()
        .unwrap();

    let result = policy.clone().with_name("test").run(&["rootfs-helper", "ls", "/"]).await;
    match result {
        Ok(r) => {
            assert!(
                r.success(),
                "ls / should succeed, stderr: {}",
                r.stderr_str().unwrap_or("")
            );
            let stdout = r.stdout_str().unwrap_or("");
            assert!(stdout.contains("usr"), "should list usr, got: {}", stdout);
            assert!(stdout.contains("tmp"), "should list tmp, got: {}", stdout);
            assert!(stdout.contains("bin"), "should list bin, got: {}", stdout);
            assert!(stdout.contains("etc"), "should list etc, got: {}", stdout);
        }
        Err(e) => eprintln!("Chroot test skipped: {}", e),
    }

    cleanup_rootfs(&rootfs);
}

/// Path traversal via /../../ stays confined — reads a file unique to the chroot.
#[tokio::test]
async fn test_chroot_no_escape() {
    let rootfs = build_test_rootfs("no-escape");

    // Write a sentinel file only inside the chroot's /etc
    let sentinel = "sandlock-chroot-sentinel";
    fs::write(rootfs.join("etc/sentinel"), sentinel).unwrap();

    let policy = Sandbox::builder()
        .chroot(&rootfs)
        .fs_read("/usr")
        .fs_read("/bin")
        .fs_read("/etc")
        .fs_read("/proc")
        .fs_read("/dev")
        .build()
        .unwrap();

    // Path traversal: /../../etc/sentinel should resolve to /etc/sentinel inside
    // the chroot (the sentinel file we created), not escape to the host.
    let result = policy.clone().with_name("test").run(&["rootfs-helper", "cat", "/../../etc/sentinel"]).await;
    match result {
        Ok(r) => {
            assert!(
                r.success(),
                "cat should succeed, stderr: {}",
                r.stderr_str().unwrap_or("")
            );
            let stdout = r.stdout_str().unwrap_or("");
            assert_eq!(
                stdout.trim(),
                sentinel,
                "should read chroot sentinel, got: {}",
                stdout
            );
        }
        Err(e) => eprintln!("Chroot test skipped: {}", e),
    }

    cleanup_rootfs(&rootfs);
}

/// pwd returns / inside chroot
#[tokio::test]
async fn test_chroot_getcwd() {
    let rootfs = build_test_rootfs("getcwd");

    let policy = Sandbox::builder()
        .chroot(&rootfs)
        .fs_read("/usr")
        .fs_read("/bin")
        .fs_read("/etc")
        .fs_read("/proc")
        .fs_read("/dev")
        .build()
        .unwrap();

    let result = policy.clone().with_name("test").run(&["rootfs-helper", "pwd"]).await;
    match result {
        Ok(r) => {
            assert!(
                r.success(),
                "pwd should succeed, stderr: {}",
                r.stderr_str().unwrap_or("")
            );
            let stdout = r.stdout_str().unwrap_or("").trim().to_string();
            assert_eq!(stdout, "/", "pwd should return /, got: {}", stdout);
        }
        Err(e) => eprintln!("Chroot test skipped: {}", e),
    }

    cleanup_rootfs(&rootfs);
}

/// echo hello > /tmp/test.txt && cat /tmp/test.txt works, file appears in rootfs/tmp
#[tokio::test]
async fn test_chroot_write_file() {
    let rootfs = build_test_rootfs("write-file");

    let policy = Sandbox::builder()
        .chroot(&rootfs)
        .fs_read("/usr")
        .fs_read("/bin")
        .fs_read("/etc")
        .fs_read("/proc")
        .fs_read("/dev")
        .fs_write("/tmp")
        .build()
        .unwrap();

    let result = policy.clone().with_name("test").run(&["rootfs-helper", "sh", "-c", "echo hello > /tmp/test.txt && cat /tmp/test.txt"],
    )
    .await;
    match result {
        Ok(r) => {
            assert!(
                r.success(),
                "should succeed, stderr: {}",
                r.stderr_str().unwrap_or("")
            );
            let stdout = r.stdout_str().unwrap_or("").trim().to_string();
            assert_eq!(stdout, "hello", "cat should output hello, got: {}", stdout);
            // File should appear in rootfs/tmp (since /tmp inside chroot maps to rootfs/tmp)
            let real_path = rootfs.join("tmp/test.txt");
            assert!(
                real_path.exists(),
                "test.txt should exist at {}",
                real_path.display()
            );
        }
        Err(e) => eprintln!("Chroot test skipped: {}", e),
    }

    cleanup_rootfs(&rootfs);
}

/// Directory opens under chroot + COW must stay inside the chroot.
#[tokio::test]
async fn test_chroot_cow_directory_open_stays_in_rootfs() {
    let rootfs = build_test_rootfs("cow-dir-open");
    let tmp_dir = rootfs.join("tmp");

    fs::write(rootfs.join("tmp/rootfs-only.txt"), "rootfs").unwrap();

    let host_marker = std::env::temp_dir().join(format!(
        "sandlock-host-marker-{}",
        std::process::id()
    ));
    fs::write(&host_marker, "host").unwrap();

    let policy = Sandbox::builder()
        .chroot(&rootfs)
        .fs_read("/usr")
        .fs_read("/bin")
        .fs_read("/etc")
        .fs_read("/proc")
        .fs_read("/dev")
        .fs_read("/tmp")
        .workdir(&tmp_dir)
        .on_exit(BranchAction::Abort)
        .build()
        .unwrap();

    let result = policy.clone().with_name("test").run(&["rootfs-helper", "ls", "/tmp"]).await;
    match result {
        Ok(r) => {
            assert!(
                r.success(),
                "ls /tmp should succeed, stderr: {}",
                r.stderr_str().unwrap_or("")
            );
            let stdout = r.stdout_str().unwrap_or("");
            assert!(
                stdout.contains("rootfs-only.txt"),
                "expected to see rootfs file in /tmp, got: {}",
                stdout
            );
            assert!(
                !stdout.contains(host_marker.file_name().unwrap().to_string_lossy().as_ref()),
                "host /tmp leaked into chroot listing: {}",
                stdout
            );
        }
        Err(e) => eprintln!("Chroot test skipped: {}", e),
    }

    let _ = fs::remove_file(&host_marker);
    cleanup_rootfs(&rootfs);
}

/// chroot + COW with BranchAction::Abort discards writes
#[tokio::test]
async fn test_chroot_with_cow() {
    let rootfs = build_test_rootfs("cow");
    let tmp_dir = rootfs.join("tmp");

    let policy = Sandbox::builder()
        .chroot(&rootfs)
        .fs_read("/usr")
        .fs_read("/bin")
        .fs_read("/etc")
        .fs_read("/proc")
        .fs_read("/dev")
        .fs_write("/tmp")
        .workdir(&tmp_dir)
        .on_exit(BranchAction::Abort)
        .build()
        .unwrap();

    let result = policy.clone().with_name("test").run(&["rootfs-helper", "sh", "-c", "echo cow-test > /tmp/cow.txt"],
    )
    .await;
    match result {
        Ok(r) => {
            assert!(
                r.success(),
                "should succeed, stderr: {}",
                r.stderr_str().unwrap_or("")
            );
            // With abort, file should NOT exist
            let cow_file = tmp_dir.join("cow.txt");
            assert!(
                !cow_file.exists(),
                "cow.txt should not exist after abort, but found at {}",
                cow_file.display()
            );
        }
        Err(e) => eprintln!("Chroot test skipped: {}", e),
    }

    cleanup_rootfs(&rootfs);
}

/// readlink /proc/self/root returns /
#[tokio::test]
async fn test_chroot_proc_self_root() {
    let rootfs = build_test_rootfs("proc-self-root");

    let policy = Sandbox::builder()
        .chroot(&rootfs)
        .fs_read("/usr")
        .fs_read("/bin")
        .fs_read("/etc")
        .fs_read("/proc")
        .fs_read("/dev")
        .build()
        .unwrap();

    let result = policy.clone().with_name("test").run(&["rootfs-helper", "readlink", "/proc/self/root"]).await;
    match result {
        Ok(r) => {
            assert!(
                r.success(),
                "readlink should succeed, stderr: {}",
                r.stderr_str().unwrap_or("")
            );
            let stdout = r.stdout_str().unwrap_or("").trim().to_string();
            assert_eq!(
                stdout, "/",
                "readlink /proc/self/root should return /, got: {}",
                stdout
            );
        }
        Err(e) => eprintln!("Chroot test skipped: {}", e),
    }

    cleanup_rootfs(&rootfs);
}

/// Writing to /tmp should fail when only fs_read is granted (no fs_write("/tmp"))
#[tokio::test]
async fn test_chroot_write_denied_without_fs_write() {
    let rootfs = build_test_rootfs("write-denied");

    let policy = minimal_exec_policy(&rootfs)
        .fs_read("/etc")
        .fs_read("/tmp")
        // Deliberately NO fs_write("/tmp")
        .build()
        .unwrap();

    let result = policy.clone().with_name("test").run(&["rootfs-helper", "sh", "-c", "echo denied > /tmp/should-fail.txt"],
    )
    .await;
    match result {
        Ok(r) => {
            assert!(
                !r.success(),
                "write should fail without fs_write, but got exit=0"
            );
        }
        Err(e) => eprintln!("Chroot test skipped: {}", e),
    }

    cleanup_rootfs(&rootfs);
}

/// execve inside chroot with fs_readable=["/"] should work — regression test
/// for a bug where the seccomp path rewrite truncated /proc/self/fd/N when
/// the original path buffer was shorter than the replacement string.
#[tokio::test]
async fn test_chroot_exec_with_root_readable() {
    let rootfs = build_test_rootfs("exec-root-readable");

    let policy = minimal_exec_policy(&rootfs)
        .fs_read("/etc")
        .fs_read("/")
        .build()
        .unwrap();

    // Use /bin/rootfs-helper which goes through the bin -> usr/bin symlink
    let result = policy.clone().with_name("test").run(&["/bin/rootfs-helper", "echo", "chroot-exec-ok"]).await;
    match result {
        Ok(r) => {
            assert!(
                r.success(),
                "/bin/rootfs-helper should succeed with fs_read(\"/\"), exit={:?} stderr: {} stdout: {}",
                r.code(), r.stderr_str().unwrap_or(""), r.stdout_str().unwrap_or("")
            );
            let stdout = r.stdout_str().unwrap_or("");
            assert!(
                stdout.contains("chroot-exec-ok"),
                "should print chroot-exec-ok, got: {}",
                stdout
            );
        }
        Err(e) => eprintln!("Chroot test skipped: {}", e),
    }

    cleanup_rootfs(&rootfs);
}

/// fs_deny should override fs_read inside chroot using virtual paths.
#[tokio::test]
async fn test_chroot_fs_deny_blocks_virtual_path() {
    let rootfs = build_test_rootfs("fs-deny");

    fs::write(rootfs.join("etc/hostname"), "sandlock-test-host").unwrap();

    let policy = minimal_exec_policy(&rootfs)
        .fs_read("/etc")
        .fs_deny("/etc/hostname")
        .build()
        .unwrap();

    let result = policy.clone().with_name("test").run(&["rootfs-helper", "cat", "/etc/hostname"]).await;
    match result {
        Ok(r) => {
            assert!(
                !r.success(),
                "cat /etc/hostname should fail when fs_deny overrides fs_read, exit={:?} stdout={}",
                r.code(),
                r.stdout_str().unwrap_or("")
            );
        }
        Err(e) => eprintln!("Chroot test skipped: {}", e),
    }

    cleanup_rootfs(&rootfs);
}

/// Reading /etc/hostname should fail when /etc is not in fs_readable
#[tokio::test]
async fn test_chroot_read_denied_without_fs_read() {
    let rootfs = build_test_rootfs("read-denied");

    // Create a hostname file in the rootfs
    fs::write(rootfs.join("etc/hostname"), "sandlock-test-host").unwrap();

    let policy = minimal_exec_policy(&rootfs)
        // Deliberately NO fs_read("/etc")
        .build()
        .unwrap();

    let result = policy.clone().with_name("test").run(&["rootfs-helper", "cat", "/etc/hostname"]).await;
    match result {
        Ok(r) => {
            assert!(
                !r.success(),
                "cat /etc/hostname should fail without fs_read(\"/etc\"), exit={:?} stdout={}",
                r.code(),
                r.stdout_str().unwrap_or("")
            );
        }
        Err(e) => eprintln!("Chroot test skipped: {}", e),
    }

    cleanup_rootfs(&rootfs);
}

/// fs_mount maps a host directory to a virtual path inside the chroot.
/// A file written to the host directory should be readable at the virtual path.
#[tokio::test]
async fn test_fs_mount_read_write() {
    let rootfs = build_test_rootfs("fs-mount-rw");

    // Create a /work directory inside the rootfs so the mount point exists
    fs::create_dir_all(rootfs.join("work")).unwrap();

    let work_dir = temp_dir("fs-mount-work");

    // Write a test file in the host directory that will be mounted at /work
    fs::write(work_dir.join("input.txt"), "hello mount").unwrap();

    let policy = minimal_exec_policy(&rootfs)
        .fs_read("/tmp")
        .fs_write("/tmp")
        .fs_read("/work")
        .fs_write("/work")
        .fs_mount("/work", &work_dir)
        .build()
        .unwrap();

    let result = policy.clone().with_name("test").run(&["rootfs-helper", "cat", "/work/input.txt"]).await;
    match result {
        Ok(r) => {
            assert!(
                r.success(),
                "cat /work/input.txt failed: exit={:?} stderr={}",
                r.code(),
                r.stderr_str().unwrap_or("")
            );
            assert_eq!(
                r.stdout_str().unwrap_or("").trim(),
                "hello mount",
                "expected 'hello mount', got: {}",
                r.stdout_str().unwrap_or("")
            );
        }
        Err(e) => eprintln!("fs_mount test skipped: {}", e),
    }

    let _ = fs::remove_dir_all(&rootfs);
    let _ = fs::remove_dir_all(&work_dir);
}

/// When the chroot image ships its own `/etc/hosts`, the synthetic file
/// the sandbox sees should be seeded from the image's content (so any
/// private-registry / internal-service entries the image baked in
/// survive virtualization) — with loopback entries added only if the
/// image didn't already provide them.
#[tokio::test]
async fn test_chroot_etc_hosts_seeded_from_image() {
    let rootfs = build_test_rootfs("etc-hosts-image-seed");
    // Put a hosts file in the image that has its own entry but is
    // missing both loopback families on purpose.
    fs::write(
        rootfs.join("etc/hosts"),
        "10.0.0.5 internal.registry.local\n",
    )
    .unwrap();

    let policy = minimal_exec_policy(&rootfs)
        .fs_read("/etc")
        .build()
        .unwrap();

    let result = policy.clone().with_name("test").run(&["rootfs-helper", "cat", "/etc/hosts"]).await;
    match result {
        Ok(r) => {
            assert!(
                r.success(),
                "cat /etc/hosts should succeed inside chroot, stderr: {}",
                r.stderr_str().unwrap_or("")
            );
            let stdout = r.stdout_str().unwrap_or("").to_string();
            assert!(
                stdout.contains("10.0.0.5 internal.registry.local"),
                "image's /etc/hosts entry missing: {stdout}"
            );
            assert!(
                stdout.contains("127.0.0.1 localhost"),
                "v4 loopback should be injected for the image: {stdout}"
            );
            assert!(
                stdout.contains("::1 localhost"),
                "v6 loopback should be injected for the image: {stdout}"
            );
        }
        Err(e) => eprintln!("Chroot test skipped: {}", e),
    }

    cleanup_rootfs(&rootfs);
}

/// When the image already ships loopback entries, the synthesizer must
/// not duplicate them — the sandbox should see exactly the image's view
/// of localhost, not two copies of it.
#[tokio::test]
async fn test_chroot_etc_hosts_no_duplicate_loopback() {
    let rootfs = build_test_rootfs("etc-hosts-no-dup");
    fs::write(
        rootfs.join("etc/hosts"),
        "127.0.0.1 localhost\n::1 localhost\n10.0.0.5 svc.local\n",
    )
    .unwrap();

    let policy = minimal_exec_policy(&rootfs)
        .fs_read("/etc")
        .build()
        .unwrap();

    let result = policy.clone().with_name("test").run(&["rootfs-helper", "cat", "/etc/hosts"]).await;
    match result {
        Ok(r) => {
            assert!(r.success(), "cat /etc/hosts should succeed");
            let stdout = r.stdout_str().unwrap_or("").to_string();
            assert_eq!(
                stdout.matches("127.0.0.1 localhost").count(),
                1,
                "v4 loopback duplicated: {stdout}"
            );
            assert_eq!(
                stdout.matches("::1 localhost").count(),
                1,
                "v6 loopback duplicated: {stdout}"
            );
            assert!(stdout.contains("10.0.0.5 svc.local"), "image entry missing: {stdout}");
        }
        Err(e) => eprintln!("Chroot test skipped: {}", e),
    }

    cleanup_rootfs(&rootfs);
}
