use yggdrasil_abi::{
    error::Error,
    io::{AccessMode, FileMode, GroupId, OpenOptions, RemoveFlags, UserId},
    path::{Path, PathBuf},
};

use crate::vfs::{
    node::{AccessToken, CreateInfo},
    FileRef, Filename, NodeRef,
};

use super::{node::CreateFileType, Node};

/// Describes a general filesystem access
pub enum Action {
    /// Access involves reading data without modification
    Read,
    /// Access involves writing file data or modifying directory content
    Write,
    /// Access involves traversing a path or opening a file for execution
    Execute,
}

/// Contains the state of program's I/O context: current working directory, UID, GID, umask, etc.
#[derive(Clone)]
pub struct IoContext {
    uid: UserId,
    gid: GroupId,
    umask: FileMode,
    cwd_node: NodeRef,
    cwd_path: PathBuf,
    root: NodeRef,
}

impl IoContext {
    /// Constructs a new [IoContext] with given root node (which also becomes the cwd). By default,
    /// the root user/group is used. Default umask is 0o022.
    pub fn new(root: NodeRef) -> Self {
        Self {
            uid: UserId::root(),
            gid: GroupId::root(),
            umask: FileMode::new(0o022),
            cwd_node: root.clone(),
            cwd_path: PathBuf::from("/"),
            root,
        }
    }

    /// "Clones" an I/O context the way it is inherited by a newly spawned child process
    pub fn inherit(other: &IoContext) -> Self {
        other.clone()
    }

    /// Returns the root node of the [IoContext]
    pub fn root(&self) -> &NodeRef {
        &self.root
    }

    /// Returns the current working directory node of the [IoContext]
    pub fn cwd(&self) -> &NodeRef {
        &self.cwd_node
    }

    /// Sets the current user ID of the context. Returns [Error::PermissionDenied] if current user
    /// is not root.
    pub fn set_uid(&mut self, uid: UserId) -> Result<(), Error> {
        if uid != self.uid && !self.uid.is_root() {
            Err(Error::PermissionDenied)
        } else {
            self.uid = uid;
            Ok(())
        }
    }

    #[cfg(test)]
    fn set_uid_unchecked(&mut self, uid: UserId) {
        self.uid = uid;
    }

    /// Updates the context's permission mask
    pub fn set_umask(&mut self, mask: FileMode) {
        self.umask = mask;
    }

    /// Returns the context's permission mask
    pub fn umask(&self) -> FileMode {
        self.umask
    }

    /// Sets the current group ID of the context. Returns [Error::PermissionDenied] if current user
    /// is not root.
    pub fn set_gid(&mut self, gid: GroupId) -> Result<(), Error> {
        if gid != self.gid && !self.uid.is_root() {
            Err(Error::PermissionDenied)
        } else {
            self.gid = gid;
            Ok(())
        }
    }

    #[cfg(test)]
    fn set_gid_unchecked(&mut self, gid: GroupId) {
        self.gid = gid;
    }

    /// Checks if the current user can access given [crate::Node] and, if so, returns an
    /// [AccessToken].
    pub fn check_access(&self, node: &NodeRef, mode: AccessMode) -> Result<AccessToken, Error> {
        let metadata = node.metadata()?;

        if mode.contains(AccessMode::READ) && !metadata.is_readable(self.uid, self.gid) {
            return Err(Error::PermissionDenied);
        }

        if mode.contains(AccessMode::WRITE) && !metadata.is_writeable(self.uid, self.gid) {
            return Err(Error::PermissionDenied);
        }

        if mode.contains(AccessMode::EXEC) && !metadata.is_executable(self.uid, self.gid) {
            return Err(Error::PermissionDenied);
        }

        Ok(unsafe { AccessToken::authorized() })
    }

    /// Changes current working directory to `path`. Will fail if access is denied or the path does
    /// not point to a directory.
    pub fn set_cwd<P: AsRef<Path>>(&mut self, path: P) -> Result<(), Error> {
        let path = path.as_ref();
        let (path, node) = self.normalize(path, true)?;
        if !node.is_directory() {
            return Err(Error::NotADirectory);
        }
        self.cwd_path = path;
        self.cwd_node = node;
        Ok(())
    }

