// Copyright 2019 The ChromiumOS Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

use std::borrow::Cow;
use std::collections::HashSet;
use std::collections::VecDeque;
use std::env;
use std::ffi::CString;
use std::ffi::OsString;
use std::fs;
use std::fs::File;
use std::io;
use std::io::Cursor;
use std::mem;
use std::ops::Deref;
use std::os::unix::ffi::OsStringExt;
use std::os::unix::fs::symlink;
use std::os::unix::fs::MetadataExt;
use std::path::Component;
use std::path::Path;
use std::path::PathBuf;
use std::u32;

use super::*;

// Used to indicate that there is no fid associated with this message.
const P9_NOFID: u32 = u32::MAX;

// The fid associated with the root directory of the server.
const ROOT_FID: u32 = 1;

// The pid of the server process, cannot be 1 since that's the kernel init
const SERVER_PID: u32 = 5;

// How big we want the default buffer to be when running tests.
const DEFAULT_BUFFER_SIZE: u32 = 4096;

// How big we want to make randomly generated files
const LOCAL_FILE_LEN: u64 = 200;

// Joins `path` to `buf`.  If `path` is '..', removes the last component from `buf`
// only if `buf` != `root` but does nothing if `buf` == `root`.  Pushes `path` onto
// `buf` if it is a normal path component.
//
// Returns an error if `path` is absolute, has more than one component, or contains
// a '.' component.
fn join_path<P: AsRef<Path>, R: AsRef<Path>>(
    mut buf: PathBuf,
    path: P,
    root: R,
) -> io::Result<PathBuf> {
    let path = path.as_ref();
    let root = root.as_ref();
    debug_assert!(buf.starts_with(root));

    if path.components().count() > 1 {
        return Err(io::Error::from_raw_os_error(libc::EINVAL));
    }

    for component in path.components() {
        match component {
            // Prefix should only appear on windows systems.
            Component::Prefix(_) => return Err(io::Error::from_raw_os_error(libc::EINVAL)),
            // Absolute paths are not allowed.
            Component::RootDir => return Err(io::Error::from_raw_os_error(libc::EINVAL)),
            // '.' elements are not allowed.
            Component::CurDir => return Err(io::Error::from_raw_os_error(libc::EINVAL)),
            Component::ParentDir => {
                // We only remove the parent path if we are not already at the root of the
                // file system.
                if buf != root {
                    buf.pop();
                }
            }
            Component::Normal(element) => buf.push(element),
        }
    }

    Ok(buf)
}

// Automatically deletes the path it contains when it goes out of scope.
struct ScopedPath<P: AsRef<Path>>(P);

impl<P: AsRef<Path>> AsRef<Path> for ScopedPath<P> {
    fn as_ref(&self) -> &Path {
        self.0.as_ref()
    }
}

impl<P: AsRef<Path>> Deref for ScopedPath<P> {
    type Target = Path;

    fn deref(&self) -> &Self::Target {
        self.0.as_ref()
    }
}

impl<P: AsRef<Path>> Drop for ScopedPath<P> {
    fn drop(&mut self) {
        if let Err(e) = fs::remove_dir_all(&**self) {
            println!("Failed to remove {}: {}", self.display(), e);
        }
    }
}

enum DirEntry<'a> {
    File {
        name: &'a str,
        content: &'a [u8],
    },
    Directory {
        name: &'a str,
        entries: &'a [DirEntry<'a>],
    },
    Symlink {
        name: &'a str,
        target: &'a str,
    },
}

impl<'a> DirEntry<'a> {
    // Creates `self` in the path given by `dir`.
    // TODO(b/228627457): clippy is warning about the `Cow` below, but it is necessary
    #[allow(clippy::ptr_arg)]
    fn create(&self, dir: &mut Cow<Path>) {
        match *self {
            DirEntry::File { name, content } => {
                let mut f = File::create(dir.join(name)).expect("failed to create file");
                f.write_all(content).expect("failed to write file content");
            }
            DirEntry::Directory { name, entries } => {
                dir.to_mut().push(name);

                fs::create_dir_all(&**dir).expect("failed to create directory");
                for e in entries {
                    e.create(dir);
                }

                assert!(dir.to_mut().pop());
            }
            DirEntry::Symlink { name, target } => {
                symlink(target, dir.join(name)).expect("failed to create symlink");
            }
        }
    }
}

// Creates a file with `name` in `dir` and fills it with random
// content.
fn create_local_file<P: AsRef<Path>>(dir: P, name: &str) -> Vec<u8> {
    let mut content = Vec::new();
    File::open("/dev/urandom")
        .and_then(|f| f.take(LOCAL_FILE_LEN).read_to_end(&mut content))
        .expect("failed to read from /dev/urandom");

    let f = DirEntry::File {
        name,
        content: &content,
    };
    f.create(&mut Cow::from(dir.as_ref()));

    content
}

// Create a symlink named `name` that links to `target`.
fn create_local_symlink<P: AsRef<Path>>(dir: P, name: &str, target: &str) {
    let f = DirEntry::Symlink { name, target };
    f.create(&mut Cow::from(dir.as_ref()));
}

