use crate::*;
use nix::errno::Errno;
use nix::fcntl::AT_FDCWD;
use nix::sys::fanotify::{
    EventFFlags, Fanotify, FanotifyResponse, InitFlags, MarkFlags, MaskFlags,
    Response,
};
use std::fs::{read_link, read_to_string, File, OpenOptions};
use std::io::ErrorKind;
use std::io::{Read, Write};
use std::os::fd::AsRawFd;
use std::thread;

#[test]
/// Run fanotify tests sequentially to avoid tmp files races
pub fn test_fanotify() {
    require_capability!("test_fanotify", CAP_SYS_ADMIN);

    test_fanotify_notifications();
    test_fanotify_responses();
    test_fanotify_overflow();
}

fn test_fanotify_notifications() {
    let group =
        Fanotify::init(InitFlags::FAN_CLASS_NOTIF, EventFFlags::O_RDONLY)
            .unwrap();
    let tempdir = tempfile::tempdir().unwrap();
    let tempfile = tempdir.path().join("test");
    OpenOptions::new()
        .write(true)
        .create_new(true)
        .open(&tempfile)
        .unwrap();

    group
        .mark(
            MarkFlags::FAN_MARK_ADD,
            MaskFlags::FAN_OPEN | MaskFlags::FAN_MODIFY | MaskFlags::FAN_CLOSE,
            AT_FDCWD,
            Some(&tempfile),
        )
        .unwrap();

    // modify test file
    {
        let mut f = OpenOptions::new().write(true).open(&tempfile).unwrap();
        f.write_all(b"hello").unwrap();
    }

    let mut events = group.read_events().unwrap();
    assert_eq!(events.len(), 1, "should have read exactly one event");
    let event = events.pop().unwrap();
    assert!(event.check_version());
    assert_eq!(
        event.mask(),
        MaskFlags::FAN_OPEN
            | MaskFlags::FAN_MODIFY
            | MaskFlags::FAN_CLOSE_WRITE
    );
    let fd_opt = event.fd();
    let fd = fd_opt.as_ref().unwrap();
    let path = read_link(format!("/proc/self/fd/{}", fd.as_raw_fd())).unwrap();
    assert_eq!(path, tempfile);

    // read test file
    {
        let mut f = File::open(&tempfile).unwrap();
        let mut s = String::new();
        f.read_to_string(&mut s).unwrap();
    }

    let mut events = group.read_events().unwrap();
    assert_eq!(events.len(), 1, "should have read exactly one event");
    let event = events.pop().unwrap();
    assert!(event.check_version());
    assert_eq!(
        event.mask(),
        MaskFlags::FAN_OPEN | MaskFlags::FAN_CLOSE_NOWRITE
    );
    let fd_opt = event.fd();
    let fd = fd_opt.as_ref().unwrap();
    let path = read_link(format!("/proc/self/fd/{}", fd.as_raw_fd())).unwrap();
    assert_eq!(path, tempfile);
}

fn test_fanotify_responses() {
    let group =
        Fanotify::init(InitFlags::FAN_CLASS_CONTENT, EventFFlags::O_RDONLY)
            .unwrap();
    let tempdir = tempfile::tempdir().unwrap();
    let tempfile = tempdir.path().join("test");
    OpenOptions::new()
        .write(true)
        .create_new(true)
        .open(&tempfile)
        .unwrap();

    group
        .mark(
            MarkFlags::FAN_MARK_ADD,
            MaskFlags::FAN_OPEN_PERM,
            AT_FDCWD,
            Some(&tempfile),
        )
        .unwrap();

    let file_thread = thread::spawn({
        let tempfile = tempfile.clone();

        move || {
            // first open, should fail
            let Err(e) = File::open(&tempfile) else {
                panic!("The first open should fail");
            };
            assert_eq!(e.kind(), ErrorKind::PermissionDenied);

            // second open, should succeed
            File::open(&tempfile).unwrap();
        }
    });

    // Deny the first open try
    let mut events = group.read_events().unwrap();
    assert_eq!(events.len(), 1, "should have read exactly one event");
    let event = events.pop().unwrap();
    assert!(event.check_version());
    assert_eq!(event.mask(), MaskFlags::FAN_OPEN_PERM);
    let fd_opt = event.fd();
    let fd = fd_opt.as_ref().unwrap();
    let path = read_link(format!("/proc/self/fd/{}", fd.as_raw_fd())).unwrap();
    assert_eq!(path, tempfile);
    group
        .write_response(FanotifyResponse::new(*fd, Response::FAN_DENY))
        .unwrap();

    // Allow the second open try
    let mut events = group.read_events().unwrap();
    assert_eq!(events.len(), 1, "should have read exactly one event");
    let event = events.pop().unwrap();
    assert!(event.check_version());
    assert_eq!(event.mask(), MaskFlags::FAN_OPEN_PERM);
    let fd_opt = event.fd();
    let fd = fd_opt.as_ref().unwrap();
    let path = read_link(format!("/proc/self/fd/{}", fd.as_raw_fd())).unwrap();
    assert_eq!(path, tempfile);
    group
        .write_response(FanotifyResponse::new(*fd, Response::FAN_ALLOW))
        .unwrap();

    file_thread.join().unwrap();
}

fn test_fanotify_overflow() {
    let max_events: usize =
        read_to_string("/proc/sys/fs/fanotify/max_queued_events")
            .unwrap()
            .trim()
            .parse()
            .unwrap();

    // make sure the kernel is configured with the default value,
    // just so this test doesn't run forever
    assert_eq!(max_events, 16384);

    let group = Fanotify::init(
        InitFlags::FAN_CLASS_NOTIF
            | InitFlags::FAN_REPORT_TID
            | InitFlags::FAN_NONBLOCK,
        EventFFlags::O_RDONLY,
    )
    .unwrap();
    let tempdir = tempfile::tempdir().unwrap();
    let tempfile = tempdir.path().join("test");

    OpenOptions::new()
        .write(true)
        .create_new(true)
        .open(&tempfile)
        .unwrap();

    group
        .mark(
            MarkFlags::FAN_MARK_ADD,
            MaskFlags::FAN_OPEN,
            AT_FDCWD,
            Some(&tempfile),
        )
        .unwrap();

    thread::scope(|s| {
        // perform 10 more events to demonstrate some will be dropped
        for _ in 0..(max_events + 10) {
            s.spawn(|| {
                File::open(&tempfile).unwrap();
            });
        }
    });

    // flush the queue until it's empty
    let mut n = 0;
    let mut last_event = None;
    loop {
        match group.read_events() {
            Ok(events) => {
                n += events.len();
                if let Some(event) = events.last() {
                    last_event = Some(event.mask());
                }
            }
            Err(e) if e == Errno::EWOULDBLOCK => break,
            Err(e) => panic!("{e:?}"),
        }
    }

    // make sure we read all we expected.
    // the +1 is for the overflow event.
    assert_eq!(n, max_events + 1);
    assert_eq!(last_event, Some(MaskFlags::FAN_Q_OVERFLOW));
}
