//! Types for interacting with files in projects.

#[cfg(not(target_arch = "wasm32"))]
use std::path::{Path, PathBuf};

use anyhow::Result;
use parse_display::{Display, FromStr};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

use crate::settings::types::Configuration;

/// State management for the application.
#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq)]
#[ts(export)]
#[serde(rename_all = "snake_case")]
pub struct ProjectState {
    pub project: Project,
    pub current_file: Option<String>,
}

impl ProjectState {
    /// Create a new project state from a path.
    #[cfg(not(target_arch = "wasm32"))]
    pub async fn new_from_path(path: PathBuf) -> Result<ProjectState> {
        // Fix for "." path, which is the current directory.

        let source_path = if path == Path::new(".") {
            std::env::current_dir().map_err(|e| anyhow::anyhow!("Error getting the current directory: {:?}", e))?
        } else {
            path
        };

        // If the path does not start with a slash, it is a relative path.
        // We need to convert it to an absolute path.
        let source_path = if source_path.is_relative() {
            std::env::current_dir()
                .map_err(|e| anyhow::anyhow!("Error getting the current directory: {:?}", e))?
                .join(source_path)
        } else {
            source_path
        };

        // If the path is a directory, let's assume it is a project directory.
        if source_path.is_dir() {
            // Load the details about the project from the path.
            let project = Project::from_path(&source_path)
                .await
                .map_err(|e| anyhow::anyhow!("Error loading project from path {}: {:?}", source_path.display(), e))?;

            // Check if we have a main.kcl file in the project.
            let project_file = source_path.join(crate::settings::types::DEFAULT_PROJECT_KCL_FILE);

            if !project_file.exists() {
                // Create the default file in the project.
                // Write the initial project file.
                tokio::fs::write(&project_file, vec![]).await?;
            }

            return Ok(ProjectState {
                project,
                current_file: Some(project_file.display().to_string()),
            });
        }

        // Check if the extension on what we are trying to open is a relevant file type.
        // Get the extension of the file.
        let extension = source_path
            .extension()
            .ok_or_else(|| anyhow::anyhow!("Error getting the extension of the file: `{}`", source_path.display()))?;
        let ext = extension.to_string_lossy().to_string();

        // Check if the extension is a relevant file type.
        if !crate::settings::utils::RELEVANT_EXTENSIONS.contains(&ext) || ext == "toml" {
            return Err(anyhow::anyhow!(
                "File type ({}) cannot be opened with this app: `{}`, try opening one of the following file types: {}",
                ext,
                source_path.display(),
                crate::settings::utils::RELEVANT_EXTENSIONS.join(", ")
            ));
        }

        // We were given a file path, not a directory.
        // Let's get the parent directory of the file.
        let parent = source_path.parent().ok_or_else(|| {
            anyhow::anyhow!(
                "Error getting the parent directory of the file: {}",
                source_path.display()
            )
        })?;

        // If we got a import model file, we need to check if we have a file in the project for
        // this import model.
        if crate::settings::utils::IMPORT_FILE_EXTENSIONS.contains(&ext) {
            let import_file_name = source_path
                .file_name()
                .ok_or_else(|| anyhow::anyhow!("Error getting the file name of the file: {}", source_path.display()))?
                .to_string_lossy()
                .to_string();
            // Check if we have a file in the project for this import model.
            let kcl_wrapper_filename = format!("{}.kcl", import_file_name);
            let kcl_wrapper_file_path = parent.join(&kcl_wrapper_filename);

            if !kcl_wrapper_file_path.exists() {
                // Create the file in the project.
                // With the default import content.
                tokio::fs::write(
                    &kcl_wrapper_file_path,
                    format!(
                        r#"// This file was automatically generated by the application when you
// double-clicked on the model file.
// You can edit this file to add your own content.
// But we recommend you keep the import statement as it is.
// For more information on the import statement, see the documentation at:
// https://zoo.dev/docs/kcl/import
const model = import("{}")"#,
                        import_file_name
                    )
                    .as_bytes(),
                )
                .await?;
            }

            // Load the details about the project from the parent directory.
            // We do this after we generate the import file so that the file is included in the project.
            let project = Project::from_path(&parent)
                .await
                .map_err(|e| anyhow::anyhow!("Error loading project from path {}: {:?}", source_path.display(), e))?;

            return Ok(ProjectState {
                project,
                current_file: Some(kcl_wrapper_file_path.display().to_string()),
            });
        }

        // Load the details about the project from the parent directory.
        let project = Project::from_path(&parent)
            .await
            .map_err(|e| anyhow::anyhow!("Error loading project from path {}: {:?}", source_path.display(), e))?;

        Ok(ProjectState {
            project,
            current_file: Some(source_path.display().to_string()),
        })
    }
}