    /// Returns the current working directory path
    pub fn cwd_path(&self) -> &Path {
        self.cwd_path.as_ref()
    }

    /// Makes a directory at given path become a "mountpoint" for the given filesystem root.
    /// When accessed, the target directory will return contents of the filesystem root instead of
    /// its own. Both the `target` path and `fs_root` Node must be directories.
    pub fn mount<P: AsRef<Path>, F: FnOnce() -> Result<NodeRef, Error>>(
        &mut self,
        target: P,
        create_fs: F,
    ) -> Result<(), Error> {
        if !self.uid.is_root() {
            return Err(Error::PermissionDenied);
        }

        let target = target.as_ref();
        if !target.is_absolute() {
            return Err(Error::InvalidArgument);
        }
        let target_node = self._find(self.root.clone(), target.trim_start_separators(), true)?;

        // Replace the root
        if target_node.is_root() {
            self.root = create_fs()?;
            Ok(())
        } else {
            target_node.mount(create_fs)
        }
    }

    /// Locates a [crate::Node] at given path and opens it with requested access options. If no
    /// such node exists and `OpenOptions::CREATE` is specified, will attempt to create a regular
    /// file node at given path and then open it.
    pub fn open<P: AsRef<Path>>(
        &mut self,
        at: Option<NodeRef>,
        path: P,
        opts: OpenOptions,
        mode: FileMode,
    ) -> Result<FileRef, Error> {
        let path = path.as_ref();
        let node = match self.find(at.clone(), path, true) {
            Ok(node) => node,
            Err(Error::DoesNotExist) if opts.contains(OpenOptions::CREATE) => {
                // let create_mode = mode & !self.umask;
                let (parent, name) = path.split_right();
                let parent = self.find(at, parent, true)?;
                let filename = Filename::new(name)?;
                let access = self.check_access(&parent, AccessMode::WRITE)?;
                let create_info = CreateInfo {
                    name: filename,
                    mode: mode & !self.umask,
                    uid: self.uid,
                    gid: self.gid,
                    ty: CreateFileType::File,
                };
                parent.create(create_info, access)?
            }
            Err(err) => return Err(err),
        };

        // If not read/write is requested, access is granted
        let mut access = unsafe { AccessToken::authorized() };

        if opts.contains(OpenOptions::READ) {
            access += self.check_access(&node, AccessMode::READ)?;
        }
        if opts.contains(OpenOptions::WRITE) {
            access += self.check_access(&node, AccessMode::WRITE)?;
        }

        node.open(opts, access)
    }

    pub fn open_executable<P: AsRef<Path>>(&mut self, path: P) -> Result<FileRef, Error> {
        let node = self.find(None, path, true)?;
        let access = self.check_access(&node, AccessMode::EXEC)?;
        node.open(OpenOptions::READ, access)
    }

    /// Creates a directory a given path
    pub fn create_directory<P: AsRef<Path>>(
        &mut self,
        at: Option<NodeRef>,
        path: P,
        mode: FileMode,
    ) -> Result<NodeRef, Error> {
        let path = path.as_ref();
        let (parent, name) = path.split_right();
        let parent = self.find(at, parent, true)?;
        let access = self.check_access(&parent, AccessMode::WRITE)?;
        let filename = Filename::new(name)?;
        let create_info = CreateInfo {
            ty: CreateFileType::Directory,
            name: filename,
            uid: self.uid,
            gid: self.gid,
            mode: mode & !self.umask,
        };

        parent.create(create_info, access)
    }

    pub fn create_symlink<P: AsRef<Path>, Q: AsRef<Path>>(
        &mut self,
        at: Option<NodeRef>,
        target: P,
        path: Q,
        mode: FileMode,
    ) -> Result<NodeRef, Error> {
        let target = target.as_ref();
        let path = path.as_ref();
        let (parent, name) = path.split_right();
        let parent = self.find(at, parent, true)?;
        let access = self.check_access(&parent, AccessMode::WRITE)?;
        let filename = Filename::new(name)?;
        let create_info = CreateInfo {
            ty: CreateFileType::Symlink(target),
            name: filename,
            uid: self.uid,
            gid: self.gid,
            mode: mode & !self.umask,
        };

        parent.create(create_info, access)
    }

