//! ## Localhost Host Bridge
//!
//! Implements the host bridge abstraction directly against the local
//! filesystem.

use std::fs::{self, OpenOptions};
use std::io::{Read, Write};
#[cfg(posix)]
use std::os::unix::fs::PermissionsExt as _;
use std::path::{Path, PathBuf};

use filetime::FileTime;
use remotefs::File;
use remotefs::fs::{FileType, Metadata, UnixPex};

use super::{HostBridge, HostResult};
use crate::host::{HostError, HostErrorType};
use crate::utils::path;

/// Returns whether `a` and `b` resolve to the same filesystem path.
/// Falls back to comparing the raw paths when canonicalization fails
/// (e.g. one of the paths doesn't exist).
fn same_file(a: &Path, b: &Path) -> bool {
    match (a.canonicalize(), b.canonicalize()) {
        (Ok(a), Ok(b)) => a == b,
        _ => a == b,
    }
}

/// Localhost is the entity which holds the information about the current directory and host.
/// It provides functions to navigate across the local host file system
pub struct Localhost {
    wrkdir: PathBuf,
    files: Vec<File>,
}

impl Localhost {
    /// Instantiates a new Localhost struct
    pub fn new(wrkdir: PathBuf) -> HostResult<Self> {
        debug!("Initializing localhost at {}", wrkdir.display());
        let mut host: Localhost = Localhost {
            wrkdir,
            files: Vec::new(),
        };
        // Check if dir exists
        let pwd = host.pwd()?;

        if !host.exists(&pwd)? {
            error!(
                "Failed to initialize localhost: {} doesn't exist",
                host.wrkdir.display()
            );
            return Err(HostError::new(
                HostErrorType::NoSuchFileOrDirectory,
                None,
                host.wrkdir.as_path(),
            ));
        }
        // Retrieve files for provided path
        host.files = match host.list_dir(&pwd) {
            Ok(files) => files,
            Err(err) => {
                error!(
                    "Failed to initialize localhost: could not scan wrkdir: {}",
                    err
                );
                return Err(err);
            }
        };
        info!("Localhost initialized with success");
        Ok(host)
    }

    /// Convert path to absolute path
    fn to_path(&self, p: &Path) -> PathBuf {
        path::absolutize(self.wrkdir.as_path(), p)
    }
}

impl HostBridge for Localhost {
    fn connect(&mut self) -> HostResult<()> {
        Ok(())
    }

    fn disconnect(&mut self) -> HostResult<()> {
        Ok(())
    }

    fn is_connected(&mut self) -> bool {
        true
    }

    fn is_localhost(&self) -> bool {
        true
    }

    fn pwd(&mut self) -> HostResult<PathBuf> {
        Ok(self.wrkdir.clone())
    }

    fn change_wrkdir(&mut self, new_dir: &std::path::Path) -> HostResult<PathBuf> {
        let new_dir: PathBuf = self.to_path(new_dir);
        info!("Changing localhost directory to {}...", new_dir.display());
        // Check whether directory exists
        if !self.exists(new_dir.as_path())? {
            error!("Could not change directory: No such file or directory");
            return Err(HostError::new(
                HostErrorType::NoSuchFileOrDirectory,
                None,
                new_dir.as_path(),
            ));
        }
        // Change directory
        if let Err(err) = std::env::set_current_dir(new_dir.as_path()) {
            error!("Could not enter directory: {}", err);
            return Err(HostError::new(
                HostErrorType::NoSuchFileOrDirectory,
                Some(err),
                new_dir.as_path(),
            ));
        }
        let prev_dir: PathBuf = self.wrkdir.clone(); // Backup location
        // Update working directory
        // Change dir
        self.wrkdir = new_dir;
        // Scan new directory
        let pwd = self.pwd()?;
        self.files = match self.list_dir(&pwd) {
            Ok(files) => files,
            Err(err) => {
                error!("Could not scan new directory: {}", err);
                // Restore directory
                self.wrkdir = prev_dir;
                return Err(err);
            }
        };
        debug!("Changed directory to {}", self.wrkdir.display());
        Ok(self.wrkdir.clone())
    }

    fn mkdir_ex(&mut self, dir_name: &std::path::Path, ignore_existing: bool) -> HostResult<()> {
        let dir_path: PathBuf = self.to_path(dir_name);
        info!("Making directory {}", dir_path.display());
        // If dir already exists, return Error
        if dir_path.exists() {
            match ignore_existing {
                true => return Ok(()),
                false => {
                    return Err(HostError::new(
                        HostErrorType::FileAlreadyExists,
                        None,
                        dir_path.as_path(),
                    ));
                }
            }
        }
        match std::fs::create_dir(dir_path.as_path()) {
            Ok(_) => {
                // Update dir
                if dir_name.is_relative() {
                    let pwd = self.pwd()?;
                    self.files = self.list_dir(&pwd)?;
                }
                info!("Created directory {}", dir_path.display());
                Ok(())
            }
            Err(err) => {
                error!("Could not make directory: {}", err);
                Err(HostError::new(
                    HostErrorType::CouldNotCreateFile,
                    Some(err),
                    dir_path.as_path(),
                ))
            }
        }
    }

