use crate::fs;
use crate::fs::Stub;

use getset::Getters;
use glob::glob;
use std::path::{Path, PathBuf};
use tempfile::{tempdir, TempDir};

pub struct Playground {
    root: TempDir,
    tests: String,
    cwd: PathBuf,
}

#[derive(Getters)]
#[get = "pub"]
pub struct Dirs {
    pub root: PathBuf,
    pub test: PathBuf,
    pub fixtures: PathBuf,
}

impl Dirs {
    pub fn formats(&self) -> PathBuf {
        self.fixtures.join("formats")
    }
}

impl Playground {
    pub fn root(&self) -> &Path {
        self.root.path()
    }

    pub fn back_to_playground(&mut self) -> &mut Self {
        self.cwd = PathBuf::from(self.root()).join(self.tests.clone());
        self
    }

    pub fn setup(topic: &str, block: impl FnOnce(Dirs, &mut Playground)) {
        let root = tempdir().expect("Couldn't create a tempdir");
        let nuplay_dir = root.path().join(topic);

        if PathBuf::from(&nuplay_dir).exists() {
            std::fs::remove_dir_all(PathBuf::from(&nuplay_dir)).expect("can not remove directory");
        }

        std::fs::create_dir(PathBuf::from(&nuplay_dir)).expect("can not create directory");

        let mut playground = Playground {
            root,
            tests: topic.to_string(),
            cwd: nuplay_dir,
        };

        let playground_root = playground.root.path();

        let fixtures = fs::fixtures();
        let fixtures = dunce::canonicalize(fixtures.clone()).unwrap_or_else(|e| {
            panic!(
                "Couldn't canonicalize fixtures path {}: {:?}",
                fixtures.display(),
                e
            )
        });

        let test = dunce::canonicalize(playground_root.join(topic)).unwrap_or_else(|e| {
            panic!(
                "Couldn't canonicalize test path {}: {:?}",
                playground_root.join(topic).display(),
                e
            )
        });

        let root = dunce::canonicalize(playground_root).unwrap_or_else(|e| {
            panic!(
                "Couldn't canonicalize tests root path {}: {:?}",
                playground_root.display(),
                e
            )
        });

        let dirs = Dirs {
            root,
            test,
            fixtures,
        };

        block(dirs, &mut playground);
    }

    pub fn mkdir(&mut self, directory: &str) -> &mut Self {
        self.cwd.push(directory);
        std::fs::create_dir_all(&self.cwd).expect("can not create directory");
        self.back_to_playground();
        self
    }

    #[cfg(not(target_arch = "wasm32"))]
    pub fn symlink(&mut self, from: impl AsRef<Path>, to: impl AsRef<Path>) -> &mut Self {
        let from = self.cwd.join(from);
        let to = self.cwd.join(to);

        let create_symlink = {
            #[cfg(unix)]
            {
                std::os::unix::fs::symlink
            }

            #[cfg(windows)]
            {
                if from.is_file() {
                    std::os::windows::fs::symlink_file
                } else if from.is_dir() {
                    std::os::windows::fs::symlink_dir
                } else {
                    panic!("symlink from must be a file or dir")
                }
            }
        };

        create_symlink(from, to).expect("can not create symlink");
        self.back_to_playground();
        self
    }

    pub fn with_files(&mut self, files: Vec<Stub>) -> &mut Self {
        let endl = fs::line_ending();

        files
            .iter()
            .map(|f| {
                let mut path = PathBuf::from(&self.cwd);

                let (file_name, contents) = match *f {
                    Stub::EmptyFile(name) => (name, "fake data".to_string()),
                    Stub::FileWithContent(name, content) => (name, content.to_string()),
                    Stub::FileWithContentToBeTrimmed(name, content) => (
                        name,
                        content
                            .lines()
                            .skip(1)
                            .map(|line| line.trim())
                            .collect::<Vec<&str>>()
                            .join(&endl),
                    ),
                };

                path.push(file_name);

                std::fs::write(path, contents.as_bytes()).expect("can not create file");
            })
            .for_each(drop);
        self.back_to_playground();
        self
    }

    pub fn within(&mut self, directory: &str) -> &mut Self {
        self.cwd.push(directory);
        std::fs::create_dir(&self.cwd).expect("can not create directory");
        self
    }

    pub fn glob_vec(pattern: &str) -> Vec<PathBuf> {
        let glob = glob(pattern);

        glob.expect("invalid pattern")
            .map(|path| {
                if let Ok(path) = path {
                    path
                } else {
                    unreachable!()
                }
            })
            .collect()
    }
}