    /// Creates an arbitrary node at given path
    pub fn create_node<P: AsRef<Path>>(
        &mut self,
        at: Option<NodeRef>,
        path: P,
        node: NodeRef,
    ) -> Result<(), Error> {
        let path = path.as_ref();
        let (parent, name) = path.split_right();
        let parent = self.find(at, parent, true)?;
        let access = self.check_access(&parent, AccessMode::WRITE)?;

        let filename = Filename::new(name)?;
        parent.create_node(node, filename, access)?;

        Ok(())
    }

    pub fn rename(
        &mut self,
        src_at: Option<NodeRef>,
        src: impl AsRef<Path>,
        dst_at: Option<NodeRef>,
        dst: impl AsRef<Path>,
    ) -> Result<(), Error> {
        let src = src.as_ref();
        let dst = dst.as_ref();

        let (dst_parent, dst_name) = dst.split_dirname();
        let (_, src_name) = src.split_dirname();

        if src_name.is_empty() {
            log::warn!("Unhandled src move path: {src:?}");
            return Err(Error::InvalidArgument);
        }

        let src_node = self.find(src_at, src, false)?;
        if src_node.is_root() {
            log::warn!("Cannot move root path");
            return Err(Error::InvalidArgument);
        }
        let src_parent = src_node.parent();

        // Try to find destination node first
        let (dst_node, dst_name) = if let Ok(node) = self.find(dst_at.clone(), dst, false) {
            // There is a node with the destination name, but it's not a directory
            if !node.is_directory() {
                return Err(Error::AlreadyExists);
            }

            // mv /a/b /c if /c exists -> move /a/b into /c with filename b
            (node, src_name)
        } else {
            // Otherwise try looking up a parent
            let node = self.find(dst_at, dst_parent, true)?;
            if !node.is_directory() {
                return Err(Error::NotADirectory);
            }

            // mv /a/b /c/d if /c/d doesn't exist -> move /a/b into /c with filename d
            (node, dst_name)
        };

        let src_name = Filename::new(src_name)?;
        let dst_name = Filename::new(dst_name)?;

        let access = self.check_access(&src_node, AccessMode::READ | AccessMode::WRITE)?
            + self.check_access(&dst_node, AccessMode::WRITE)?;

        Node::move_node(&src_parent, &dst_node, src_name, dst_name, access)?;

        Ok(())
    }

    /// Removes a device or regular file node at given path
    pub fn remove<P: AsRef<Path>>(
        &mut self,
        at: Option<NodeRef>,
        path: P,
        flags: RemoveFlags,
    ) -> Result<(), Error> {
        let path = path.as_ref();
        let (parent, name) = path.trim_end_separators().split_right();

        if name.is_empty() {
            log::warn!("Tried to remove weird path: {:?}", path);
            return Err(Error::DoesNotExist);
        }

        let parent = self.find(at, parent, false)?;
        let access = self.check_access(&parent, AccessMode::WRITE)?;
        let filename = Filename::new(name)?;

        let child = parent.lookup_or_load(filename, access.clone())?;

        if child.is_directory() {
            if !flags.contains_any(RemoveFlags::DIRECTORY | RemoveFlags::DIRECTORY_ONLY) {
                return Err(Error::IsADirectory);
            }
        } else if flags.contains_any(RemoveFlags::DIRECTORY_ONLY) {
            return Err(Error::NotADirectory);
        }

        parent.remove_file(filename, access)
    }

    /// Locates a [crate::Node] pointed to by given [Path]
    pub fn find<P: AsRef<Path>>(
        &self,
        at: Option<NodeRef>,
        path: P,
        follow_links: bool,
    ) -> Result<NodeRef, Error> {
        let mut path = path.as_ref();
        let at = if path.is_absolute() {
            path = path.trim_start_separators();
            self.root.clone()
        } else if let Some(at) = at {
            at
        } else {
            self.cwd_node.clone()
        };

        self._find(at, path, follow_links)
    }