    fn remove(&mut self, entry: &File) -> HostResult<()> {
        if entry.is_dir() {
            // If file doesn't exist; return error
            debug!("Removing directory {}", entry.path().display());
            if !entry.path().exists() {
                error!("Directory doesn't exist");
                return Err(HostError::new(
                    HostErrorType::NoSuchFileOrDirectory,
                    None,
                    entry.path(),
                ));
            }
            // Remove
            match std::fs::remove_dir_all(entry.path()) {
                Ok(_) => {
                    // Update dir
                    let pwd = self.pwd()?;
                    self.files = self.list_dir(&pwd)?;
                    info!("Removed directory {}", entry.path().display());
                    Ok(())
                }
                Err(err) => {
                    error!("Could not remove directory: {}", err);
                    Err(HostError::new(
                        HostErrorType::DeleteFailed,
                        Some(err),
                        entry.path(),
                    ))
                }
            }
        } else {
            // If file doesn't exist; return error
            debug!("Removing file {}", entry.path().display());
            if !entry.path().exists() {
                error!("File doesn't exist");
                return Err(HostError::new(
                    HostErrorType::NoSuchFileOrDirectory,
                    None,
                    entry.path(),
                ));
            }
            // Remove
            match std::fs::remove_file(entry.path()) {
                Ok(_) => {
                    // Update dir
                    let pwd = self.pwd()?;
                    self.files = self.list_dir(&pwd)?;
                    info!("Removed file {}", entry.path().display());
                    Ok(())
                }
                Err(err) => {
                    error!("Could not remove file: {}", err);
                    Err(HostError::new(
                        HostErrorType::DeleteFailed,
                        Some(err),
                        entry.path(),
                    ))
                }
            }
        }
    }

    fn rename(&mut self, entry: &File, dst_path: &std::path::Path) -> HostResult<()> {
        match std::fs::rename(entry.path(), dst_path) {
            Ok(_) => {
                // Scan dir
                let pwd = self.pwd()?;
                self.files = self.list_dir(&pwd)?;
                debug!(
                    "Moved file {} to {}",
                    entry.path().display(),
                    dst_path.display()
                );
                Ok(())
            }
            Err(err) => {
                error!(
                    "Failed to move {} to {}: {}",
                    entry.path().display(),
                    dst_path.display(),
                    err
                );
                Err(HostError::new(
                    HostErrorType::CouldNotCreateFile,
                    Some(err),
                    entry.path(),
                ))
            }
        }
    }

    fn copy(&mut self, entry: &File, dst: &std::path::Path) -> HostResult<()> {
        // Get absolute path of dest
        let dst: PathBuf = self.to_path(dst);
        info!(
            "Copying file {} to {}",
            entry.path().display(),
            dst.display()
        );
        // Match entry
        if entry.is_dir() {
            // If destination path doesn't exist, create destination
            if !dst.exists() {
                debug!("Directory {} doesn't exist; creating it", dst.display());
                self.mkdir(dst.as_path())?;
            }
            // Scan dir
            let dir_files: Vec<File> = self.list_dir(entry.path())?;
            // Iterate files
            for dir_entry in dir_files.iter() {
                // Calculate dst
                let mut sub_dst: PathBuf = dst.clone();
                sub_dst.push(dir_entry.name());
                // Call function recursively
                self.copy(dir_entry, sub_dst.as_path())?;
            }
        } else {
            // Copy file
            // If destination path is a directory, push file name
            let dst: PathBuf = match dst.as_path().is_dir() {
                true => {
                    let mut p: PathBuf = dst.clone();
                    p.push(entry.name().as_str());
                    p
                }
                false => dst.clone(),
            };
            // Refuse to copy a file onto itself; std::fs::copy would otherwise
            // truncate the source to 0 bytes (#421, e.g. empty destination).
            if same_file(entry.path(), dst.as_path()) {
                error!("Refusing to copy file onto itself: {}", dst.display());
                return Err(HostError::new(
                    HostErrorType::CouldNotCreateFile,
                    None,
                    dst.as_path(),
                ));
            }
            // Copy entry path to dst path
            if let Err(err) = std::fs::copy(entry.path(), dst.as_path()) {
                error!("Failed to copy file: {}", err);
                return Err(HostError::new(
                    HostErrorType::CouldNotCreateFile,
                    Some(err),
                    entry.path(),
                ));
            }
            info!("File copied");
        }
        // Reload directory if dst is pwd
        let pwd = self.pwd()?;
        match dst.is_dir() {
            true => {
                if dst == pwd {
                    self.files = self.list_dir(&pwd)?;
                } else if let Some(parent) = dst.parent() {
                    // If parent is pwd, scan directory
                    if parent == pwd {
                        self.files = self.list_dir(&pwd)?;
                    }
                }
            }
            false => {
                if let Some(parent) = dst.parent() {
                    // If parent is pwd, scan directory
                    if parent == pwd {
                        self.files = self.list_dir(&pwd)?;
                    }
                }
            }
        }
        Ok(())
    }