fn check_qid(qid: &Qid, md: &fs::Metadata) {
    let ty = if md.is_dir() {
        P9_QTDIR
    } else if md.is_file() {
        P9_QTFILE
    } else if md.file_type().is_symlink() {
        P9_QTSYMLINK
    } else {
        panic!("unknown file type: {:?}", md.file_type());
    };
    assert_eq!(qid.ty, ty);
    assert_eq!(qid.version, md.mtime() as u32);
    assert_eq!(qid.path, md.ino());
}

fn check_attr(server: &mut Server, fid: u32, md: &fs::Metadata) {
    let tgetattr = Tgetattr {
        fid,
        request_mask: P9_GETATTR_BASIC,
    };

    let rgetattr = server.get_attr(&tgetattr).expect("failed to call get_attr");

    let ty = if md.is_dir() {
        P9_QTDIR
    } else if md.is_file() {
        P9_QTFILE
    } else if md.file_type().is_symlink() {
        P9_QTSYMLINK
    } else {
        panic!("unknown file type: {:?}", md.file_type());
    };
    assert_eq!(rgetattr.valid, P9_GETATTR_BASIC);
    assert_eq!(rgetattr.qid.ty, ty);
    assert_eq!(rgetattr.qid.version, md.mtime() as u32);
    assert_eq!(rgetattr.qid.path, md.ino());
    assert_eq!(rgetattr.mode, md.mode());
    assert_eq!(rgetattr.uid, md.uid());
    assert_eq!(rgetattr.gid, md.gid());
    assert_eq!(rgetattr.nlink, md.nlink());
    assert_eq!(rgetattr.rdev, md.rdev());
    assert_eq!(rgetattr.size, md.size());
    assert_eq!(rgetattr.atime_sec, md.atime() as u64);
    assert_eq!(rgetattr.atime_nsec, md.atime_nsec() as u64);
    assert_eq!(rgetattr.mtime_sec, md.mtime() as u64);
    assert_eq!(rgetattr.mtime_nsec, md.mtime_nsec() as u64);
    assert_eq!(rgetattr.ctime_sec, md.ctime() as u64);
    assert_eq!(rgetattr.ctime_nsec, md.ctime_nsec() as u64);
    assert_eq!(rgetattr.btime_sec, 0);
    assert_eq!(rgetattr.btime_nsec, 0);
    assert_eq!(rgetattr.gen, 0);
    assert_eq!(rgetattr.data_version, 0);
}

fn check_content(server: &mut Server, content: &[u8], fid: u32) {
    for offset in 0..content.len() {
        let tread = Tread {
            fid,
            offset: offset as u64,
            count: DEFAULT_BUFFER_SIZE,
        };

        let rread = server.read(&tread).expect("failed to read file");
        assert_eq!(content[offset..], rread.data[..]);
    }
}

fn walk<P: Into<PathBuf>>(
    server: &mut Server,
    start: P,
    fid: u32,
    newfid: u32,
    names: Vec<String>,
) {
    let mut mds = Vec::with_capacity(names.len());
    let mut buf = start.into();
    for name in &names {
        buf.push(name);
        mds.push(
            buf.symlink_metadata()
                .expect("failed to get metadata for path"),
        );
    }

    let twalk = Twalk {
        fid,
        newfid,
        wnames: names,
    };

    let rwalk = server.walk(&twalk).expect("failed to walk directoy");
    assert_eq!(mds.len(), rwalk.wqids.len());
    for (md, qid) in mds.iter().zip(rwalk.wqids.iter()) {
        check_qid(qid, md);
    }
}

fn open<P: Into<PathBuf>>(
    server: &mut Server,
    dir: P,
    dir_fid: u32,
    name: &str,
    fid: u32,
    flags: u32,
) -> io::Result<Rlopen> {
    let wnames = if name.is_empty() {
        vec![]
    } else {
        vec![String::from(name)]
    };
    walk(server, dir, dir_fid, fid, wnames);

    let tlopen = Tlopen { fid, flags };

    server.lopen(&tlopen)
}

fn write<P: AsRef<Path>>(server: &mut Server, dir: P, name: &str, fid: u32, flags: u32) {
    let file_path = dir.as_ref().join(name);
    let file_len = if file_path.exists() {
        fs::symlink_metadata(&file_path)
            .expect("unable to get metadata for file")
            .len() as usize
    } else {
        0usize
    };
    let mut new_content = Vec::new();
    File::open("/dev/urandom")
        .and_then(|f| f.take(200).read_to_end(&mut new_content))
        .expect("failed to read from /dev/urandom");

    let twrite = Twrite {
        fid,
        offset: 0,
        data: Data(new_content),
    };

    let rwrite = server.write(&twrite).expect("failed to write file");
    assert_eq!(rwrite.count, twrite.data.len() as u32);

    let tfsync = Tfsync { fid, datasync: 0 };
    server.fsync(&tfsync).expect("failed to sync file contents");

    let actual_content = fs::read(file_path).expect("failed to read back content from file");

    // If the file was opened append-only, then the content should have been
    // written to the end even though the offset was 0.
    let idx = if flags & P9_APPEND == 0 { 0 } else { file_len };
    assert_eq!(actual_content[idx..], twrite.data[..]);
}