/// Project route information.
#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq)]
#[ts(export)]
#[serde(rename_all = "snake_case")]
pub struct ProjectRoute {
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub project_name: Option<String>,
    pub project_path: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub current_file_name: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub current_file_path: Option<String>,
}

impl ProjectRoute {
    /// Get the project state from the url in the route.
    pub fn from_route(configuration: &Configuration, route: &str) -> Result<Self> {
        let path = std::path::Path::new(route);
        // Check if the default project path is in the route.
        let (project_path, project_name) = if path.starts_with(&configuration.settings.project.directory)
            && configuration.settings.project.directory != std::path::PathBuf::default()
        {
            // Get the project name.
            if let Some(project_name) = path
                .strip_prefix(&configuration.settings.project.directory)?
                .iter()
                .next()
            {
                (
                    configuration
                        .settings
                        .project
                        .directory
                        .join(project_name)
                        .display()
                        .to_string(),
                    Some(project_name.to_string_lossy().to_string()),
                )
            } else {
                (configuration.settings.project.directory.display().to_string(), None)
            }
        } else {
            // Assume the project path is the parent directory of the file.
            let project_dir = if path.display().to_string().ends_with(".kcl") {
                path.parent()
                    .ok_or_else(|| anyhow::anyhow!("Parent directory not found: {}", path.display()))?
            } else {
                path
            };

            if project_dir == std::path::Path::new("/") {
                (
                    path.display().to_string(),
                    Some(
                        path.file_name()
                            .ok_or_else(|| anyhow::anyhow!("File name not found: {}", path.display()))?
                            .to_string_lossy()
                            .to_string(),
                    ),
                )
            } else if let Some(project_name) = project_dir.file_name() {
                (
                    project_dir.display().to_string(),
                    Some(project_name.to_string_lossy().to_string()),
                )
            } else {
                (project_dir.display().to_string(), None)
            }
        };

        let (current_file_name, current_file_path) = if path.display().to_string() == project_path {
            (None, None)
        } else {
            (
                Some(
                    path.file_name()
                        .ok_or_else(|| anyhow::anyhow!("File name not found: {}", path.display()))?
                        .to_string_lossy()
                        .to_string(),
                ),
                Some(path.display().to_string()),
            )
        };

        Ok(Self {
            project_name,
            project_path,
            current_file_name,
            current_file_path,
        })
    }
}

/// Information about project.
#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq)]
#[ts(export)]
#[serde(rename_all = "snake_case")]
pub struct Project {
    #[serde(flatten)]
    pub file: FileEntry,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub metadata: Option<FileMetadata>,
    #[serde(default)]
    #[ts(type = "number")]
    pub kcl_file_count: u64,
    #[serde(default)]
    #[ts(type = "number")]
    pub directory_count: u64,
    /// The default file to open on load.
    pub default_file: String,
}

impl Project {
    #[cfg(not(target_arch = "wasm32"))]
    /// Populate a project from a path.
    pub async fn from_path<P: AsRef<std::path::Path>>(path: P) -> Result<Self> {
        // Check if they are using '.' as the path.
        let path = if path.as_ref() == std::path::Path::new(".") {
            std::env::current_dir()?
        } else {
            path.as_ref().to_path_buf()
        };

        // Make sure the path exists.
        if !path.exists() {
            return Err(anyhow::anyhow!("Path does not exist"));
        }

        let file = crate::settings::utils::walk_dir(&path).await?;
        let metadata = std::fs::metadata(&path).ok().map(|m| m.into());
        let mut project = Self {
            file: file.clone(),
            metadata,
            kcl_file_count: 0,
            directory_count: 0,
            default_file: get_default_kcl_file_for_dir(path, file).await?,
        };
        project.populate_kcl_file_count()?;
        project.populate_directory_count()?;
        Ok(project)
    }

    /// Populate the number of KCL files in the project.
    pub fn populate_kcl_file_count(&mut self) -> Result<()> {
        let mut count = 0;
        if let Some(children) = &self.file.children {
            for entry in children.iter() {
                if entry.name.ends_with(".kcl") {
                    count += 1;
                } else {
                    count += entry.kcl_file_count();
                }
            }
        }

        self.kcl_file_count = count;
        Ok(())
    }