    fn stat(&mut self, path: &std::path::Path) -> HostResult<File> {
        info!("Stating file {}", path.display());
        let path: PathBuf = self.to_path(path);
        let attr = match fs::metadata(path.as_path()) {
            Ok(metadata) => metadata,
            Err(err) => {
                error!("Could not read file metadata: {}", err);
                return Err(HostError::new(
                    HostErrorType::FileNotAccessible,
                    Some(err),
                    path.as_path(),
                ));
            }
        };
        let mut metadata = Metadata::from(attr);
        if let Ok(symlink) = fs::read_link(path.as_path()) {
            metadata.set_symlink(symlink);
            metadata.file_type = FileType::Symlink;
        }
        // Match dir / file
        Ok(File { path, metadata })
    }

    fn exists(&mut self, path: &Path) -> HostResult<bool> {
        let path: PathBuf = self.to_path(path);

        Ok(path.exists())
    }

    fn list_dir(&mut self, path: &Path) -> HostResult<Vec<File>> {
        info!("Reading directory {}", path.display());
        match std::fs::read_dir(path) {
            Ok(e) => {
                let mut fs_entries: Vec<File> = Vec::new();
                for entry in e.flatten() {
                    // NOTE: 0.4.1, don't fail if stat for one file fails
                    match self.stat(entry.path().as_path()) {
                        Ok(entry) => fs_entries.push(entry),
                        Err(e) => error!("Failed to stat {}: {}", entry.path().display(), e),
                    }
                }
                Ok(fs_entries)
            }
            Err(err) => Err(HostError::new(
                HostErrorType::DirNotAccessible,
                Some(err),
                path,
            )),
        }
    }

    fn setstat(&mut self, path: &Path, metadata: &Metadata) -> HostResult<()> {
        debug!("Setting stat for file at {}", path.display());
        if let Some(mtime) = metadata.modified {
            let mtime = FileTime::from_system_time(mtime);
            debug!("setting mtime {:?}", mtime);
            filetime::set_file_mtime(path, mtime)
                .map_err(|e| HostError::new(HostErrorType::FileNotAccessible, Some(e), path))?;
        }
        if let Some(atime) = metadata.accessed {
            let atime = FileTime::from_system_time(atime);
            filetime::set_file_atime(path, atime)
                .map_err(|e| HostError::new(HostErrorType::FileNotAccessible, Some(e), path))?;
        }
        #[cfg(posix)]
        if let Some(mode) = metadata.mode {
            self.chmod(path, mode)?;
        }
        Ok(())
    }

    fn exec(&mut self, cmd: &str) -> HostResult<String> {
        // Make command
        let args: Vec<&str> = cmd.split(' ').collect();
        let cmd: &str = match args.first() {
            Some(cmd) => cmd,
            None => {
                error!("Empty command provided to exec");
                return Err(HostError::new(
                    HostErrorType::ExecutionFailed,
                    None,
                    self.wrkdir.as_path(),
                ));
            }
        };
        let argv: &[&str] = &args[1..];
        info!("Executing command: {} {:?}", cmd, argv);
        match std::process::Command::new(cmd).args(argv).output() {
            Ok(output) => match std::str::from_utf8(&output.stdout) {
                Ok(s) => {
                    info!("Command output: {}", s);
                    Ok(s.to_string())
                }
                Err(_) => Ok(String::new()),
            },
            Err(err) => {
                error!("Failed to run command: {}", err);
                Err(HostError::new(
                    HostErrorType::ExecutionFailed,
                    Some(err),
                    self.wrkdir.as_path(),
                ))
            }
        }
    }

    #[cfg(posix)]
    fn symlink(&mut self, src: &Path, dst: &Path) -> HostResult<()> {
        let src = self.to_path(src);
        std::os::unix::fs::symlink(dst, src.as_path()).map_err(|e| {
            error!(
                "Failed to create symlink at {} pointing at {}: {}",
                src.display(),
                dst.display(),
                e
            );
            HostError::new(HostErrorType::CouldNotCreateFile, Some(e), src.as_path())
        })
    }

    #[cfg(win)]
    fn symlink(&mut self, _src: &Path, _dst: &Path) -> HostResult<()> {
        warn!("Cannot create symlink on Windows");

        Err(HostError::from(HostErrorType::NotImplemented))
    }