    fn _resolve(&self, mut at: NodeRef, follow_links: bool) -> Result<NodeRef, Error> {
        loop {
            if let Some(target) = at.mountpoint_target() {
                at = target.clone();
                continue;
            }

            if follow_links {
                // Resolve the link
                match self.resolve_symbolic(&at) {
                    Ok((node, _)) => {
                        at = node;
                    }
                    // Not a link, not an error
                    Err(Error::InvalidFile) => break Ok(at),
                    Err(e) => return Err(e),
                }
                continue;
            }

            break Ok(at);
        }
    }

    fn resolve_symbolic(&self, at: &NodeRef) -> Result<(NodeRef, PathBuf), Error> {
        let token = self.check_access(at, AccessMode::READ)?;

        match at.read_symlink_path(token.clone()) {
            Ok(path) => {
                // Resolve the link to node
                let path = PathBuf::from(path);
                let target = if path.is_absolute() {
                    self._find(self.root.clone(), path.trim_start_separators(), false)?
                } else {
                    self._find(at.parent(), &path, false)?
                };
                Ok((target, path))
            }
            Err(e) => Err(e),
        }
    }

    fn visit(&self, mut at: NodeRef, follow_links: bool) -> Result<(NodeRef, PathBuf), Error> {
        let mut buffer = PathBuf::new();

        loop {
            if let Some(target) = at.mountpoint_target() {
                at = target;
                continue;
            }

            if follow_links {
                match self.resolve_symbolic(&at) {
                    // Absolute link
                    Ok((node, path)) if path.is_absolute() => {
                        log::debug!("Follow absolute link -> {path:?}");
                        buffer = path;
                        at = node;
                    }
                    // Relative link
                    Ok((node, path)) => {
                        buffer.push(path);
                        at = node;
                    }
                    // Not a link
                    Err(Error::InvalidFile) => {
                        break Ok((at, buffer));
                    }
                    Err(error) => break Err(error),
                }
            } else {
                break Ok((at, buffer));
            }
        }
    }

    fn normalize<P: AsRef<Path>>(
        &self,
        input: P,
        follow_link: bool,
    ) -> Result<(PathBuf, NodeRef), Error> {
        let path = input.as_ref();

        let (mut buffer, mut node) = if path.is_absolute() {
            (PathBuf::from_str("/"), self.root.clone())
        } else {
            (self.cwd_path.clone(), self.cwd_node.clone())
        };

        let mut path = path.trim_start_separators();

        while !path.is_empty() {
            let (name, tail) = path.split_left();

            let name = match name {
                Path::PARENT_NAME => {
                    // Pop the last element
                    buffer.pop();
                    node = node.parent();
                    if node.mountpoint_target().is_some() {
                        // Just exited root -> mountpoint, go up to mountpoint's parent
                        node = node.parent();
                    }
                    path = tail;
                    continue;
                }
                Path::SELF_NAME => {
                    path = tail;
                    continue;
                }
                name => name,
            };

            if !node.is_directory() {
                return Err(Error::NotADirectory);
            }

            let access = self.check_access(&node, AccessMode::EXEC)?;
            let filename = Filename::new(name)?;
            let child = node.lookup_or_load(filename, access)?.flatten_hardlink()?;

            let follow_links = follow_link || !tail.is_empty();

            let (child, resolved) = self.visit(child, follow_links)?;
            if resolved.is_empty() {
                buffer.push(name);
            } else if resolved.is_absolute() {
                buffer = resolved;
            } else {
                buffer.push(resolved);
            };
            node = child;
            path = tail;
        }

        Ok((buffer, node.flatten_hardlink()?))
    }