    /// Populate the number of directories in the project.
    pub fn populate_directory_count(&mut self) -> Result<()> {
        let mut count = 0;
        if let Some(children) = &self.file.children {
            for entry in children.iter() {
                count += entry.directory_count();
            }
        }

        self.directory_count = count;
        Ok(())
    }
}

/// Get the default KCL file for a directory.
/// This determines what the default file to open is.
#[cfg(not(target_arch = "wasm32"))]
#[async_recursion::async_recursion]
pub async fn get_default_kcl_file_for_dir<P>(dir: P, file: FileEntry) -> Result<String>
where
    P: AsRef<Path> + Send,
{
    // Make sure the dir is a directory.
    if !dir.as_ref().is_dir() {
        return Err(anyhow::anyhow!("Path `{}` is not a directory", dir.as_ref().display()));
    }

    let default_file = dir.as_ref().join(crate::settings::types::DEFAULT_PROJECT_KCL_FILE);
    if !default_file.exists() {
        // Find a kcl file in the directory.
        if let Some(children) = file.children {
            for entry in children.iter() {
                if entry.name.ends_with(".kcl") {
                    return Ok(dir.as_ref().join(&entry.name).display().to_string());
                } else if entry.children.is_some() {
                    // Recursively find a kcl file in the directory.
                    return get_default_kcl_file_for_dir(entry.path.clone(), entry.clone()).await;
                }
            }
        }

        // If we didn't find a kcl file, create one.
        tokio::fs::write(&default_file, vec![]).await?;
    }

    Ok(default_file.display().to_string())
}

#[cfg(not(target_arch = "wasm32"))]
/// Rename a directory for a project.
/// This returns the new path of the directory.
pub async fn rename_project_directory<P>(path: P, new_name: &str) -> Result<std::path::PathBuf>
where
    P: AsRef<Path> + Send,
{
    if new_name.is_empty() {
        return Err(anyhow::anyhow!("New name for project cannot be empty"));
    }

    // Make sure the path is a directory.
    if !path.as_ref().is_dir() {
        return Err(anyhow::anyhow!("Path `{}` is not a directory", path.as_ref().display()));
    }

    // Make sure the new name does not exist.
    let new_path = path
        .as_ref()
        .parent()
        .ok_or_else(|| anyhow::anyhow!("Parent directory of `{}` not found", path.as_ref().display()))?
        .join(new_name);
    if new_path.exists() {
        return Err(anyhow::anyhow!(
            "Path `{}` already exists, cannot rename to an existing path",
            new_path.display()
        ));
    }

    tokio::fs::rename(path.as_ref(), &new_path).await?;
    Ok(new_path)
}

/// Information about a file or directory.
#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq)]
#[ts(export)]
#[serde(rename_all = "snake_case")]
pub struct FileEntry {
    pub path: String,
    pub name: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub children: Option<Vec<FileEntry>>,
}

impl FileEntry {
    /// Recursively get the number of kcl files in the file entry.
    pub fn kcl_file_count(&self) -> u64 {
        let mut count = 0;
        if let Some(children) = &self.children {
            for entry in children.iter() {
                if entry.name.ends_with(".kcl") {
                    count += 1;
                } else {
                    count += entry.kcl_file_count();
                }
            }
        }
        count
    }

    /// Recursively get the number of directories in the file entry.
    pub fn directory_count(&self) -> u64 {
        let mut count = 0;
        if let Some(children) = &self.children {
            for entry in children.iter() {
                if entry.children.is_some() {
                    count += 1;
                }
            }
        }
        count
    }
}

/// Metadata about a file or directory.
#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq)]
#[ts(export)]
#[serde(rename_all = "snake_case")]
pub struct FileMetadata {
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub accessed: Option<chrono::DateTime<chrono::Utc>>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub created: Option<chrono::DateTime<chrono::Utc>>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub r#type: Option<FileType>,
    #[serde(default)]
    #[ts(type = "number")]
    pub size: u64,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub modified: Option<chrono::DateTime<chrono::Utc>>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub permission: Option<FilePermission>,
}

/// The type of a file.
#[derive(Debug, Copy, Clone, Deserialize, Serialize, JsonSchema, Display, FromStr, ts_rs::TS, PartialEq, Eq)]
#[ts(export)]
#[serde(rename_all = "snake_case")]
#[display(style = "snake_case")]
pub enum FileType {
    /// A file.
    File,
    /// A directory.
    Directory,
    /// A symbolic link.
    Symlink,
}

/// The permissions of a file.
#[derive(Debug, Copy, Clone, Deserialize, Serialize, JsonSchema, Display, FromStr, ts_rs::TS, PartialEq, Eq)]
#[ts(export)]
#[serde(rename_all = "snake_case")]
#[display(style = "snake_case")]
pub enum FilePermission {
    /// Read permission.
    Read,
    /// Write permission.
    Write,
    /// Execute permission.
    Execute,
}