    #[cfg(posix)]
    fn chmod(&mut self, path: &std::path::Path, pex: UnixPex) -> HostResult<()> {
        let path: PathBuf = self.to_path(path);
        // Get metadta
        match fs::metadata(path.as_path()) {
            Ok(metadata) => {
                let mut mpex = metadata.permissions();
                mpex.set_mode(pex.into());
                match fs::set_permissions(path.as_path(), mpex) {
                    Ok(_) => {
                        info!("Changed mode for {} to {:?}", path.display(), pex);
                        Ok(())
                    }
                    Err(err) => {
                        error!("Could not change mode for file {}: {}", path.display(), err);
                        Err(HostError::new(
                            HostErrorType::FileNotAccessible,
                            Some(err),
                            path.as_path(),
                        ))
                    }
                }
            }
            Err(err) => {
                error!(
                    "Chmod failed; could not read metadata for file {}: {}",
                    path.display(),
                    err
                );
                Err(HostError::new(
                    HostErrorType::FileNotAccessible,
                    Some(err),
                    path.as_path(),
                ))
            }
        }
    }

    #[cfg(win)]
    fn chmod(&mut self, _path: &std::path::Path, _pex: UnixPex) -> HostResult<()> {
        warn!("Cannot set file mode on Windows");

        Err(HostError::from(HostErrorType::NotImplemented))
    }

    fn open_file(&mut self, file: &std::path::Path) -> HostResult<Box<dyn Read + Send>> {
        let file: PathBuf = self.to_path(file);
        info!("Opening file {} for read", file.display());
        if !self.exists(file.as_path())? {
            error!("File doesn't exist!");
            return Err(HostError::new(
                HostErrorType::NoSuchFileOrDirectory,
                None,
                file.as_path(),
            ));
        }
        match OpenOptions::new()
            .create(false)
            .read(true)
            .write(false)
            .open(file.as_path())
        {
            Ok(f) => Ok(Box::new(f)),
            Err(err) => {
                error!("Could not open file for read: {}", err);
                Err(HostError::new(
                    HostErrorType::FileNotAccessible,
                    Some(err),
                    file.as_path(),
                ))
            }
        }
    }

    fn create_file(
        &mut self,
        file: &Path,
        _metadata: &Metadata,
    ) -> HostResult<Box<dyn Write + Send>> {
        let file: PathBuf = self.to_path(file);
        info!("Opening file {} for write", file.display());
        match OpenOptions::new()
            .create(true)
            .write(true)
            .truncate(true)
            .open(file.as_path())
        {
            Ok(f) => Ok(Box::new(f)),
            Err(err) => {
                error!("Failed to open file: {}", err);
                match self.exists(file.as_path())? {
                    true => Err(HostError::new(
                        HostErrorType::ReadonlyFile,
                        Some(err),
                        file.as_path(),
                    )),
                    false => Err(HostError::new(
                        HostErrorType::FileNotAccessible,
                        Some(err),
                        file.as_path(),
                    )),
                }
            }
        }
    }

    fn finalize_write(&mut self, _writer: Box<dyn Write + Send>) -> HostResult<()> {
        // no-op
        Ok(())
    }
}

#[cfg(test)]
mod tests {

    #[cfg(posix)]
    use std::fs::File as StdFile;
    #[cfg(posix)]
    use std::io::Write;
    use std::ops::AddAssign;
    #[cfg(posix)]
    use std::os::unix::fs::{PermissionsExt, symlink};
    use std::time::{Duration, SystemTime};

    use pretty_assertions::assert_eq;

    use super::*;
    use crate::utils::test_helpers::create_sample_file;
    #[cfg(posix)]
    use crate::utils::test_helpers::make_file_at;
    #[cfg(posix)]
    use crate::utils::test_helpers::make_fsentry;

    #[test]
    fn test_host_error_new() {
        let error: HostError =
            HostError::new(HostErrorType::CouldNotCreateFile, None, Path::new("/tmp"));
        assert!(error.ioerr.is_none());
        assert_eq!(error.path.as_ref().unwrap(), Path::new("/tmp"));
    }

    #[test]
    #[cfg(posix)]
    #[cfg(not(feature = "isolated-tests"))]
    fn test_host_localhost_new() {
        let host: Localhost = Localhost::new(PathBuf::from("/dev")).ok().unwrap();
        assert_eq!(host.wrkdir, PathBuf::from("/dev"));
        // Scan dir
        let entries = std::fs::read_dir(PathBuf::from("/dev").as_path()).unwrap();
        let mut counter: usize = 0;
        for _ in entries {
            counter += 1;
        }
        assert_eq!(host.files.len(), counter);
    }

    #[test]
    #[cfg(win)]
    fn test_host_localhost_new() {
        let host: Localhost = Localhost::new(PathBuf::from("C:\\users")).ok().unwrap();
        assert_eq!(host.wrkdir, PathBuf::from("C:\\users"));
        // Scan dir
        let entries = std::fs::read_dir(PathBuf::from("C:\\users").as_path()).unwrap();
        let mut counter: usize = 0;
        for _ in entries {
            counter += 1;
        }
        assert_eq!(host.files.len(), counter);
    }