    fn _find(&self, mut at: NodeRef, mut path: &Path, follow_link: bool) -> Result<NodeRef, Error> {
        while !path.is_empty() {
            let (name, tail) = path.split_left();

            let name = match name {
                Path::SELF_NAME => {
                    path = tail;
                    continue;
                }
                Path::PARENT_NAME => {
                    at = at.parent();
                    if at.mountpoint_target().is_some() {
                        // Just exited root -> mountpoint, go up to mountpoint's parent
                        at = at.parent();
                    }
                    path = tail;
                    continue;
                }
                name => name,
            };

            if !at.is_directory() {
                log::info!("at is not a dir");
                return Err(Error::NotADirectory);
            }

            let access = self.check_access(&at, AccessMode::EXEC)?;
            let filename = Filename::new(name)?;
            let child = at.lookup_or_load(filename, access)?.flatten_hardlink()?;
            let follow_links = follow_link || !tail.is_empty();

            let child = self._resolve(child, follow_links)?;

            at = child;
            path = tail;
        }

        at.flatten_hardlink()
    }
}

#[cfg(test)]
mod tests {
    use alloc::sync::Arc;
    use yggdrasil_abi::{
        error::Error,
        io::{FileMode, GroupId, OpenOptions, UserId},
        path::{Path, PathBuf},
    };

    use crate::vfs::{
        impls::{const_value_node, fixed_hardlink, fixed_path_symlink, mdir, value_node},
        node::AccessToken,
        Node, Read,
    };

    use super::IoContext;

    #[test_log::test]
    fn normalize() {
        // FS tree:
        // /already
        //      absolute
        //          inside
        //      relative
        //          nested
        //              item
        //          path
        //      link -> absolute
        //      deeplink -> relative/nested
        //      abslink -> /other/dir
        //      nestedlink -> abslink
        //      whatever
        //      mountpoint -> FS2
        //  /inroot
        //  /other
        //      dir
        //
        //  FS2 tree:
        //  /fs2
        //      directory

        let fs2d0d0 = mdir::<&str, _>([]);
        let fs2d0 = mdir([("directory", fs2d0d0.clone())]);
        let fs2 = mdir([("fs2", fs2d0.clone())]);

        // already/absolute/inside
        let d0d0f0 = const_value_node("file1");
        // already/relative/path
        let d0d1f0 = const_value_node("file2");
        // already/relative/nested/item
        let d0d1d0f0 = const_value_node("xxx");
        // already/relative/nested
        let d0d1d0 = mdir([("item", d0d1d0f0.clone())]);
        // already/link
        let d0l0 = fixed_path_symlink("absolute");
        // already/deeplink
        let d0l1 = fixed_path_symlink("relative/nested");
        // already/abslink
        let d0l2 = fixed_path_symlink("/other/dir");
        // already/nestedlink
        let d0l3 = fixed_path_symlink("abslink");
        // already/whatever
        let d0f0 = const_value_node("file3");
        // already/absolute
        let d0d0 = mdir([("inside", d0d0f0.clone())]);
        // already/relative
        let d0d1 = mdir([("path", d0d1f0.clone()), ("nested", d0d1d0.clone())]);
        // already/mountpoint
        let d0m0 = mdir::<&str, _>([]);
        // already
        let d0 = mdir([
            ("absolute", d0d0.clone()),
            ("relative", d0d1.clone()),
            ("link", d0l0),
            ("deeplink", d0l1),
            ("whatever", d0f0),
            ("abslink", d0l2.clone()),
            ("nestedlink", d0l3.clone()),
            ("mountpoint", d0m0.clone()),
        ]);
        // dir
        let d1d0 = mdir::<&str, _>([]);
        // other
        let d1 = mdir([("dir", d1d0.clone())]);
        // inroot
        let f0 = const_value_node("file4");

        let root = mdir([
            ("already", d0.clone()),
            ("inroot", f0.clone()),
            ("other", d1.clone()),
        ]);

        let tests = [
            // Absolute paths
            ("/already/absolute", "/already/absolute", &d0d0),
            ("/already///absolute", "/already/absolute", &d0d0),
            ("/already/././absolute", "/already/absolute", &d0d0),
            (
                "/already/.././already/../already/absolute",
                "/already/absolute",
                &d0d0,
            ),
            // Relative paths
            ("relative/path", "/already/relative/path", &d0d1f0),
            ("./relative/path", "/already/relative/path", &d0d1f0),
            ("././relative/././path", "/already/relative/path", &d0d1f0),
            (
                "../../already/../../../../already/./relative/path",
                "/already/relative/path",
                &d0d1f0,
            ),
            ("../inroot", "/inroot", &f0),
            ("../../../inroot", "/inroot", &f0),
            // Path going through a link
            ("/already/link", "/already/absolute", &d0d0),
            ("/already/link/inside", "/already/absolute/inside", &d0d0f0),
            ("/already/link/..", "/already", &d0),
            ("/already/link/../link", "/already/absolute", &d0d0),
            ("link", "/already/absolute", &d0d0),
            ("link/..", "/already", &d0),
            // Path going through a link, pointing to a nested path
            ("/already/deeplink", "/already/relative/nested", &d0d1d0),
            ("/already/deeplink/..", "/already/relative", &d0d1),
            // Path going through a link, pointing to an absolute path
            ("/already/abslink", "/other/dir", &d1d0),
            ("/already/abslink/..", "/other", &d1),
            ("/already/abslink/../dir", "/other/dir", &d1d0),
            // Path going through a link, pointing to a link, pointing to an absolute path
            ("/already/nestedlink", "/other/dir", &d1d0),
            ("/already/nestedlink/.", "/other/dir", &d1d0),
            ("nestedlink/..", "/other", &d1),
            // Path going through a mountpoint
            ("/already/mountpoint", "/already/mountpoint", &fs2),
            ("/already/mountpoint/fs2", "/already/mountpoint/fs2", &fs2d0),
            ("/already/mountpoint/fs2/..", "/already/mountpoint", &fs2),
            ("/already/mountpoint/..", "/already", &d0),
        ];

        let mut ioctx = IoContext::new(root.clone());
        ioctx.cwd_node = d0.clone();
        ioctx.cwd_path = PathBuf::from_str("/already");
        ioctx
            .mount("/already/mountpoint", || Ok(fs2.clone()))
            .unwrap();

        for (input, expected, expected_node) in tests {
            let (output, node) = ioctx.normalize(input, true).unwrap();
            assert_eq!(output.as_str(), expected);
            assert!(Arc::ptr_eq(&node, expected_node));
        }
    }