impl From<std::fs::FileType> for FileType {
    fn from(file_type: std::fs::FileType) -> Self {
        if file_type.is_file() {
            FileType::File
        } else if file_type.is_dir() {
            FileType::Directory
        } else if file_type.is_symlink() {
            FileType::Symlink
        } else {
            unreachable!()
        }
    }
}

impl From<std::fs::Permissions> for FilePermission {
    fn from(permissions: std::fs::Permissions) -> Self {
        #[cfg(unix)]
        {
            use std::os::unix::fs::PermissionsExt;
            let mode = permissions.mode();
            if mode & 0o400 != 0 {
                FilePermission::Read
            } else if mode & 0o200 != 0 {
                FilePermission::Write
            } else if mode & 0o100 != 0 {
                FilePermission::Execute
            } else {
                unreachable!()
            }
        }
        #[cfg(not(unix))]
        {
            if permissions.readonly() {
                FilePermission::Read
            } else {
                FilePermission::Write
            }
        }
    }
}

impl From<std::fs::Metadata> for FileMetadata {
    fn from(metadata: std::fs::Metadata) -> Self {
        Self {
            accessed: metadata.accessed().ok().map(|t| t.into()),
            created: metadata.created().ok().map(|t| t.into()),
            r#type: Some(metadata.file_type().into()),
            size: metadata.len(),
            modified: metadata.modified().ok().map(|t| t.into()),
            permission: Some(metadata.permissions().into()),
        }
    }
}

#[cfg(test)]
mod tests {
    use pretty_assertions::assert_eq;

    #[test]
    fn test_project_route_from_route_std_path() {
        let mut configuration = crate::settings::types::Configuration::default();
        configuration.settings.project.directory =
            std::path::PathBuf::from("/Users/macinatormax/Documents/kittycad-modeling-projects");

        let route = "/Users/macinatormax/Documents/kittycad-modeling-projects/assembly/main.kcl";
        let state = super::ProjectRoute::from_route(&configuration, route).unwrap();
        assert_eq!(
            state,
            super::ProjectRoute {
                project_name: Some("assembly".to_string()),
                project_path: "/Users/macinatormax/Documents/kittycad-modeling-projects/assembly".to_string(),
                current_file_name: Some("main.kcl".to_string()),
                current_file_path: Some(
                    "/Users/macinatormax/Documents/kittycad-modeling-projects/assembly/main.kcl".to_string()
                ),
            }
        );
    }

    #[test]
    fn test_project_route_from_route_std_path_dir() {
        let mut configuration = crate::settings::types::Configuration::default();
        configuration.settings.project.directory =
            std::path::PathBuf::from("/Users/macinatormax/Documents/kittycad-modeling-projects");

        let route = "/Users/macinatormax/Documents/kittycad-modeling-projects/assembly";
        let state = super::ProjectRoute::from_route(&configuration, route).unwrap();
        assert_eq!(
            state,
            super::ProjectRoute {
                project_name: Some("assembly".to_string()),
                project_path: "/Users/macinatormax/Documents/kittycad-modeling-projects/assembly".to_string(),
                current_file_name: None,
                current_file_path: None,
            }
        );
    }

    #[test]
    fn test_project_route_from_route_std_path_dir_empty() {
        let mut configuration = crate::settings::types::Configuration::default();
        configuration.settings.project.directory =
            std::path::PathBuf::from("/Users/macinatormax/Documents/kittycad-modeling-projects");

        let route = "/Users/macinatormax/Documents/kittycad-modeling-projects";
        let state = super::ProjectRoute::from_route(&configuration, route).unwrap();
        assert_eq!(
            state,
            super::ProjectRoute {
                project_name: None,
                project_path: "/Users/macinatormax/Documents/kittycad-modeling-projects".to_string(),
                current_file_name: None,
                current_file_path: None,
            }
        );
    }

    #[test]
    fn test_project_route_from_route_outside_std_path() {
        let mut configuration = crate::settings::types::Configuration::default();
        configuration.settings.project.directory =
            std::path::PathBuf::from("/Users/macinatormax/Documents/kittycad-modeling-projects");

        let route = "/Users/macinatormax/kittycad/modeling-app/main.kcl";
        let state = super::ProjectRoute::from_route(&configuration, route).unwrap();
        assert_eq!(
            state,
            super::ProjectRoute {
                project_name: Some("modeling-app".to_string()),
                project_path: "/Users/macinatormax/kittycad/modeling-app".to_string(),
                current_file_name: Some("main.kcl".to_string()),
                current_file_path: Some("/Users/macinatormax/kittycad/modeling-app/main.kcl".to_string()),
            }
        );
    }

