use std::{
    fs, io,
    sync::atomic::{AtomicUsize, Ordering},
};

use paths::{Utf8Path, Utf8PathBuf};

pub(crate) struct TestDir {
    path: Utf8PathBuf,
    keep: bool,
}

impl TestDir {
    pub(crate) fn new() -> TestDir {
        TestDir::new_dir(false)
    }

    pub(crate) fn new_symlink() -> TestDir {
        TestDir::new_dir(true)
    }

    fn new_dir(symlink: bool) -> TestDir {
        let temp_dir = std::env::temp_dir();
        // On MacOS builders on GitHub actions, the temp dir is a symlink, and
        // that causes problems down the line. Specifically:
        // * Cargo may emit different PackageId depending on the working directory
        // * rust-analyzer may fail to map LSP URIs to correct paths.
        //
        // Work-around this by canonicalizing. Note that we don't want to do this
        // on *every* OS, as on windows `canonicalize` itself creates problems.
        #[cfg(target_os = "macos")]
        let temp_dir = temp_dir.canonicalize().unwrap();

        let base = temp_dir.join("testdir");
        let pid = std::process::id();

        static CNT: AtomicUsize = AtomicUsize::new(0);
        for _ in 0..100 {
            let cnt = CNT.fetch_add(1, Ordering::Relaxed);
            let path = base.join(format!("{pid}_{cnt}"));
            if path.is_dir() {
                continue;
            }
            fs::create_dir_all(&path).unwrap();

            #[cfg(any(
                target_os = "macos",
                target_os = "linux",
                target_os = "windows",
                target_os = "freebsd"
            ))]
            if symlink {
                let symlink_path = base.join(format!("{pid}_{cnt}_symlink"));
                #[cfg(any(target_os = "macos", target_os = "linux", target_os = "freebsd"))]
                std::os::unix::fs::symlink(path, &symlink_path).unwrap();

                #[cfg(target_os = "windows")]
                std::os::windows::fs::symlink_dir(path, &symlink_path).unwrap();

                return TestDir {
                    path: Utf8PathBuf::from_path_buf(symlink_path).unwrap(),
                    keep: false,
                };
            }

            return TestDir { path: Utf8PathBuf::from_path_buf(path).unwrap(), keep: false };
        }
        panic!("Failed to create a temporary directory")
    }

    #[allow(unused)]
    pub(crate) fn keep(mut self) -> TestDir {
        self.keep = true;
        self
    }
    pub(crate) fn path(&self) -> &Utf8Path {
        &self.path
    }
}

impl Drop for TestDir {
    fn drop(&mut self) {
        if self.keep {
            return;
        }

        let filetype = fs::symlink_metadata(&self.path).unwrap().file_type();
        let actual_path = filetype.is_symlink().then(|| fs::read_link(&self.path).unwrap());

        if let Some(actual_path) = actual_path {
            remove_dir_all(Utf8Path::from_path(&actual_path).unwrap()).unwrap_or_else(|err| {
                panic!(
                    "failed to remove temporary link to directory {}: {err}",
                    actual_path.display()
                )
            })
        }

        remove_dir_all(&self.path).unwrap_or_else(|err| {
            panic!("failed to remove temporary directory {}: {err}", self.path)
        });
    }
}

#[cfg(not(windows))]
fn remove_dir_all(path: &Utf8Path) -> io::Result<()> {
    fs::remove_dir_all(path)
}

#[cfg(windows)]
fn remove_dir_all(path: &Utf8Path) -> io::Result<()> {
    for _ in 0..99 {
        if fs::remove_dir_all(path).is_ok() {
            return Ok(());
        }
        std::thread::sleep(std::time::Duration::from_millis(10))
    }
    fs::remove_dir_all(path)
}