    #[test_log::test]
    fn access() {
        let f1 = const_value_node("file1");
        let f2 = const_value_node("file2");
        let f3 = value_node("file3".to_owned());
        let root = mdir([("f1", f1.clone()), ("f2", f2.clone()), ("f3", f3.clone())]);

        let mut ioctx = IoContext::new(root.clone());

        let uid = unsafe { UserId::from_raw(1) };
        let gid = unsafe { GroupId::from_raw(1) };

        // 1:1
        ioctx.set_uid_unchecked(uid);
        ioctx.set_gid_unchecked(gid);

        // 1:0, 0444
        f1.set_access(
            Some(uid),
            None,
            Some(FileMode::new(0o444)),
            AccessToken::test_authorized(),
        )
        .unwrap();

        // 0:1, 0644
        f2.set_access(
            None,
            Some(gid),
            Some(FileMode::new(0o644)),
            AccessToken::test_authorized(),
        )
        .unwrap();

        // 1:1, 0644
        f3.set_access(
            Some(uid),
            Some(gid),
            Some(FileMode::new(0o644)),
            AccessToken::test_authorized(),
        )
        .unwrap();

        // f1, read-only
        ioctx
            .open(None, "/f1", OpenOptions::READ, FileMode::empty())
            .unwrap();
        // f1, write
        let err = ioctx
            .open(None, "/f1", OpenOptions::WRITE, FileMode::empty())
            .unwrap_err();
        assert_eq!(err, Error::PermissionDenied);

        // f2, read-only
        ioctx
            .open(None, "/f2", OpenOptions::READ, FileMode::empty())
            .unwrap();
        // f2, write
        let err = ioctx
            .open(None, "/f2", OpenOptions::WRITE, FileMode::empty())
            .unwrap_err();
        assert_eq!(err, Error::PermissionDenied);

        // f3, read-only
        ioctx
            .open(None, "/f3", OpenOptions::READ, FileMode::empty())
            .unwrap();
        // f3, write
        ioctx
            .open(None, "/f3", OpenOptions::WRITE, FileMode::empty())
            .unwrap();
    }