    #[test]
    fn test_project_route_from_route_outside_std_path_dir() {
        let mut configuration = crate::settings::types::Configuration::default();
        configuration.settings.project.directory =
            std::path::PathBuf::from("/Users/macinatormax/Documents/kittycad-modeling-projects");

        let route = "/Users/macinatormax/kittycad/modeling-app";
        let state = super::ProjectRoute::from_route(&configuration, route).unwrap();
        assert_eq!(
            state,
            super::ProjectRoute {
                project_name: Some("modeling-app".to_string()),
                project_path: "/Users/macinatormax/kittycad/modeling-app".to_string(),
                current_file_name: None,
                current_file_path: None,
            }
        );
    }

    #[test]
    fn test_project_route_from_route_browser() {
        let mut configuration = crate::settings::types::Configuration::default();
        configuration.settings.project.directory = std::path::PathBuf::default();

        let route = "/browser/main.kcl";
        let state = super::ProjectRoute::from_route(&configuration, route).unwrap();
        assert_eq!(
            state,
            super::ProjectRoute {
                project_name: Some("browser".to_string()),
                project_path: "/browser".to_string(),
                current_file_name: Some("main.kcl".to_string()),
                current_file_path: Some("/browser/main.kcl".to_string()),
            }
        );
    }

    #[test]
    fn test_project_route_from_route_browser_no_path() {
        let mut configuration = crate::settings::types::Configuration::default();
        configuration.settings.project.directory = std::path::PathBuf::default();

        let route = "/browser";
        let state = super::ProjectRoute::from_route(&configuration, route).unwrap();
        assert_eq!(
            state,
            super::ProjectRoute {
                project_name: Some("browser".to_string()),
                project_path: "/browser".to_string(),
                current_file_name: None,
                current_file_path: None,
            }
        );
    }

    #[test]
    fn test_project_route_from_route_non_main_file() {
        let mut configuration = crate::settings::types::Configuration::default();
        configuration.settings.project.directory =
            std::path::PathBuf::from("/Users/macinatormax/Documents/kittycad-modeling-projects");

        let route = "/Users/macinatormax/Documents/kittycad-modeling-projects/assembly/thing.kcl";
        let state = super::ProjectRoute::from_route(&configuration, route).unwrap();
        assert_eq!(
            state,
            super::ProjectRoute {
                project_name: Some("assembly".to_string()),
                project_path: "/Users/macinatormax/Documents/kittycad-modeling-projects/assembly".to_string(),
                current_file_name: Some("thing.kcl".to_string()),
                current_file_path: Some(
                    "/Users/macinatormax/Documents/kittycad-modeling-projects/assembly/thing.kcl".to_string()
                ),
            }
        );
    }

    #[tokio::test]
    async fn test_default_kcl_file_for_dir_non_exist() {
        let name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
        let dir = std::env::temp_dir().join(&name);
        std::fs::create_dir_all(&dir).unwrap();
        let file = crate::settings::utils::walk_dir(&dir).await.unwrap();

        let default_file = super::get_default_kcl_file_for_dir(&dir, file).await.unwrap();
        assert_eq!(default_file, dir.join("main.kcl").display().to_string());

        std::fs::remove_dir_all(dir).unwrap();
    }

    #[tokio::test]
    async fn test_default_kcl_file_for_dir_main_kcl() {
        let name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
        let dir = std::env::temp_dir().join(&name);
        std::fs::create_dir_all(&dir).unwrap();
        std::fs::write(dir.join("main.kcl"), vec![]).unwrap();
        let file = crate::settings::utils::walk_dir(&dir).await.unwrap();

        let default_file = super::get_default_kcl_file_for_dir(&dir, file).await.unwrap();
        assert_eq!(default_file, dir.join("main.kcl").display().to_string());

        std::fs::remove_dir_all(dir).unwrap();
    }

    #[tokio::test]
    async fn test_default_kcl_file_for_dir_thing_kcl() {
        let name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
        let dir = std::env::temp_dir().join(&name);
        std::fs::create_dir_all(&dir).unwrap();
        std::fs::write(dir.join("thing.kcl"), vec![]).unwrap();
        let file = crate::settings::utils::walk_dir(&dir).await.unwrap();

        let default_file = super::get_default_kcl_file_for_dir(&dir, file).await.unwrap();
        assert_eq!(default_file, dir.join("thing.kcl").display().to_string());
        std::fs::remove_dir_all(dir).unwrap();
    }