fn create<P: Into<PathBuf>>(
    server: &mut Server,
    dir: P,
    dir_fid: u32,
    fid: u32,
    name: &str,
    flags: u32,
    mode: u32,
) -> io::Result<Rlcreate> {
    // The `fid` in the lcreate call initially points to the directory
    // but is supposed to point to the newly created file after the call
    // completes.  Duplicate the fid so that we don't end up consuming the
    // directory fid.
    walk(server, dir, dir_fid, fid, Vec::new());

    let tlcreate = Tlcreate {
        fid,
        name: String::from(name),
        flags,
        mode,
        gid: 0,
    };

    server.lcreate(&tlcreate)
}

struct Readdir<'a> {
    server: &'a mut Server,
    fid: u32,
    offset: u64,
    cursor: Cursor<Vec<u8>>,
}

impl<'a> Iterator for Readdir<'a> {
    type Item = Dirent;

    fn next(&mut self) -> Option<Self::Item> {
        if self.cursor.position() >= self.cursor.get_ref().len() as u64 {
            let treaddir = Treaddir {
                fid: self.fid,
                offset: self.offset,
                count: DEFAULT_BUFFER_SIZE,
            };

            let Rreaddir { data } = self
                .server
                .readdir(&treaddir)
                .expect("failed to read directory");
            if data.is_empty() {
                // No more entries.
                return None;
            }

            mem::drop(mem::replace(&mut self.cursor, Cursor::new(data.0)));
        }

        let dirent: Dirent = WireFormat::decode(&mut self.cursor).expect("failed to decode dirent");
        self.offset = dirent.offset;

        Some(dirent)
    }
}

fn readdir(server: &mut Server, fid: u32) -> Readdir {
    Readdir {
        server,
        fid,
        offset: 0,
        cursor: Cursor::new(Vec::new()),
    }
}

// Sets up the server to start handling messages.  Creates a new temporary
// directory to act as the server root and sends an initial Tattach message.
// At the end of setup, fid 1 points to the root of the server.
fn setup<P: AsRef<Path>>(name: P) -> (ScopedPath<OsString>, Server) {
    let mut test_dir = env::var_os("T")
        .map(PathBuf::from)
        .unwrap_or_else(env::temp_dir);
    test_dir.push(name);

    let mut os_str = OsString::from(test_dir);
    os_str.push(".XXXXXX");

    // Create a c string and release ownership.  This seems like the only way
    // to get a *mut c_char.
    let buf = CString::new(os_str.into_vec())
        .expect("failed to create CString")
        .into_raw();

    // Safe because this will only modify the contents of `buf`.
    let ret = unsafe { libc::mkdtemp(buf) };

    // Take ownership of the buffer back before checking the result.  Safe because
    // this was created by a call to into_raw() above and mkdtemp will not overwrite
    // the trailing '\0'.
    let buf = unsafe { CString::from_raw(buf) };

    assert!(!ret.is_null());

    let test_dir = ScopedPath(OsString::from_vec(buf.into_bytes()));

    // Create a basic file system hierarchy.
    let entries = [
        DirEntry::Directory {
            name: "subdir",
            entries: &[
                DirEntry::File {
                    name: "b",
                    content: b"hello, world!",
                },
                DirEntry::Directory {
                    name: "nested",
                    entries: &[DirEntry::File {
                        name: "Огонь по готовности!",
                        content: &[
                            0xe9u8, 0xbeu8, 0x8du8, 0xe3u8, 0x81u8, 0x8cu8, 0xe6u8, 0x88u8, 0x91u8,
                            0xe3u8, 0x81u8, 0x8cu8, 0xe6u8, 0x95u8, 0xb5u8, 0xe3u8, 0x82u8, 0x92u8,
                            0xe5u8, 0x96u8, 0xb0u8, 0xe3u8, 0x82u8, 0x89u8, 0xe3u8, 0x81u8, 0x86u8,
                            0x21u8,
                        ],
                    }],
                },
            ],
        },
        DirEntry::File {
            name: "世界.txt",
            content: &[
                0xe3u8, 0x81u8, 0x93u8, 0xe3u8, 0x82u8, 0x93u8, 0xe3u8, 0x81u8, 0xabu8, 0xe3u8,
                0x81u8, 0xa1u8, 0xe3u8, 0x81u8, 0xafu8,
            ],
        },
    ];

    for e in &entries {
        e.create(&mut Cow::from(&*test_dir));
    }

    let md = test_dir
        .symlink_metadata()
        .expect("failed to get metadata for root dir");

    let mut server = Server::new(&*test_dir, Default::default(), Default::default())
        .expect("Failed to create server");

    let tversion = Tversion {
        msize: DEFAULT_BUFFER_SIZE,
        version: String::from("9P2000.L"),
    };

    let rversion = server
        .version(&tversion)
        .expect("failed to get version from server");
    assert_eq!(rversion.msize, DEFAULT_BUFFER_SIZE);
    assert_eq!(rversion.version, "9P2000.L");

    let tattach = Tattach {
        fid: ROOT_FID,
        afid: P9_NOFID,
        uname: String::from("unittest"),
        aname: String::from(""),
        n_uname: 1000,
    };

    let rattach = server.attach(&tattach).expect("failed to attach to server");
    check_qid(&rattach.qid, &md);

    (test_dir, server)
}