    #[test]
    #[should_panic]
    fn test_host_localhost_new_bad() {
        Localhost::new(PathBuf::from("/omargabber/123/345"))
            .ok()
            .unwrap();
    }

    #[test]
    #[cfg(posix)]
    fn test_host_localhost_pwd() {
        let mut host: Localhost = Localhost::new(PathBuf::from("/dev")).ok().unwrap();
        assert_eq!(host.pwd().unwrap(), PathBuf::from("/dev"));
    }

    #[test]
    #[cfg(posix)]
    #[cfg(not(feature = "isolated-tests"))]
    fn test_host_localhost_change_dir() {
        let mut host: Localhost = Localhost::new(PathBuf::from("/dev")).ok().unwrap();
        let new_dir: PathBuf = PathBuf::from("/dev");
        assert!(host.change_wrkdir(new_dir.as_path()).is_ok());
        // Verify new files
        // Scan dir
        let entries = std::fs::read_dir(new_dir.as_path()).unwrap();
        let mut counter: usize = 0;
        for _ in entries {
            counter += 1;
        }
        assert_eq!(host.files.len(), counter);
    }

    #[test]
    #[cfg(posix)]
    #[should_panic]
    fn test_host_localhost_change_dir_failed() {
        let mut host: Localhost = Localhost::new(PathBuf::from("/dev")).ok().unwrap();
        let new_dir: PathBuf = PathBuf::from("/omar/gabber/123/456");
        assert!(host.change_wrkdir(new_dir.as_path()).is_ok());
    }

    #[test]
    #[cfg(posix)]
    fn test_host_localhost_open_read() {
        let mut host: Localhost = Localhost::new(PathBuf::from("/dev")).ok().unwrap();
        // Create temp file
        let file: tempfile::NamedTempFile = create_sample_file();
        assert!(host.open_file(file.path()).is_ok());
    }

    #[test]
    #[cfg(posix)]
    #[should_panic]
    fn test_host_localhost_open_read_err_no_such_file() {
        let mut host: Localhost = Localhost::new(PathBuf::from("/dev")).ok().unwrap();
        assert!(
            host.open_file(PathBuf::from("/bin/foo-bar-test-omar-123-456-789.txt").as_path())
                .is_ok()
        );
    }

    #[test]
    #[cfg(any(target_os = "macos", target_os = "linux"))]
    fn test_host_localhost_open_read_err_not_accessible() {
        let mut host: Localhost = Localhost::new(PathBuf::from("/dev")).ok().unwrap();
        let file: tempfile::NamedTempFile = create_sample_file();
        //let mut perms = fs::metadata(file.path())?.permissions();
        fs::set_permissions(file.path(), PermissionsExt::from_mode(0o222)).unwrap();
        //fs::set_permissions(file.path(), perms)?;
        assert!(host.open_file(file.path()).is_err());
    }

    #[test]
    #[cfg(posix)]
    fn test_host_localhost_open_write() {
        let mut host: Localhost = Localhost::new(PathBuf::from("/dev")).ok().unwrap();
        // Create temp file
        let file: tempfile::NamedTempFile = create_sample_file();
        assert!(host.create_file(file.path(), &Metadata::default()).is_ok());
    }

    #[test]
    #[cfg(any(target_os = "macos", target_os = "linux"))]
    fn test_host_localhost_open_write_err() {
        let mut host: Localhost = Localhost::new(PathBuf::from("/dev")).ok().unwrap();
        let file: tempfile::NamedTempFile = create_sample_file();
        //let mut perms = fs::metadata(file.path())?.permissions();
        fs::set_permissions(file.path(), PermissionsExt::from_mode(0o444)).unwrap();
        //fs::set_permissions(file.path(), perms)?;
        assert!(host.create_file(file.path(), &Metadata::default()).is_err());
    }

    #[test]
    #[cfg(posix)]
    fn should_resolve_relative_exists_and_stat_from_workdir() {
        let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap();
        let dir_path: &Path = tmpdir.path();
        assert!(make_file_at(dir_path, "nested.txt").is_ok());
        let mut host: Localhost = Localhost::new(PathBuf::from(dir_path)).ok().unwrap();

        assert!(host.exists(Path::new("nested.txt")).unwrap());
        assert!(!host.exists(Path::new("missing.txt")).unwrap());

        let entry = host.stat(Path::new("nested.txt")).unwrap();
        assert_eq!(entry.path(), &dir_path.join("nested.txt"));
        assert!(entry.is_file());
    }

