#[cfg(all(
    target_os = "linux",
    any(target_arch = "x86_64", target_arch = "x86"),
    target_env = "gnu"
))]
use memoffset::offset_of;
use nix::errno::Errno;
use nix::sys::ptrace;
#[cfg(any(target_os = "android", target_os = "linux"))]
use nix::sys::ptrace::Options;
use nix::unistd::getpid;

#[cfg(any(target_os = "android", target_os = "linux"))]
use std::mem;

use crate::*;

#[test]
fn test_ptrace() {
    // Just make sure ptrace can be called at all, for now.
    // FIXME: qemu-user doesn't implement ptrace on all arches, so permit ENOSYS
    require_capability!("test_ptrace", CAP_SYS_PTRACE);
    let err = ptrace::attach(getpid()).unwrap_err();
    assert!(
        err == Errno::EPERM || err == Errno::EINVAL || err == Errno::ENOSYS
    );
}

// Just make sure ptrace_setoptions can be called at all, for now.
#[test]
#[cfg(any(target_os = "android", target_os = "linux"))]
fn test_ptrace_setoptions() {
    require_capability!("test_ptrace_setoptions", CAP_SYS_PTRACE);
    let err = ptrace::setoptions(getpid(), Options::PTRACE_O_TRACESYSGOOD)
        .unwrap_err();
    assert_ne!(err, Errno::EOPNOTSUPP);
}

// Just make sure ptrace_getevent can be called at all, for now.
#[test]
#[cfg(any(target_os = "android", target_os = "linux"))]
fn test_ptrace_getevent() {
    require_capability!("test_ptrace_getevent", CAP_SYS_PTRACE);
    let err = ptrace::getevent(getpid()).unwrap_err();
    assert_ne!(err, Errno::EOPNOTSUPP);
}

// Just make sure ptrace_getsiginfo can be called at all, for now.
#[test]
#[cfg(any(target_os = "android", target_os = "linux"))]
fn test_ptrace_getsiginfo() {
    require_capability!("test_ptrace_getsiginfo", CAP_SYS_PTRACE);
    if let Err(Errno::EOPNOTSUPP) = ptrace::getsiginfo(getpid()) {
        panic!("ptrace_getsiginfo returns Errno::EOPNOTSUPP!");
    }
}

// Just make sure ptrace_setsiginfo can be called at all, for now.
#[test]
#[cfg(any(target_os = "android", target_os = "linux"))]
fn test_ptrace_setsiginfo() {
    require_capability!("test_ptrace_setsiginfo", CAP_SYS_PTRACE);
    let siginfo = unsafe { mem::zeroed() };
    if let Err(Errno::EOPNOTSUPP) = ptrace::setsiginfo(getpid(), &siginfo) {
        panic!("ptrace_setsiginfo returns Errno::EOPNOTSUPP!");
    }
}

#[test]
fn test_ptrace_cont() {
    use nix::sys::ptrace;
    use nix::sys::signal::{raise, Signal};
    use nix::sys::wait::{waitpid, WaitPidFlag, WaitStatus};
    use nix::unistd::fork;
    use nix::unistd::ForkResult::*;

    require_capability!("test_ptrace_cont", CAP_SYS_PTRACE);

    let _m = crate::FORK_MTX.lock();

    // FIXME: qemu-user doesn't implement ptrace on all architectures
    // and returns ENOSYS in this case.
    // We (ab)use this behavior to detect the affected platforms
    // and skip the test then.
    // On valid platforms the ptrace call should return Errno::EPERM, this
    // is already tested by `test_ptrace`.
    let err = ptrace::attach(getpid()).unwrap_err();
    if err == Errno::ENOSYS {
        return;
    }

    match unsafe { fork() }.expect("Error: Fork Failed") {
        Child => {
            ptrace::traceme().unwrap();
            // As recommended by ptrace(2), raise SIGTRAP to pause the child
            // until the parent is ready to continue
            loop {
                raise(Signal::SIGTRAP).unwrap();
            }
        }
        Parent { child } => {
            assert_eq!(
                waitpid(child, None),
                Ok(WaitStatus::Stopped(child, Signal::SIGTRAP))
            );
            ptrace::cont(child, None).unwrap();
            assert_eq!(
                waitpid(child, None),
                Ok(WaitStatus::Stopped(child, Signal::SIGTRAP))
            );
            ptrace::cont(child, Some(Signal::SIGKILL)).unwrap();
            match waitpid(child, None) {
                Ok(WaitStatus::Signaled(pid, Signal::SIGKILL, _))
                    if pid == child =>
                {
                    // FIXME It's been observed on some systems (apple) the
                    // tracee may not be killed but remain as a zombie process
                    // affecting other wait based tests. Add an extra kill just
                    // to make sure there are no zombies.
                    let _ = waitpid(child, Some(WaitPidFlag::WNOHANG));
                    while ptrace::cont(child, Some(Signal::SIGKILL)).is_ok() {
                        let _ = waitpid(child, Some(WaitPidFlag::WNOHANG));
                    }
                }
                _ => panic!("The process should have been killed"),
            }
        }
    }
}