#[test]
fn path_joins() {
    let root = PathBuf::from("/a/b/c");
    let path = PathBuf::from("/a/b/c/d/e/f");

    assert_eq!(
        &join_path(path.clone(), "nested", &root).expect("normal"),
        Path::new("/a/b/c/d/e/f/nested")
    );

    let p1 = join_path(path, "..", &root).expect("parent 1");
    assert_eq!(&p1, Path::new("/a/b/c/d/e/"));

    let p2 = join_path(p1, "..", &root).expect("parent 2");
    assert_eq!(&p2, Path::new("/a/b/c/d/"));

    let p3 = join_path(p2, "..", &root).expect("parent 3");
    assert_eq!(&p3, Path::new("/a/b/c/"));

    let p4 = join_path(p3, "..", &root).expect("parent of root");
    assert_eq!(&p4, Path::new("/a/b/c/"));
}

#[test]
fn invalid_joins() {
    let root = PathBuf::from("/a");
    let path = PathBuf::from("/a/b");

    join_path(path.clone(), ".", &root).expect_err("current directory");
    join_path(path.clone(), "c/d/e", &root).expect_err("too many components");
    join_path(path, "/c/d/e", &root).expect_err("absolute path");
}

#[test]
fn clunk() {
    let (_test_dir, mut server) = setup("clunk");

    let tclunk = Tclunk { fid: ROOT_FID };
    server.clunk(&tclunk).expect("failed to clunk root fid");
}

#[test]
fn get_attr() {
    let (test_dir, mut server) = setup("get_attr");

    let md = test_dir
        .symlink_metadata()
        .expect("failed to get metadata for test dir");

    check_attr(&mut server, ROOT_FID, &md);
}

#[test]
fn tree_walk() {
    let (test_dir, mut server) = setup("readdir");

    let mut next_fid = ROOT_FID + 1;

    let mut dirs = VecDeque::new();
    dirs.push_back(test_dir.to_path_buf());

    while let Some(dir) = dirs.pop_front() {
        let dfid = next_fid;
        next_fid += 1;

        let wnames: Vec<String> = dir
            .strip_prefix(&test_dir)
            .expect("test directory is not prefix of subdir")
            .components()
            .map(|c| Path::new(&c).to_string_lossy().to_string())
            .collect();
        walk(&mut server, &*test_dir, ROOT_FID, dfid, wnames);

        let md = dir.symlink_metadata().expect("failed to get metadata");

        check_attr(&mut server, dfid, &md);

        let fid = next_fid;
        next_fid += 1;
        open(&mut server, &dir, dfid, "", fid, P9_DIRECTORY).expect("Failed to open directory");
        for dirent in readdir(&mut server, fid) {
            if dirent.name == "." || dirent.name == ".." {
                continue;
            }

            let entry_path = dir.join(&dirent.name);
            assert!(
                entry_path.exists(),
                "directory entry \"{}\" does not exist",
                entry_path.display()
            );
            let md = fs::symlink_metadata(&entry_path).expect("failed to get metadata for entry");

            let ty = if md.is_dir() {
                dirs.push_back(dir.join(dirent.name));
                libc::DT_DIR
            } else if md.is_file() {
                libc::DT_REG
            } else if md.file_type().is_symlink() {
                libc::DT_LNK
            } else {
                panic!("unknown file type: {:?}", md.file_type());
            };

            assert_eq!(dirent.ty, ty);
            check_qid(&dirent.qid, &md);
        }

        let tclunk = Tclunk { fid };
        server.clunk(&tclunk).expect("failed to clunk fid");
    }
}

#[test]
fn create_existing_file() {
    let (test_dir, mut server) = setup("create_existing");

    let name = "existing";
    create_local_file(&test_dir, name);

    let fid = ROOT_FID + 1;
    create(
        &mut server,
        &*test_dir,
        ROOT_FID,
        fid,
        name,
        P9_APPEND,
        0o644,
    )
    .expect_err("successfully created existing file");
}

enum SetAttrKind {
    File,
    Directory,
}

fn set_attr_test<F>(kind: SetAttrKind, set_fields: F) -> io::Result<fs::Metadata>
where
    F: FnOnce(&mut Tsetattr),
{
    let (test_dir, mut server) = setup("set_attr");

    let name = "existing";
    match kind {
        SetAttrKind::File => {
            create_local_file(&test_dir, name);
        }
        SetAttrKind::Directory => {
            let tmkdir = Tmkdir {
                dfid: ROOT_FID,
                name: String::from(name),
                mode: 0o755,
                gid: 0,
            };

            let rmkdir = server.mkdir(&tmkdir).expect("failed to create directory");
            let md = fs::symlink_metadata(test_dir.join(name))
                .expect("failed to get metadata for directory");

            assert!(md.is_dir());
            check_qid(&rmkdir.qid, &md);
        }
    };

    let fid = ROOT_FID + 1;
    walk(
        &mut server,
        &*test_dir,
        ROOT_FID,
        fid,
        vec![String::from(name)],
    );

    let mut tsetattr = Tsetattr {
        fid,
        valid: 0,
        mode: 0,
        uid: 0,
        gid: 0,
        size: 0,
        atime_sec: 0,
        atime_nsec: 0,
        mtime_sec: 0,
        mtime_nsec: 0,
    };

    set_fields(&mut tsetattr);
    server.set_attr(&tsetattr)?;

    fs::symlink_metadata(test_dir.join(name))
}

