use std::fs::File;
use std::io::{Read, Write};
use std::os::unix::prelude::*;
use std::path::Path;
use tempfile::tempfile;

use libc::{_exit, STDOUT_FILENO};
use nix::fcntl::{open, OFlag};
use nix::pty::*;
use nix::sys::stat;
use nix::sys::termios::*;
use nix::unistd::{close, pause, write};

/// Regression test for Issue #659
/// This is the correct way to explicitly close a `PtyMaster`
#[test]
fn test_explicit_close() {
    let mut f = {
        let m = posix_openpt(OFlag::O_RDWR).unwrap();
        close(m.into_raw_fd()).unwrap();
        tempfile().unwrap()
    };
    // This should work.  But if there's been a double close, then it will
    // return EBADF
    f.write_all(b"whatever").unwrap();
}

/// Test equivalence of `ptsname` and `ptsname_r`
#[test]
#[cfg(any(target_os = "android", target_os = "linux"))]
fn test_ptsname_equivalence() {
    let _m = crate::PTSNAME_MTX.lock();

    // Open a new PTTY master
    let master_fd = posix_openpt(OFlag::O_RDWR).unwrap();
    assert!(master_fd.as_raw_fd() > 0);

    // Get the name of the slave
    let slave_name = unsafe { ptsname(&master_fd) }.unwrap();
    let slave_name_r = ptsname_r(&master_fd).unwrap();
    assert_eq!(slave_name, slave_name_r);
}

/// Test data copying of `ptsname`
// TODO need to run in a subprocess, since ptsname is non-reentrant
#[test]
#[cfg(any(target_os = "android", target_os = "linux"))]
fn test_ptsname_copy() {
    let _m = crate::PTSNAME_MTX.lock();

    // Open a new PTTY master
    let master_fd = posix_openpt(OFlag::O_RDWR).unwrap();
    assert!(master_fd.as_raw_fd() > 0);

    // Get the name of the slave
    let slave_name1 = unsafe { ptsname(&master_fd) }.unwrap();
    let slave_name2 = unsafe { ptsname(&master_fd) }.unwrap();
    assert_eq!(slave_name1, slave_name2);
    // Also make sure that the string was actually copied and they point to different parts of
    // memory.
    assert_ne!(slave_name1.as_ptr(), slave_name2.as_ptr());
}

/// Test data copying of `ptsname_r`
#[test]
#[cfg(any(target_os = "android", target_os = "linux"))]
fn test_ptsname_r_copy() {
    // Open a new PTTY master
    let master_fd = posix_openpt(OFlag::O_RDWR).unwrap();
    assert!(master_fd.as_raw_fd() > 0);

    // Get the name of the slave
    let slave_name1 = ptsname_r(&master_fd).unwrap();
    let slave_name2 = ptsname_r(&master_fd).unwrap();
    assert_eq!(slave_name1, slave_name2);
    assert_ne!(slave_name1.as_ptr(), slave_name2.as_ptr());
}

/// Test that `ptsname` returns different names for different devices
#[test]
#[cfg(any(target_os = "android", target_os = "linux"))]
fn test_ptsname_unique() {
    let _m = crate::PTSNAME_MTX.lock();

    // Open a new PTTY master
    let master1_fd = posix_openpt(OFlag::O_RDWR).unwrap();
    assert!(master1_fd.as_raw_fd() > 0);

    // Open a second PTTY master
    let master2_fd = posix_openpt(OFlag::O_RDWR).unwrap();
    assert!(master2_fd.as_raw_fd() > 0);

    // Get the name of the slave
    let slave_name1 = unsafe { ptsname(&master1_fd) }.unwrap();
    let slave_name2 = unsafe { ptsname(&master2_fd) }.unwrap();
    assert_ne!(slave_name1, slave_name2);
}

/// Common setup for testing PTTY pairs
fn open_ptty_pair() -> (PtyMaster, File) {
    let _m = crate::PTSNAME_MTX.lock();

    // Open a new PTTY master
    let master = posix_openpt(OFlag::O_RDWR).expect("posix_openpt failed");

    // Allow a slave to be generated for it
    grantpt(&master).expect("grantpt failed");
    unlockpt(&master).expect("unlockpt failed");

    // Get the name of the slave
    let slave_name = unsafe { ptsname(&master) }.expect("ptsname failed");

    // Open the slave device
    let slave_fd =
        open(Path::new(&slave_name), OFlag::O_RDWR, stat::Mode::empty())
            .unwrap();

    #[cfg(target_os = "illumos")]
    // TODO: rewrite using ioctl!
    #[allow(clippy::comparison_chain)]
    {
        use libc::{ioctl, I_FIND, I_PUSH};

        // On illumos systems, as per pts(7D), one must push STREAMS modules
        // after opening a device path returned from ptsname().
        let ptem = b"ptem\0";
        let ldterm = b"ldterm\0";
        let r = unsafe { ioctl(slave_fd, I_FIND, ldterm.as_ptr()) };
        if r < 0 {
            panic!("I_FIND failure");
        } else if r == 0 {
            if unsafe { ioctl(slave_fd, I_PUSH, ptem.as_ptr()) } < 0 {
                panic!("I_PUSH ptem failure");
            }
            if unsafe { ioctl(slave_fd, I_PUSH, ldterm.as_ptr()) } < 0 {
                panic!("I_PUSH ldterm failure");
            }
        }
    }

    let slave = unsafe { File::from_raw_fd(slave_fd) };

    (master, slave)
}