    #[test]
    fn cwd() {
        let d1_f1 = const_value_node("dir1-file1");
        let d1 = mdir([("f1", d1_f1.clone())]);
        let f1 = const_value_node("file1");
        let root = mdir([("f1", f1.clone()), ("d1", d1.clone())]);

        let mut ioctx = IoContext::new(root.clone());

        assert_eq!(ioctx.cwd_path(), Path::from_str("/"));

        let node = ioctx.find(None, "f1", true).unwrap();
        assert!(Arc::ptr_eq(&node, &f1));

        let node = ioctx.find(None, "d1/f1", true).unwrap();
        assert!(Arc::ptr_eq(&node, &d1_f1));

        let node = ioctx.find(None, "d1", true).unwrap();
        assert!(Arc::ptr_eq(&node, &d1));

        ioctx.set_cwd("/d1").unwrap();
        assert_eq!(ioctx.cwd_path(), Path::from_str("/d1"));

        let err = ioctx.find(None, "d1", true).unwrap_err();
        assert_eq!(err, Error::DoesNotExist);

        let node = ioctx.find(None, "f1", true).unwrap();
        assert!(Arc::ptr_eq(&node, &d1_f1));

        let node = ioctx.find(None, "../d1/f1", true).unwrap();
        assert!(Arc::ptr_eq(&node, &d1_f1));

        let node = ioctx.find(None, "/f1", true).unwrap();
        assert!(Arc::ptr_eq(&node, &f1));

        let node = ioctx.find(None, "../f1", true).unwrap();
        assert!(Arc::ptr_eq(&node, &f1));

        let node = ioctx.find(None, "..", true).unwrap();
        assert!(Arc::ptr_eq(&node, &root));
    }

    #[test]
    fn mount_resolution() {
        let root2_f1 = const_value_node("root2-file1");
        let root2 = mdir([("f1", root2_f1.clone())]);
        let root1_f1 = const_value_node("root1-file1");
        let root1 = mdir([("f1", root1_f1.clone())]);

        let mut ioctx = IoContext::new(root1.clone());

        let node = ioctx.find(None, "/f1", true).unwrap();
        assert!(Arc::ptr_eq(&node, &root1_f1));

        ioctx.mount("/", || Ok(root2.clone())).unwrap();

        let node = ioctx.find(None, "/f1", true).unwrap();
        assert!(Arc::ptr_eq(&node, &root2_f1));

        let node = ioctx.find(None, "/", true).unwrap();
        assert!(Arc::ptr_eq(&node, &root2));
    }

    #[test]
    fn symlink_resolution() {
        let f1 = const_value_node("file1");
        let l1 = fixed_path_symlink("f1");
        let root = mdir([("l1", l1.clone()), ("f1", f1.clone())]);

        let ioctx = IoContext::new(root.clone());

        // No follow
        let node = ioctx.find(None, "/l1", false).unwrap();
        assert!(Arc::ptr_eq(&node, &l1));

        // Follow
        let node = ioctx.find(None, "/l1", true).unwrap();
        assert!(Arc::ptr_eq(&node, &f1));
    }

    #[test]
    fn open_or_create() {
        let root_d1_f1 = const_value_node("dir1/file1");
        let root_f1 = const_value_node("file1");
        let root_d1 = mdir([("f1", root_d1_f1.clone())]);
        let root = mdir([("f1", root_f1.clone()), ("d1", root_d1.clone())]);

        let mut ioctx = IoContext::new(root.clone());
        let mut buf = [0; 512];

        let file = ioctx
            .open(None, "/d1/f1", OpenOptions::READ, FileMode::empty())
            .unwrap();
        assert!(Arc::ptr_eq(&file.node().unwrap(), &root_d1_f1));
        assert_eq!(file.read(&mut buf).unwrap(), 10);
        assert_eq!(&buf[..10], b"dir1/file1");
        drop(file);

        let file = ioctx
            .open(
                None,
                "/d1/f1",
                OpenOptions::READ | OpenOptions::CREATE,
                FileMode::empty(),
            )
            .unwrap();
        assert!(Arc::ptr_eq(&file.node().unwrap(), &root_d1_f1));
        assert_eq!(file.read(&mut buf).unwrap(), 10);
        assert_eq!(&buf[..10], b"dir1/file1");
        drop(file);

        let err = ioctx
            .open(
                None,
                "/d1/f2",
                OpenOptions::WRITE | OpenOptions::CREATE,
                FileMode::empty(),
            )
            .unwrap_err();
        assert_eq!(err, Error::ReadOnly);
    }