#[test]
fn set_len() {
    let len = 661;
    let md = set_attr_test(SetAttrKind::File, |tsetattr| {
        tsetattr.valid = P9_SETATTR_SIZE;
        tsetattr.size = len;
    })
    .expect("failed to run set length of file");

    assert_eq!(md.size(), len);
}

#[test]
fn set_file_mode() {
    let mode = 0o640;
    let md = set_attr_test(SetAttrKind::File, |tsetattr| {
        tsetattr.valid = P9_SETATTR_MODE;
        tsetattr.mode = mode;
    })
    .expect("failed to set mode");

    assert_eq!(md.mode() & 0o777, mode);
}

#[test]
fn set_file_mtime() {
    let (secs, nanos) = (1245247825, 524617);
    let md = set_attr_test(SetAttrKind::File, |tsetattr| {
        tsetattr.valid = P9_SETATTR_MTIME | P9_SETATTR_MTIME_SET;
        tsetattr.mtime_sec = secs;
        tsetattr.mtime_nsec = nanos;
    })
    .expect("failed to set mtime");

    assert_eq!(md.mtime() as u64, secs);
    assert_eq!(md.mtime_nsec() as u64, nanos);
}

#[test]
fn set_file_atime() {
    let (secs, nanos) = (9247605, 4016);
    let md = set_attr_test(SetAttrKind::File, |tsetattr| {
        tsetattr.valid = P9_SETATTR_ATIME | P9_SETATTR_ATIME_SET;
        tsetattr.atime_sec = secs;
        tsetattr.atime_nsec = nanos;
    })
    .expect("failed to set atime");

    assert_eq!(md.atime() as u64, secs);
    assert_eq!(md.atime_nsec() as u64, nanos);
}

#[test]
fn set_dir_mode() {
    let mode = 0o640;
    let md = set_attr_test(SetAttrKind::Directory, |tsetattr| {
        tsetattr.valid = P9_SETATTR_MODE;
        tsetattr.mode = mode;
    })
    .expect("failed to set mode");

    assert_eq!(md.mode() & 0o777, mode);
}

#[test]
fn set_dir_mtime() {
    let (secs, nanos) = (1245247825, 524617);
    let md = set_attr_test(SetAttrKind::Directory, |tsetattr| {
        tsetattr.valid = P9_SETATTR_MTIME | P9_SETATTR_MTIME_SET;
        tsetattr.mtime_sec = secs;
        tsetattr.mtime_nsec = nanos;
    })
    .expect("failed to set mtime");

    assert_eq!(md.mtime() as u64, secs);
    assert_eq!(md.mtime_nsec() as u64, nanos);
}

#[test]
fn set_dir_atime() {
    let (secs, nanos) = (9247605, 4016);
    let md = set_attr_test(SetAttrKind::Directory, |tsetattr| {
        tsetattr.valid = P9_SETATTR_ATIME | P9_SETATTR_ATIME_SET;
        tsetattr.atime_sec = secs;
        tsetattr.atime_nsec = nanos;
    })
    .expect("failed to set atime");

    assert_eq!(md.atime() as u64, secs);
    assert_eq!(md.atime_nsec() as u64, nanos);
}

#[test]
fn huge_directory() {
    let (test_dir, mut server) = setup("huge_directory");

    let name = "newdir";
    let newdir = test_dir.join(name);
    fs::create_dir(&newdir).expect("failed to create directory");

    let dfid = ROOT_FID + 1;
    walk(
        &mut server,
        &*test_dir,
        ROOT_FID,
        dfid,
        vec![String::from(name)],
    );

    // Create ~4K files in the directory and then attempt to read them all.
    let mut filenames = HashSet::with_capacity(4096);
    for i in 0..4096 {
        let name = format!("file_{}", i);
        create_local_file(&newdir, &name);
        assert!(filenames.insert(name));
    }

    let fid = dfid + 1;
    open(&mut server, &newdir, dfid, "", fid, P9_DIRECTORY).expect("Failed to open directory");
    for f in readdir(&mut server, fid) {
        let path = newdir.join(&f.name);

        let md = fs::symlink_metadata(path).expect("failed to get metadata for path");
        check_qid(&f.qid, &md);

        if f.name == "." || f.name == ".." {
            assert_eq!(f.ty, libc::DT_DIR);
        } else {
            assert_eq!(f.ty, libc::DT_REG);
            assert!(filenames.remove(&f.name));
        }
    }

    assert!(filenames.is_empty());
}

#[test]
fn mkdir() {
    let (test_dir, mut server) = setup("mkdir");

    let name = "conan";
    let tmkdir = Tmkdir {
        dfid: ROOT_FID,
        name: String::from(name),
        mode: 0o755,
        gid: 0,
    };

    let rmkdir = server.mkdir(&tmkdir).expect("failed to create directory");
    let md =
        fs::symlink_metadata(test_dir.join(name)).expect("failed to get metadata for directory");

    assert!(md.is_dir());
    check_qid(&rmkdir.qid, &md);
}