    #[cfg(posix)]
    #[test]
    fn test_host_localhost_symlinks() {
        let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap();
        // Create sample file
        assert!(StdFile::create(format!("{}/foo.txt", tmpdir.path().display()).as_str()).is_ok());
        // Create symlink
        assert!(
            symlink(
                format!("{}/foo.txt", tmpdir.path().display()),
                format!("{}/bar.txt", tmpdir.path().display())
            )
            .is_ok()
        );
        // Get dir
        let host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap();
        let files: Vec<File> = host.files.clone();
        // Verify files
        let file_0: &File = files.first().unwrap();
        if file_0.name() == *"foo.txt" {
            assert!(file_0.metadata.symlink.is_none());
        } else {
            assert_eq!(
                file_0.metadata.symlink.as_ref().unwrap(),
                &PathBuf::from(format!("{}/foo.txt", tmpdir.path().display()))
            );
        }
        // Verify simlink
        let file_1: &File = files.get(1).unwrap();
        if file_1.name() == *"bar.txt" {
            assert_eq!(
                file_1.metadata.symlink.as_ref().unwrap(),
                &PathBuf::from(format!("{}/foo.txt", tmpdir.path().display()))
            );
        } else {
            assert!(file_1.metadata.symlink.is_none());
        }
    }

    #[test]
    #[cfg(posix)]
    fn test_host_localhost_mkdir() {
        let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap();
        let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap();
        let files: Vec<File> = host.files.clone();
        assert_eq!(files.len(), 0); // There should be 0 files now
        assert!(host.mkdir(PathBuf::from("test_dir").as_path()).is_ok());
        let files: Vec<File> = host.files.clone();
        assert_eq!(files.len(), 1); // There should be 1 file now
        // Try to re-create directory
        assert!(host.mkdir(PathBuf::from("test_dir").as_path()).is_err());
        // Try abs path
        assert!(
            host.mkdir_ex(PathBuf::from("/tmp/test_dir_123456789").as_path(), true)
                .is_ok()
        );
        // Fail
        assert!(
            host.mkdir_ex(
                PathBuf::from("/aaaa/oooooo/tmp/test_dir_123456789").as_path(),
                true
            )
            .is_err()
        );
    }

    #[test]
    #[cfg(posix)]
    fn test_host_localhost_remove() {
        let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap();
        // Create sample file
        assert!(StdFile::create(format!("{}/foo.txt", tmpdir.path().display()).as_str()).is_ok());
        let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap();
        let files: Vec<File> = host.files.clone();
        assert_eq!(files.len(), 1); // There should be 1 file now
        // Remove file
        assert!(host.remove(files.first().unwrap()).is_ok());
        // There should be 0 files now
        let files: Vec<File> = host.files.clone();
        assert_eq!(files.len(), 0); // There should be 0 files now
        // Create directory
        assert!(host.mkdir(PathBuf::from("test_dir").as_path()).is_ok());
        // Delete directory
        let files: Vec<File> = host.files.clone();
        assert_eq!(files.len(), 1); // There should be 1 file now
        assert!(host.remove(files.first().unwrap()).is_ok());
        // Remove unexisting directory
        assert!(
            host.remove(&make_fsentry(PathBuf::from("/a/b/c/d"), true))
                .is_err()
        );
        assert!(
            host.remove(&make_fsentry(PathBuf::from("/aaaaaaa"), false))
                .is_err()
        );
    }

    #[test]
    #[cfg(posix)]
    fn test_host_localhost_rename() {
        let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap();
        // Create sample file
        let src_path: PathBuf =
            PathBuf::from(format!("{}/foo.txt", tmpdir.path().display()).as_str());
        assert!(StdFile::create(src_path.as_path()).is_ok());
        let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap();
        let files: Vec<File> = host.files.clone();
        assert_eq!(files.len(), 1); // There should be 1 file now
        assert_eq!(files.first().unwrap().name(), "foo.txt");
        // Rename file
        let dst_path: PathBuf =
            PathBuf::from(format!("{}/bar.txt", tmpdir.path().display()).as_str());
        assert!(
            host.rename(files.first().unwrap(), dst_path.as_path())
                .is_ok()
        );
        // There should be still 1 file now, but named bar.txt
        let files: Vec<File> = host.files.clone();
        assert_eq!(files.len(), 1); // There should be 0 files now
        assert_eq!(files.first().unwrap().name(), "bar.txt");
        // Fail
        let bad_path: PathBuf = PathBuf::from("/asdailsjoidoewojdijow/ashdiuahu");
        assert!(
            host.rename(files.first().unwrap(), bad_path.as_path())
                .is_err()
        );
    }

    #[test]
    fn should_setstat() {
        let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap();
        let file: tempfile::NamedTempFile = create_sample_file();
        let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap();
        // stat
        let mut filemeta = host.stat(file.path()).unwrap();

        let mut new_atime = SystemTime::UNIX_EPOCH;
        new_atime.add_assign(Duration::from_secs(1612164210));

        let mut new_mtime = SystemTime::UNIX_EPOCH;
        new_mtime.add_assign(Duration::from_secs(1613160210));

        filemeta.metadata.accessed = Some(new_atime);
        filemeta.metadata.modified = Some(new_mtime);

        // setstat
        assert!(host.setstat(filemeta.path(), filemeta.metadata()).is_ok());
        let new_metadata = host.stat(file.path()).unwrap();

        assert_eq!(new_metadata.metadata().accessed, Some(new_atime));
        assert_eq!(new_metadata.metadata().modified, Some(new_mtime));
    }