    #[test]
    fn hard_links() {
        // Hard link to file
        let f0 = const_value_node("test");
        let h0 = fixed_hardlink(f0.clone()).unwrap();
        let d0 = mdir([("f0", f0.clone()), ("h0", h0.clone())]);

        let ioctx = IoContext::new(d0.clone());

        let node = ioctx.find(None, "/f0", false).unwrap();
        let (path, n) = ioctx.normalize("/f0", false).unwrap();
        assert_eq!(path.as_str(), "/f0");
        assert!(Arc::ptr_eq(&n, &f0));
        assert!(Arc::ptr_eq(&node, &f0));
        let node = ioctx.find(None, "/h0", false).unwrap();
        let (path, n) = ioctx.normalize("/h0", false).unwrap();
        assert_eq!(path.as_str(), "/h0");
        assert!(Arc::ptr_eq(&n, &f0));
        assert!(Arc::ptr_eq(&node, &f0));

        // Hard link to directory
        let d1f0 = const_value_node("test");
        let d1 = mdir([("f0", d1f0.clone())]);
        let h0 = fixed_hardlink(d1f0.clone()).unwrap();
        let d0 = mdir([("d1", d1.clone()), ("h0", h0.clone())]);

        let ioctx = IoContext::new(d0.clone());

        let node = ioctx.find(None, "/d1/f0", false).unwrap();
        let (path, n) = ioctx.normalize("/d1/f0", false).unwrap();
        assert_eq!(path.as_str(), "/d1/f0");
        assert!(Arc::ptr_eq(&n, &d1f0));
        assert!(Arc::ptr_eq(&node, &d1f0));
        let node = ioctx.find(None, "/h0", false).unwrap();
        let (path, n) = ioctx.normalize("/h0", false).unwrap();
        assert_eq!(path.as_str(), "/h0");
        assert!(Arc::ptr_eq(&n, &d1f0));
        assert!(Arc::ptr_eq(&node, &d1f0));
    }

    #[test]
    fn find_tests() {
        let root_d1_f1 = const_value_node("dir1/file1");
        let root_f1 = const_value_node("file1");
        let root_d1 = mdir([("f1", root_d1_f1.clone())]);
        let root = mdir([("f1", root_f1.clone()), ("d1", root_d1.clone())]);

        let ioctx = IoContext::new(root.clone());

        // Ok paths
        let node = ioctx.find(None, Path::empty(), false).unwrap();
        assert!(Arc::ptr_eq(&node, &root));
        let node = ioctx.find(None, Path::from_str("/"), false).unwrap();
        assert!(Arc::ptr_eq(&node, &root));

        // Weird paths
        let node = ioctx
            .find(None, Path::from_str("////.////./"), false)
            .unwrap();
        assert!(Arc::ptr_eq(&node, &root));
        let node = ioctx
            .find(None, Path::from_str("/../.././//."), false)
            .unwrap();
        assert!(Arc::ptr_eq(&node, &root));

        let node = ioctx.find(None, Path::from_str("/f1"), false).unwrap();
        assert!(Arc::ptr_eq(&node, &root_f1));
        let node = ioctx.find(None, Path::from_str("/d1/f1"), false).unwrap();
        assert!(Arc::ptr_eq(&node, &root_d1_f1));
        let node = ioctx
            .find(None, Path::from_str("/d1/../d1/./f1"), false)
            .unwrap();
        assert!(Arc::ptr_eq(&node, &root_d1_f1));
        let node = ioctx.find(None, Path::from_str("/d1/.."), false).unwrap();
        assert!(Arc::ptr_eq(&node, &root));
    }
}