#[test]
fn unlink_all() {
    let (test_dir, mut server) = setup("readdir");

    let mut next_fid = ROOT_FID + 1;

    let mut dirs = VecDeque::new();
    dirs.push_back((ROOT_FID, test_dir.to_path_buf()));

    // First iterate over the whole directory.
    let mut unlinks = VecDeque::new();
    while let Some((dfid, dir)) = dirs.pop_front() {
        let mut names = VecDeque::new();
        for entry in fs::read_dir(dir).expect("failed to read directory") {
            let entry = entry.expect("unable to iterate over directory");
            let ft = entry
                .file_type()
                .expect("failed to get file type for entry");
            if ft.is_dir() {
                let fid = next_fid;
                next_fid += 1;

                let wnames: Vec<String> = entry
                    .path()
                    .strip_prefix(&test_dir)
                    .expect("test directory is not prefix of subdir")
                    .components()
                    .map(|c| Path::new(&c).to_string_lossy().to_string())
                    .collect();
                walk(&mut server, &*test_dir, ROOT_FID, fid, wnames);
                dirs.push_back((fid, entry.path()));
            }

            names.push_back((
                entry
                    .file_name()
                    .into_string()
                    .expect("failed to convert entry name to string"),
                if ft.is_dir() {
                    libc::AT_REMOVEDIR as u32
                } else {
                    0
                },
            ));
        }

        unlinks.push_back((dfid, names));
    }

    // Now remove everything in reverse order.
    while let Some((dfid, names)) = unlinks.pop_back() {
        for (name, flags) in names {
            let tunlinkat = Tunlinkat {
                dirfd: dfid,
                name,
                flags,
            };

            server.unlink_at(&tunlinkat).expect("failed to unlink path");
        }
    }
}

#[test]
fn rename_at() {
    let (test_dir, mut server) = setup("rename");

    let name = "oldfile";
    let content = create_local_file(&test_dir, name);

    let newname = "newfile";
    let trename = Trenameat {
        olddirfid: ROOT_FID,
        oldname: String::from(name),
        newdirfid: ROOT_FID,
        newname: String::from(newname),
    };

    server.rename_at(&trename).expect("failed to rename file");

    assert!(!test_dir.join(name).exists());

    let mut newcontent = Vec::with_capacity(content.len());
    let size = File::open(test_dir.join(newname))
        .expect("failed to open file")
        .read_to_end(&mut newcontent)
        .expect("failed to read new file content");
    assert_eq!(size, content.len());
    assert_eq!(newcontent, content);
}

fn setlk_tlock(fid: u32, len: u64, start: u64, type_: i32) -> Tlock {
    Tlock {
        fid,
        type_: type_ as u8,
        flags: 0,
        start,
        length: len,
        proc_id: SERVER_PID,
        client_id: String::from("test-server"),
    }
}

fn getlk_tgetlock(fid: u32, type_: i32) -> Tgetlock {
    Tgetlock {
        fid,
        type_: type_ as u8,
        start: 0,
        length: 0,
        proc_id: SERVER_PID,
        client_id: String::from("test-server"),
    }
}

fn setup_simple_lock_no_open() -> Server {
    let (test_dir, server) = setup("simple lock");

    let filename = "file";
    create_local_file(&test_dir, filename);

    server
}

fn setup_simple_lock(flags: u32) -> Server {
    let (test_dir, mut server) = setup("simple lock");

    let filename = "file";
    create_local_file(&test_dir, filename);

    open(
        &mut server,
        &*test_dir,
        ROOT_FID,
        filename,
        ROOT_FID + 1,
        flags,
    )
    .expect("failed to open file");

    server
}

#[test]
fn lock_rdlck_no_open_file() {
    let mut server = setup_simple_lock_no_open();

    let tlock = setlk_tlock(ROOT_FID + 1, 8, 0, libc::F_RDLCK);

    server.lock(&tlock).expect_err("Bad file descriptor");
}

#[test]
fn lock_rdlck() {
    let mut server = setup_simple_lock(P9_RDWR);

    let tlock = setlk_tlock(ROOT_FID + 1, 8, 0, libc::F_RDLCK);

    server.lock(&tlock).expect("failed to lock file");
}
#[test]
fn lock_wrlck_no_open_file() {
    let mut server = setup_simple_lock_no_open();

    let tlock = setlk_tlock(ROOT_FID + 1, 8, 0, libc::F_WRLCK);

    server.lock(&tlock).expect_err("Bad file descriptor");
}
#[test]
fn lock_wrlck() {
    let mut server = setup_simple_lock(P9_RDWR);

    let tlock = setlk_tlock(ROOT_FID + 1, 8, 0, libc::F_WRLCK);

    server.lock(&tlock).expect("failed to lock file");
}

#[test]
fn lock_unlck_no_lock() {
    let mut server = setup_simple_lock(P9_RDWR);

    let tlock = setlk_tlock(ROOT_FID + 1, 0, 0, libc::F_UNLCK);

    server.lock(&tlock).expect("failed to lock file");
}

#[test]
fn lock_unlck() {
    let mut server = setup_simple_lock(P9_RDWR);

    let tlock = setlk_tlock(ROOT_FID + 1, LOCAL_FILE_LEN / 2, 0, libc::F_RDLCK);

    server.lock(&tlock).expect("failed to lock file");

    let tlock = setlk_tlock(ROOT_FID + 1, 0, 0, libc::F_UNLCK);

    server.lock(&tlock).expect("failed to lock file");
}