/// Test opening a master/slave PTTY pair
///
/// This uses a common `open_ptty_pair` because much of these functions aren't useful by
/// themselves. So for this test we perform the basic act of getting a file handle for a
/// master/slave PTTY pair, then just sanity-check the raw values.
#[test]
fn test_open_ptty_pair() {
    let (master, slave) = open_ptty_pair();
    assert!(master.as_raw_fd() > 0);
    assert!(slave.as_raw_fd() > 0);
}

/// Put the terminal in raw mode.
fn make_raw(fd: RawFd) {
    let mut termios = tcgetattr(fd).unwrap();
    cfmakeraw(&mut termios);
    tcsetattr(fd, SetArg::TCSANOW, &termios).unwrap();
}

/// Test `io::Read` on the PTTY master
#[test]
fn test_read_ptty_pair() {
    let (mut master, mut slave) = open_ptty_pair();
    make_raw(slave.as_raw_fd());

    let mut buf = [0u8; 5];
    slave.write_all(b"hello").unwrap();
    master.read_exact(&mut buf).unwrap();
    assert_eq!(&buf, b"hello");

    let mut master = &master;
    slave.write_all(b"hello").unwrap();
    master.read_exact(&mut buf).unwrap();
    assert_eq!(&buf, b"hello");
}

/// Test `io::Write` on the PTTY master
#[test]
fn test_write_ptty_pair() {
    let (mut master, mut slave) = open_ptty_pair();
    make_raw(slave.as_raw_fd());

    let mut buf = [0u8; 5];
    master.write_all(b"adios").unwrap();
    slave.read_exact(&mut buf).unwrap();
    assert_eq!(&buf, b"adios");

    let mut master = &master;
    master.write_all(b"adios").unwrap();
    slave.read_exact(&mut buf).unwrap();
    assert_eq!(&buf, b"adios");
}

#[test]
fn test_openpty() {
    // openpty uses ptname(3) internally
    let _m = crate::PTSNAME_MTX.lock();

    let pty = openpty(None, None).unwrap();
    assert!(pty.master > 0);
    assert!(pty.slave > 0);

    // Writing to one should be readable on the other one
    let string = "foofoofoo\n";
    let mut buf = [0u8; 10];
    write(pty.master, string.as_bytes()).unwrap();
    crate::read_exact(pty.slave, &mut buf);

    assert_eq!(&buf, string.as_bytes());

    // Read the echo as well
    let echoed_string = "foofoofoo\r\n";
    let mut buf = [0u8; 11];
    crate::read_exact(pty.master, &mut buf);
    assert_eq!(&buf, echoed_string.as_bytes());

    let string2 = "barbarbarbar\n";
    let echoed_string2 = "barbarbarbar\r\n";
    let mut buf = [0u8; 14];
    write(pty.slave, string2.as_bytes()).unwrap();
    crate::read_exact(pty.master, &mut buf);

    assert_eq!(&buf, echoed_string2.as_bytes());

    close(pty.master).unwrap();
    close(pty.slave).unwrap();
}

#[test]
fn test_openpty_with_termios() {
    // openpty uses ptname(3) internally
    let _m = crate::PTSNAME_MTX.lock();

    // Open one pty to get attributes for the second one
    let mut termios = {
        let pty = openpty(None, None).unwrap();
        assert!(pty.master > 0);
        assert!(pty.slave > 0);
        let termios = tcgetattr(pty.slave).unwrap();
        close(pty.master).unwrap();
        close(pty.slave).unwrap();
        termios
    };
    // Make sure newlines are not transformed so the data is preserved when sent.
    termios.output_flags.remove(OutputFlags::ONLCR);

    let pty = openpty(None, &termios).unwrap();
    // Must be valid file descriptors
    assert!(pty.master > 0);
    assert!(pty.slave > 0);

    // Writing to one should be readable on the other one
    let string = "foofoofoo\n";
    let mut buf = [0u8; 10];
    write(pty.master, string.as_bytes()).unwrap();
    crate::read_exact(pty.slave, &mut buf);

    assert_eq!(&buf, string.as_bytes());

    // read the echo as well
    let echoed_string = "foofoofoo\n";
    crate::read_exact(pty.master, &mut buf);
    assert_eq!(&buf, echoed_string.as_bytes());

    let string2 = "barbarbarbar\n";
    let echoed_string2 = "barbarbarbar\n";
    let mut buf = [0u8; 13];
    write(pty.slave, string2.as_bytes()).unwrap();
    crate::read_exact(pty.master, &mut buf);

    assert_eq!(&buf, echoed_string2.as_bytes());

    close(pty.master).unwrap();
    close(pty.slave).unwrap();
}

#[test]
fn test_forkpty() {
    use nix::sys::signal::*;
    use nix::sys::wait::wait;
    use nix::unistd::ForkResult::*;
    // forkpty calls openpty which uses ptname(3) internally.
    let _m0 = crate::PTSNAME_MTX.lock();
    // forkpty spawns a child process
    let _m1 = crate::FORK_MTX.lock();

    let string = "naninani\n";
    let echoed_string = "naninani\r\n";
    let pty = unsafe { forkpty(None, None).unwrap() };
    match pty.fork_result {
        Child => {
            write(STDOUT_FILENO, string.as_bytes()).unwrap();
            pause(); // we need the child to stay alive until the parent calls read
            unsafe {
                _exit(0);
            }
        }
        Parent { child } => {
            let mut buf = [0u8; 10];
            assert!(child.as_raw() > 0);
            crate::read_exact(pty.master, &mut buf);
            kill(child, SIGTERM).unwrap();
            wait().unwrap(); // keep other tests using generic wait from getting our child
            assert_eq!(&buf, echoed_string.as_bytes());
            close(pty.master).unwrap();
        }
    }
}