    #[cfg(posix)]
    #[test]
    fn test_host_chmod() {
        let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap();
        let file: tempfile::NamedTempFile = create_sample_file();
        let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap();
        // Chmod to file
        assert!(host.chmod(file.path(), UnixPex::from(0o755)).is_ok());
        // Chmod to dir
        assert!(host.chmod(tmpdir.path(), UnixPex::from(0o750)).is_ok());
        // Error
        assert!(
            host.chmod(
                Path::new("/tmp/krgiogoiegj/kwrgnoerig"),
                UnixPex::from(0o777)
            )
            .is_err()
        );
    }

    #[cfg(posix)]
    #[test]
    fn test_host_copy_file_absolute() {
        let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap();
        // Create file in tmpdir
        let mut file1_path: PathBuf = PathBuf::from(tmpdir.path());
        file1_path.push("foo.txt");
        // Write file 1
        let mut file1 = StdFile::create(file1_path.as_path()).ok().unwrap();
        assert!(file1.write_all(b"Hello world!\n").is_ok());
        // Get file 2 path
        let mut file2_path: PathBuf = PathBuf::from(tmpdir.path());
        file2_path.push("bar.txt");
        // Create host
        let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap();
        let file1_entry: File = host.files.first().unwrap().clone();
        assert_eq!(file1_entry.name(), String::from("foo.txt"));
        // Copy
        assert!(host.copy(&file1_entry, file2_path.as_path()).is_ok());
        // Verify host has two files
        assert_eq!(host.files.len(), 2);
        // Fail copy
        assert!(
            host.copy(
                &make_fsentry(PathBuf::from("/a/a7/a/a7a"), false),
                PathBuf::from("571k422i").as_path()
            )
            .is_err()
        );
    }

    #[cfg(posix)]
    #[test]
    fn test_host_copy_file_relative() {
        let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap();
        // Create file in tmpdir
        let mut file1_path: PathBuf = PathBuf::from(tmpdir.path());
        file1_path.push("foo.txt");
        // Write file 1
        let mut file1 = StdFile::create(file1_path.as_path()).ok().unwrap();
        assert!(file1.write_all(b"Hello world!\n").is_ok());
        // Get file 2 path
        let file2_path: PathBuf = PathBuf::from("bar.txt");
        // Create host
        let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap();
        let file1_entry: File = host.files.first().unwrap().clone();
        assert_eq!(file1_entry.name(), String::from("foo.txt"));
        // Copy
        assert!(host.copy(&file1_entry, file2_path.as_path()).is_ok());
        // Verify host has two files
        assert_eq!(host.files.len(), 2);
    }

    #[cfg(posix)]
    #[test]
    fn test_host_should_not_truncate_file_when_copying_to_empty_dest() {
        // Regression test for #421: copying a file with an empty destination
        // must not empty (truncate to 0KB) the original file.
        let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap();
        let mut file1_path: PathBuf = PathBuf::from(tmpdir.path());
        file1_path.push("foo.txt");
        let mut file1 = StdFile::create(file1_path.as_path()).ok().unwrap();
        assert!(file1.write_all(b"Hello world!\n").is_ok());
        // Create host
        let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap();
        let file1_entry: File = host.files.first().unwrap().clone();
        assert_eq!(file1_entry.name(), String::from("foo.txt"));
        // Copy with empty destination -> must fail and leave file untouched
        assert!(
            host.copy(&file1_entry, PathBuf::from("").as_path())
                .is_err()
        );
        // Original file must still hold its content
        assert_eq!(
            std::fs::read(file1_path.as_path()).unwrap(),
            b"Hello world!\n"
        );
    }

    #[cfg(posix)]
    #[test]
    fn test_host_copy_directory_absolute() {
        let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap();
        // Create directory in tmpdir
        let mut dir_src: PathBuf = PathBuf::from(tmpdir.path());
        dir_src.push("test_dir/");
        assert!(std::fs::create_dir(dir_src.as_path()).is_ok());
        // Create file in src dir
        let mut file1_path: PathBuf = dir_src.clone();
        file1_path.push("foo.txt");
        // Write file 1
        let mut file1 = StdFile::create(file1_path.as_path()).ok().unwrap();
        assert!(file1.write_all(b"Hello world!\n").is_ok());
        // Copy dir src to dir ddest
        let mut dir_dest: PathBuf = PathBuf::from(tmpdir.path());
        dir_dest.push("test_dest_dir/");
        // Create host
        let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap();
        let dir_src_entry: File = host.files.first().unwrap().clone();
        assert_eq!(dir_src_entry.name(), String::from("test_dir"));
        // Copy
        assert!(host.copy(&dir_src_entry, dir_dest.as_path()).is_ok());
        // Verify host has two files
        assert_eq!(host.files.len(), 2);
        // Verify dir_dest contains foo.txt
        let mut test_file_path: PathBuf = dir_dest.clone();
        test_file_path.push("foo.txt");
        assert!(host.stat(test_file_path.as_path()).is_ok());
    }