#[test]
fn lock_unlck_relock() {
    let mut server = setup_simple_lock(P9_RDWR);

    let tlock = setlk_tlock(ROOT_FID + 1, LOCAL_FILE_LEN / 2, 0, libc::F_RDLCK);

    server.lock(&tlock).expect("failed to lock file");

    let tlock = setlk_tlock(ROOT_FID + 1, 0, 0, libc::F_UNLCK);

    server.lock(&tlock).expect("failed to lock file");

    let tlock = setlk_tlock(ROOT_FID + 1, LOCAL_FILE_LEN / 2, 0, libc::F_RDLCK);

    server.lock(&tlock).expect("failed to lock file");
}

#[test]
fn getlock_rdlck_nolock() {
    let mut server = setup_simple_lock(P9_RDWR);

    let tgetlock = getlk_tgetlock(ROOT_FID + 1, libc::F_RDLCK);

    server
        .get_lock(&tgetlock)
        .expect("failed to get lock on file");
}

#[test]
fn getlock_wrlck() {
    let mut server = setup_simple_lock(P9_RDWR);

    let tlock = setlk_tlock(ROOT_FID + 1, LOCAL_FILE_LEN / 2, 0, libc::F_WRLCK);

    server.lock(&tlock).expect("failed to lock file");

    let tgetlock = getlk_tgetlock(ROOT_FID + 1, libc::F_WRLCK);

    server
        .get_lock(&tgetlock)
        .expect("failed to get lock on file");
}

#[test]
fn getlock_rdlck() {
    let mut server = setup_simple_lock(P9_RDWR);

    let tlock = setlk_tlock(ROOT_FID + 1, LOCAL_FILE_LEN / 2, 0, libc::F_RDLCK);

    server.lock(&tlock).expect("failed to lock file");

    let tgetlock = getlk_tgetlock(ROOT_FID + 1, libc::F_RDLCK);

    server
        .get_lock(&tgetlock)
        .expect("failed to get lock on file");
}

macro_rules! open_test {
    ($name:ident, $flags:expr) => {
        #[test]
        fn $name() {
            let (test_dir, mut server) = setup("open");

            let fid = ROOT_FID + 1;
            let name = "test.txt";
            let content = create_local_file(&test_dir, name);

            let rlopen = open(&mut server, &*test_dir, ROOT_FID, name, fid, $flags as u32)
                .expect("failed to open file");

            let md =
                fs::symlink_metadata(test_dir.join(name)).expect("failed to get metadata for file");
            check_qid(&rlopen.qid, &md);
            assert_eq!(rlopen.iounit, 0);

            check_attr(&mut server, fid, &md);

            // Check that the file has the proper contents as long as we didn't
            // truncate it first.
            if $flags & P9_TRUNC == 0 && $flags & P9_WRONLY == 0 {
                check_content(&mut server, &content, fid);
            }

            // Check that we can write to the file.
            if $flags & P9_RDWR != 0 || $flags & P9_WRONLY != 0 {
                write(&mut server, &test_dir, name, fid, $flags);
            }

            let tclunk = Tclunk { fid };
            server.clunk(&tclunk).expect("Unable to clunk file");
        }
    };
    ($name:ident, $flags:expr, $expected_err:expr) => {
        #[test]
        fn $name() {
            let (test_dir, mut server) = setup("open_fail");

            let fid = ROOT_FID + 1;
            let name = "test.txt";
            create_local_file(&test_dir, name);

            let err = open(&mut server, &*test_dir, ROOT_FID, name, fid, $flags as u32)
                .expect_err("successfully opened file");
            assert_eq!(err.kind(), $expected_err);

            let tclunk = Tclunk { fid };
            server.clunk(&tclunk).expect("Unable to clunk file");
        }
    };
}

open_test!(read_only_file_open, P9_RDONLY);
open_test!(read_write_file_open, P9_RDWR);
open_test!(write_only_file_open, P9_WRONLY);

open_test!(create_read_only_file_open, P9_CREATE | P9_RDONLY);
open_test!(create_read_write_file_open, P9_CREATE | P9_RDWR);
open_test!(create_write_only_file_open, P9_CREATE | P9_WRONLY);

open_test!(append_read_only_file_open, P9_APPEND | P9_RDONLY);
open_test!(append_read_write_file_open, P9_APPEND | P9_RDWR);
open_test!(append_write_only_file_open, P9_APPEND | P9_WRONLY);

open_test!(trunc_read_only_file_open, P9_TRUNC | P9_RDONLY);
open_test!(trunc_read_write_file_open, P9_TRUNC | P9_RDWR);
open_test!(trunc_write_only_file_open, P9_TRUNC | P9_WRONLY);

open_test!(
    create_append_read_only_file_open,
    P9_CREATE | P9_APPEND | P9_RDONLY
);
open_test!(
    create_append_read_write_file_open,
    P9_CREATE | P9_APPEND | P9_RDWR
);
open_test!(
    create_append_wronly_file_open,
    P9_CREATE | P9_APPEND | P9_WRONLY
);