    #[tokio::test]
    async fn test_default_kcl_file_for_dir_nested_main_kcl() {
        let name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
        let dir = std::env::temp_dir().join(&name);
        std::fs::create_dir_all(&dir).unwrap();
        std::fs::create_dir_all(dir.join("assembly")).unwrap();
        std::fs::write(dir.join("assembly").join("main.kcl"), vec![]).unwrap();
        let file = crate::settings::utils::walk_dir(&dir).await.unwrap();

        let default_file = super::get_default_kcl_file_for_dir(&dir, file).await.unwrap();
        assert_eq!(
            default_file,
            dir.join("assembly").join("main.kcl").display().to_string()
        );

        std::fs::remove_dir_all(dir).unwrap();
    }

    #[tokio::test]
    async fn test_default_kcl_file_for_dir_nested_thing_kcl() {
        let name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
        let dir = std::env::temp_dir().join(&name);
        std::fs::create_dir_all(&dir).unwrap();
        std::fs::create_dir_all(dir.join("assembly")).unwrap();
        std::fs::write(dir.join("assembly").join("thing.kcl"), vec![]).unwrap();
        let file = crate::settings::utils::walk_dir(&dir).await.unwrap();

        let default_file = super::get_default_kcl_file_for_dir(&dir, file).await.unwrap();
        assert_eq!(
            default_file,
            dir.join("assembly").join("thing.kcl").display().to_string()
        );
        std::fs::remove_dir_all(dir).unwrap();
    }

    #[tokio::test]
    async fn test_rename_project_directory_empty_dir() {
        let name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
        let dir = std::env::temp_dir().join(&name);
        std::fs::create_dir_all(&dir).unwrap();

        let new_name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
        let new_dir = super::rename_project_directory(&dir, &new_name).await.unwrap();
        assert_eq!(new_dir, std::env::temp_dir().join(&new_name));

        std::fs::remove_dir_all(new_dir).unwrap();
    }

    #[tokio::test]
    async fn test_rename_project_directory_empty_name() {
        let name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
        let dir = std::env::temp_dir().join(&name);
        std::fs::create_dir_all(&dir).unwrap();

        let result = super::rename_project_directory(&dir, "").await;
        assert!(result.is_err());
        assert_eq!(result.unwrap_err().to_string(), "New name for project cannot be empty");

        std::fs::remove_dir_all(dir).unwrap();
    }

    #[tokio::test]
    async fn test_rename_project_directory_non_empty_dir() {
        let name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
        let dir = std::env::temp_dir().join(&name);
        std::fs::create_dir_all(&dir).unwrap();
        std::fs::write(dir.join("main.kcl"), vec![]).unwrap();

        let new_name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
        let new_dir = super::rename_project_directory(&dir, &new_name).await.unwrap();
        assert_eq!(new_dir, std::env::temp_dir().join(&new_name));

        std::fs::remove_dir_all(new_dir).unwrap();
    }

    #[tokio::test]
    async fn test_rename_project_directory_non_empty_dir_recursive() {
        let name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
        let dir = std::env::temp_dir().join(&name);
        std::fs::create_dir_all(&dir).unwrap();
        std::fs::create_dir_all(dir.join("assembly")).unwrap();
        std::fs::write(dir.join("assembly").join("main.kcl"), vec![]).unwrap();

        let new_name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
        let new_dir = super::rename_project_directory(&dir, &new_name).await.unwrap();
        assert_eq!(new_dir, std::env::temp_dir().join(&new_name));

        std::fs::remove_dir_all(new_dir).unwrap();
    }

    #[tokio::test]
    async fn test_rename_project_directory_dir_is_file() {
        let name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
        let dir = std::env::temp_dir().join(&name);
        std::fs::write(&dir, vec![]).unwrap();

        let new_name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
        let result = super::rename_project_directory(&dir, &new_name).await;
        assert!(result.is_err());
        assert_eq!(
            result.unwrap_err().to_string(),
            format!("Path `{}` is not a directory", dir.display())
        );

        std::fs::remove_file(dir).unwrap();
    }

    #[tokio::test]
    async fn test_rename_project_directory_new_name_exists() {
        let name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
        let dir = std::env::temp_dir().join(&name);
        std::fs::create_dir_all(&dir).unwrap();

        let new_name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
        let new_dir = std::env::temp_dir().join(&new_name);
        std::fs::create_dir_all(&new_dir).unwrap();

        let result = super::rename_project_directory(&dir, &new_name).await;
        assert!(result.is_err());
        assert_eq!(
            result.unwrap_err().to_string(),
            format!(
                "Path `{}` already exists, cannot rename to an existing path",
                new_dir.display()
            )
        );

        std::fs::remove_dir_all(new_dir).unwrap();
    }

