use nix::errno::Errno;
use nix::unistd::getpid;
use nix::sys::ptrace;
#[cfg(any(target_os = "android", target_os = "linux"))]
use nix::sys::ptrace::Options;

#[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!(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!(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 retunrs 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::signal::kill;
    use nix::sys::ptrace;
    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;

            // kill entry
            ptrace::syscall(child, None).unwrap();
            assert_eq!(waitpid(child, None), Ok(WaitStatus::PtraceSyscall(child)));
            assert_eq!(get_syscall_id(), ::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);

            // 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)));
        },
    }
}