open_test!(
    create_trunc_read_only_file_open,
    P9_CREATE | P9_TRUNC | P9_RDONLY
);
open_test!(
    create_trunc_read_write_file_open,
    P9_CREATE | P9_TRUNC | P9_RDWR
);
open_test!(
    create_trunc_wronly_file_open,
    P9_CREATE | P9_TRUNC | P9_WRONLY
);

open_test!(
    append_trunc_read_only_file_open,
    P9_APPEND | P9_TRUNC | P9_RDONLY
);
open_test!(
    append_trunc_read_write_file_open,
    P9_APPEND | P9_TRUNC | P9_RDWR
);
open_test!(
    append_trunc_wronly_file_open,
    P9_APPEND | P9_TRUNC | P9_WRONLY
);

open_test!(
    create_append_trunc_read_only_file_open,
    P9_CREATE | P9_APPEND | P9_TRUNC | P9_RDONLY
);
open_test!(
    create_append_trunc_read_write_file_open,
    P9_CREATE | P9_APPEND | P9_TRUNC | P9_RDWR
);
open_test!(
    create_append_trunc_wronly_file_open,
    P9_CREATE | P9_APPEND | P9_TRUNC | P9_WRONLY
);

open_test!(
    create_excl_read_only_file_open,
    P9_CREATE | P9_EXCL | P9_RDONLY,
    io::ErrorKind::AlreadyExists
);
open_test!(
    create_excl_read_write_file_open,
    P9_CREATE | P9_EXCL | P9_RDWR,
    io::ErrorKind::AlreadyExists
);
open_test!(
    create_excl_wronly_file_open,
    P9_CREATE | P9_EXCL | P9_WRONLY,
    io::ErrorKind::AlreadyExists
);

macro_rules! create_test {
    ($name:ident, $flags:expr, $mode:expr) => {
        #[test]
        fn $name() {
            let (test_dir, mut server) = setup("create");

            let name = "foo.txt";
            let fid = ROOT_FID + 1;
            let rlcreate = create(&mut server, &*test_dir, ROOT_FID, fid, name, $flags, $mode)
                .expect("failed to create file");

            let md =
                fs::symlink_metadata(test_dir.join(name)).expect("failed to get metadata for file");
            assert_eq!(rlcreate.iounit, 0);
            check_qid(&rlcreate.qid, &md);
            check_attr(&mut server, fid, &md);

            // Check that we can write to the file.
            if $flags & P9_RDWR != 0 || $flags & P9_WRONLY != 0 {
                write(&mut server, &test_dir, name, fid, $flags);
            }

            let tclunk = Tclunk { fid };
            server.clunk(&tclunk).expect("Unable to clunk file");
        }
    };
    ($name:ident, $flags:expr, $mode:expr, $expected_err:expr) => {
        #[test]
        fn $name() {
            let (test_dir, mut server) = setup("create_fail");

            let name = "foo.txt";
            // The `fid` in the lcreate call initially points to the directory
            // but is supposed to point to the newly created file after the call
            // completes.  Duplicate the fid so that we don't end up consuming the
            // root fid.
            let fid = ROOT_FID + 1;
            let err = create(&mut server, &*test_dir, ROOT_FID, fid, name, $flags, $mode)
                .expect_err("successfully created file");
            assert_eq!(err.kind(), $expected_err);
        }
    };
}

create_test!(read_only_file_create, P9_RDONLY, 0o600u32);
create_test!(read_write_file_create, P9_RDWR, 0o600u32);
create_test!(write_only_file_create, P9_WRONLY, 0o600u32);

create_test!(
    append_read_only_file_create,
    P9_APPEND | P9_RDONLY,
    0o600u32
);
create_test!(append_read_write_file_create, P9_APPEND | P9_RDWR, 0o600u32);
create_test!(append_wronly_file_create, P9_APPEND | P9_WRONLY, 0o600u32);

#[test]
fn lcreate_set_len() {
    let (test_dir, mut server) = setup("lcreate_set_len");

    let name = "foo.txt";
    let fid = ROOT_FID + 1;
    create(
        &mut server,
        &*test_dir,
        ROOT_FID,
        fid,
        name,
        P9_RDWR,
        0o600u32,
    )
    .expect("failed to create file");

    let tsetattr = Tsetattr {
        fid,
        valid: 0x8, // P9_SETATTR_SIZE
        size: 100,
        // The other fields are not used because the relevant flags aren't set in `valid`.
        mode: 0,
        uid: 0,
        gid: 0,
        atime_sec: 0,
        atime_nsec: 0,
        mtime_sec: 0,
        mtime_nsec: 0,
    };
    server
        .set_attr(&tsetattr)
        .expect("failed to set file length after lcreate");

    let tclunk = Tclunk { fid };
    server.clunk(&tclunk).expect("Unable to clunk file");
}

#[test]
fn readlink() {
    let (test_dir, mut server) = setup("readlink");
    create_local_symlink(&test_dir, "symlink", "target/of/symlink");

    let fid = ROOT_FID + 1;
    walk(
        &mut server,
        &*test_dir,
        ROOT_FID,
        fid,
        vec!["symlink".into()],
    );

    let treadlink = Treadlink { fid };

    let rreadlink = server.readlink(&treadlink).expect("failed to readlink");

    assert_eq!(rreadlink.target, "target/of/symlink");
}