    #[tokio::test]
    async fn test_project_state_new_from_path_source_path_dot() {
        let name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
        let tmp_project_dir = std::env::temp_dir().join(&name);
        std::fs::create_dir_all(&tmp_project_dir).unwrap();
        // Set the current directory to the temp project directory.
        // This is to simulate the "." path.
        std::env::set_current_dir(&tmp_project_dir).unwrap();

        let state = super::ProjectState::new_from_path(std::path::PathBuf::from("."))
            .await
            .unwrap();

        assert_eq!(state.project.file.name, name);
        assert_eq!(
            state
                .project
                .file
                .path
                // macOS adds /private to the path i think because we changed curdirs
                .trim_start_matches("/private"),
            tmp_project_dir.display().to_string()
        );
        assert_eq!(
            state
                .current_file
                .unwrap()
                // macOS adds /private to the path i think because we changed curdirs
                .trim_start_matches("/private"),
            tmp_project_dir.join("main.kcl").display().to_string()
        );
        assert_eq!(
            state
                .project
                .default_file
                // macOS adds /private to the path i think because we changed curdirs
                .trim_start_matches("/private"),
            tmp_project_dir.join("main.kcl").display().to_string()
        );

        std::fs::remove_dir_all(tmp_project_dir).unwrap();
    }

    #[tokio::test]
    async fn test_project_state_new_from_path_main_kcl_not_exists() {
        let name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
        let tmp_project_dir = std::env::temp_dir().join(&name);
        std::fs::create_dir_all(&tmp_project_dir).unwrap();

        let state = super::ProjectState::new_from_path(tmp_project_dir.clone())
            .await
            .unwrap();

        assert_eq!(state.project.file.name, name);
        assert_eq!(state.project.file.path, tmp_project_dir.display().to_string());
        assert_eq!(
            state.current_file,
            Some(tmp_project_dir.join("main.kcl").display().to_string())
        );
        assert_eq!(
            state.project.default_file,
            tmp_project_dir.join("main.kcl").display().to_string()
        );

        std::fs::remove_dir_all(tmp_project_dir).unwrap();
    }

    #[tokio::test]
    async fn test_project_state_new_from_path_main_kcl_exists() {
        let name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
        let tmp_project_dir = std::env::temp_dir().join(&name);
        std::fs::create_dir_all(&tmp_project_dir).unwrap();
        std::fs::write(tmp_project_dir.join("main.kcl"), vec![]).unwrap();

        let state = super::ProjectState::new_from_path(tmp_project_dir.clone())
            .await
            .unwrap();

        assert_eq!(state.project.file.name, name);
        assert_eq!(state.project.file.path, tmp_project_dir.display().to_string());
        assert_eq!(
            state.current_file,
            Some(tmp_project_dir.join("main.kcl").display().to_string())
        );
        assert_eq!(
            state.project.default_file,
            tmp_project_dir.join("main.kcl").display().to_string()
        );

        std::fs::remove_dir_all(tmp_project_dir).unwrap();
    }

    #[tokio::test]
    async fn test_project_state_new_from_path_explicit_open_main_kcl() {
        let name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
        let tmp_project_dir = std::env::temp_dir().join(&name);
        std::fs::create_dir_all(&tmp_project_dir).unwrap();
        std::fs::write(tmp_project_dir.join("main.kcl"), vec![]).unwrap();

        let state = super::ProjectState::new_from_path(tmp_project_dir.join("main.kcl"))
            .await
            .unwrap();

        assert_eq!(state.project.file.name, name);
        assert_eq!(state.project.file.path, tmp_project_dir.display().to_string());
        assert_eq!(
            state.current_file,
            Some(tmp_project_dir.join("main.kcl").display().to_string())
        );
        assert_eq!(
            state.project.default_file,
            tmp_project_dir.join("main.kcl").display().to_string()
        );

        std::fs::remove_dir_all(tmp_project_dir).unwrap();
    }