#[cfg(target_os = "linux")]
#[test]
fn test_ptrace_interrupt() {
    use nix::sys::ptrace;
    use nix::sys::signal::Signal;
    use nix::sys::wait::{waitpid, WaitPidFlag, WaitStatus};
    use nix::unistd::fork;
    use nix::unistd::ForkResult::*;
    use std::thread::sleep;
    use std::time::Duration;

    require_capability!("test_ptrace_interrupt", CAP_SYS_PTRACE);

    let _m = crate::FORK_MTX.lock();

    match unsafe { fork() }.expect("Error: Fork Failed") {
        Child => loop {
            sleep(Duration::from_millis(1000));
        },
        Parent { child } => {
            ptrace::seize(child, ptrace::Options::PTRACE_O_TRACESYSGOOD)
                .unwrap();
            ptrace::interrupt(child).unwrap();
            assert_eq!(
                waitpid(child, None),
                Ok(WaitStatus::PtraceEvent(child, Signal::SIGTRAP, 128))
            );
            ptrace::syscall(child, None).unwrap();
            assert_eq!(
                waitpid(child, None),
                Ok(WaitStatus::PtraceSyscall(child))
            );
            ptrace::detach(child, Some(Signal::SIGKILL)).unwrap();
            match waitpid(child, None) {
                Ok(WaitStatus::Signaled(pid, Signal::SIGKILL, _))
                    if pid == child =>
                {
                    let _ = waitpid(child, Some(WaitPidFlag::WNOHANG));
                    while ptrace::cont(child, Some(Signal::SIGKILL)).is_ok() {
                        let _ = waitpid(child, Some(WaitPidFlag::WNOHANG));
                    }
                }
                _ => panic!("The process should have been killed"),
            }
        }
    }
}

// ptrace::{setoptions, getregs} are only available in these platforms
#[cfg(all(
    target_os = "linux",
    any(target_arch = "x86_64", target_arch = "x86"),
    target_env = "gnu"
))]
#[test]
fn test_ptrace_syscall() {
    use nix::sys::ptrace;
    use nix::sys::signal::kill;
    use nix::sys::signal::Signal;
    use nix::sys::wait::{waitpid, WaitStatus};
    use nix::unistd::fork;
    use nix::unistd::getpid;
    use nix::unistd::ForkResult::*;

    require_capability!("test_ptrace_syscall", CAP_SYS_PTRACE);

    let _m = crate::FORK_MTX.lock();

    match unsafe { fork() }.expect("Error: Fork Failed") {
        Child => {
            ptrace::traceme().unwrap();
            // first sigstop until parent is ready to continue
            let pid = getpid();
            kill(pid, Signal::SIGSTOP).unwrap();
            kill(pid, Signal::SIGTERM).unwrap();
            unsafe {
                ::libc::_exit(0);
            }
        }

        Parent { child } => {
            assert_eq!(
                waitpid(child, None),
                Ok(WaitStatus::Stopped(child, Signal::SIGSTOP))
            );

            // set this option to recognize syscall-stops
            ptrace::setoptions(child, ptrace::Options::PTRACE_O_TRACESYSGOOD)
                .unwrap();

            #[cfg(target_arch = "x86_64")]
            let get_syscall_id =
                || ptrace::getregs(child).unwrap().orig_rax as libc::c_long;

            #[cfg(target_arch = "x86")]
            let get_syscall_id =
                || ptrace::getregs(child).unwrap().orig_eax as libc::c_long;

            // this duplicates `get_syscall_id` for the purpose of testing `ptrace::read_user`.
            #[cfg(target_arch = "x86_64")]
            let rax_offset = offset_of!(libc::user_regs_struct, orig_rax);
            #[cfg(target_arch = "x86")]
            let rax_offset = offset_of!(libc::user_regs_struct, orig_eax);

            let get_syscall_from_user_area = || {
                // Find the offset of `user.regs.rax` (or `user.regs.eax` for x86)
                let rax_offset = offset_of!(libc::user, regs) + rax_offset;
                ptrace::read_user(child, rax_offset as _).unwrap()
                    as libc::c_long
            };

            // kill entry
            ptrace::syscall(child, None).unwrap();
            assert_eq!(
                waitpid(child, None),
                Ok(WaitStatus::PtraceSyscall(child))
            );
            assert_eq!(get_syscall_id(), ::libc::SYS_kill);
            assert_eq!(get_syscall_from_user_area(), ::libc::SYS_kill);

            // kill exit
            ptrace::syscall(child, None).unwrap();
            assert_eq!(
                waitpid(child, None),
                Ok(WaitStatus::PtraceSyscall(child))
            );
            assert_eq!(get_syscall_id(), ::libc::SYS_kill);
            assert_eq!(get_syscall_from_user_area(), ::libc::SYS_kill);

            // receive signal
            ptrace::syscall(child, None).unwrap();
            assert_eq!(
                waitpid(child, None),
                Ok(WaitStatus::Stopped(child, Signal::SIGTERM))
            );

            // inject signal
            ptrace::syscall(child, Signal::SIGTERM).unwrap();
            assert_eq!(
                waitpid(child, None),
                Ok(WaitStatus::Signaled(child, Signal::SIGTERM, false))
            );
        }
    }
}