    #[cfg(posix)]
    #[test]
    fn test_host_copy_directory_relative() {
        let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap();
        // Create directory in tmpdir
        let mut dir_src: PathBuf = PathBuf::from(tmpdir.path());
        dir_src.push("test_dir/");
        assert!(std::fs::create_dir(dir_src.as_path()).is_ok());
        // Create file in src dir
        let mut file1_path: PathBuf = dir_src.clone();
        file1_path.push("foo.txt");
        // Write file 1
        let mut file1 = StdFile::create(file1_path.as_path()).ok().unwrap();
        assert!(file1.write_all(b"Hello world!\n").is_ok());
        // Copy dir src to dir ddest
        let dir_dest: PathBuf = PathBuf::from("test_dest_dir/");
        // Create host
        let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap();
        let dir_src_entry: File = host.files.first().unwrap().clone();
        assert_eq!(dir_src_entry.name(), String::from("test_dir"));
        // Copy
        assert!(host.copy(&dir_src_entry, dir_dest.as_path()).is_ok());
        // Verify host has two files
        assert_eq!(host.files.len(), 2);
        // Verify dir_dest contains foo.txt
        let mut test_file_path: PathBuf = dir_dest.clone();
        test_file_path.push("foo.txt");
        assert!(host.stat(test_file_path.as_path()).is_ok());
    }

    #[test]
    fn test_host_exec() {
        let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap();
        let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap();
        // Execute
        assert!(host.exec("echo 5").ok().unwrap().as_str().contains("5"));
    }

    #[test]
    fn test_host_exec_empty_command() {
        let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap();
        let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap();
        // Execute empty command should return error
        assert!(host.exec("").is_err());
    }

    #[cfg(posix)]
    #[test]
    fn should_create_symlink() {
        let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap();
        let dir_path: &Path = tmpdir.path();
        // Make file
        assert!(make_file_at(dir_path, "pippo.txt").is_ok());
        let mut host: Localhost = Localhost::new(PathBuf::from(dir_path)).ok().unwrap();
        let mut p = dir_path.to_path_buf();
        p.push("pippo.txt");
        // Make symlink
        assert!(host.symlink(Path::new("link.txt"), p.as_path()).is_ok());
        // Fail symlink
        assert!(host.symlink(Path::new("link.txt"), p.as_path()).is_err());
        assert!(
            host.symlink(Path::new("/tmp/oooo/aaaa"), p.as_path())
                .is_err()
        );
    }

    #[cfg(posix)]
    #[test]
    fn should_stat_relative_symlink_as_symlink() {
        let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap();
        let dir_path: &Path = tmpdir.path();
        assert!(make_file_at(dir_path, "target.txt").is_ok());
        let mut host: Localhost = Localhost::new(PathBuf::from(dir_path)).ok().unwrap();
        let target = dir_path.join("target.txt");

        assert!(
            host.symlink(Path::new("link.txt"), target.as_path())
                .is_ok()
        );

        let entry = host.stat(Path::new("link.txt")).unwrap();
        assert_eq!(entry.path(), &dir_path.join("link.txt"));
        assert!(entry.is_symlink());
        assert_eq!(entry.metadata().symlink.as_ref(), Some(&target));
    }

    #[test]
    fn test_host_fmt_error() {
        let err: HostError = HostError::new(
            HostErrorType::CouldNotCreateFile,
            Some(std::io::Error::from(std::io::ErrorKind::AddrInUse)),
            Path::new("/tmp"),
        );
        assert_eq!(
            format!("{err}"),
            String::from("Could not create file: address in use (/tmp)"),
        );
        assert_eq!(
            format!("{}", HostError::from(HostErrorType::DeleteFailed)),
            String::from("Could not delete file")
        );
        assert_eq!(
            format!("{}", HostError::from(HostErrorType::ExecutionFailed)),
            String::from("Command execution failed"),
        );
        assert_eq!(
            format!("{}", HostError::from(HostErrorType::DirNotAccessible)),
            String::from("Could not access directory"),
        );
        assert_eq!(
            format!("{}", HostError::from(HostErrorType::NoSuchFileOrDirectory)),
            String::from("No such file or directory")
        );
        assert_eq!(
            format!("{}", HostError::from(HostErrorType::ReadonlyFile)),
            String::from("File is readonly")
        );
        assert_eq!(
            format!("{}", HostError::from(HostErrorType::FileNotAccessible)),
            String::from("Could not access file")
        );
        assert_eq!(
            format!("{}", HostError::from(HostErrorType::FileAlreadyExists)),
            String::from("File already exists")
        );
    }
}