    #[tokio::test]
    async fn test_project_state_new_from_path_explicit_open_thing_kcl() {
        let name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
        let tmp_project_dir = std::env::temp_dir().join(&name);
        std::fs::create_dir_all(&tmp_project_dir).unwrap();
        std::fs::write(tmp_project_dir.join("thing.kcl"), vec![]).unwrap();

        let state = super::ProjectState::new_from_path(tmp_project_dir.join("thing.kcl"))
            .await
            .unwrap();

        assert_eq!(state.project.file.name, name);
        assert_eq!(state.project.file.path, tmp_project_dir.display().to_string());
        assert_eq!(
            state.current_file,
            Some(tmp_project_dir.join("thing.kcl").display().to_string())
        );
        assert_eq!(
            state.project.default_file,
            tmp_project_dir.join("thing.kcl").display().to_string()
        );

        std::fs::remove_dir_all(tmp_project_dir).unwrap();
    }

    #[tokio::test]
    async fn test_project_state_new_from_path_explicit_open_model_obj() {
        let name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
        let tmp_project_dir = std::env::temp_dir().join(&name);
        std::fs::create_dir_all(&tmp_project_dir).unwrap();
        std::fs::write(tmp_project_dir.join("model.obj"), vec![]).unwrap();

        let state = super::ProjectState::new_from_path(tmp_project_dir.join("model.obj"))
            .await
            .unwrap();

        assert_eq!(state.project.file.name, name);
        assert_eq!(state.project.file.path, tmp_project_dir.display().to_string());
        assert_eq!(
            state.current_file,
            Some(tmp_project_dir.join("model.obj.kcl").display().to_string())
        );
        assert_eq!(
            state.project.default_file,
            tmp_project_dir.join("model.obj.kcl").display().to_string()
        );

        // Get the contents of the generated kcl file.
        let kcl_file_contents = tokio::fs::read(tmp_project_dir.join("model.obj.kcl")).await.unwrap();
        assert_eq!(
            String::from_utf8_lossy(&kcl_file_contents),
            r#"// This file was automatically generated by the application when you
// double-clicked on the model file.
// You can edit this file to add your own content.
// But we recommend you keep the import statement as it is.
// For more information on the import statement, see the documentation at:
// https://zoo.dev/docs/kcl/import
const model = import("model.obj")"#
        );

        std::fs::remove_dir_all(tmp_project_dir).unwrap();
    }

    #[tokio::test]
    async fn test_project_state_new_from_path_explicit_open_settings_toml() {
        let name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
        let tmp_project_dir = std::env::temp_dir().join(&name);
        std::fs::create_dir_all(&tmp_project_dir).unwrap();
        std::fs::write(tmp_project_dir.join("settings.toml"), vec![]).unwrap();

        let result = super::ProjectState::new_from_path(tmp_project_dir.join("settings.toml")).await;

        assert!(result.is_err());
        assert_eq!(
            result.unwrap_err().to_string(),
            format!(
                "File type (toml) cannot be opened with this app: `{}`, try opening one of the following file types: stp, glb, fbxb, fbx, gltf, obj, ply, sldprt, step, stl, kcl",
                tmp_project_dir.join("settings.toml").display()
            )
        );

        std::fs::remove_dir_all(tmp_project_dir).unwrap();
    }

    #[tokio::test]
    async fn test_project_state_new_from_path_explicit_open_non_relevant_file() {
        let name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
        let tmp_project_dir = std::env::temp_dir().join(&name);
        std::fs::create_dir_all(&tmp_project_dir).unwrap();
        std::fs::write(tmp_project_dir.join("settings.docx"), vec![]).unwrap();

        let result = super::ProjectState::new_from_path(tmp_project_dir.join("settings.docx")).await;

        assert!(result.is_err());
        assert_eq!(
            result.unwrap_err().to_string(),
            format!(
                "File type (docx) cannot be opened with this app: `{}`, try opening one of the following file types: stp, glb, fbxb, fbx, gltf, obj, ply, sldprt, step, stl, kcl",
                tmp_project_dir.join("settings.docx").display()
            )
        );

        std::fs::remove_dir_all(tmp_project_dir).unwrap();
    }

    #[tokio::test]
    async fn test_project_state_new_from_path_explicit_open_no_file_extension() {
        let name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
        let tmp_project_dir = std::env::temp_dir().join(&name);
        std::fs::create_dir_all(&tmp_project_dir).unwrap();
        std::fs::write(tmp_project_dir.join("file"), vec![]).unwrap();

        let result = super::ProjectState::new_from_path(tmp_project_dir.join("file")).await;

        assert!(result.is_err());
        assert_eq!(
            result.unwrap_err().to_string(),
            format!(
                "Error getting the extension of the file: `{}`",
                tmp_project_dir.join("file").display()
            )
        );

        std::fs::remove_dir_all(tmp_project_dir).unwrap();
    }
}
