use std::borrow::Cow;
use std::io;
use std::path::Component;
use std::path::Path;
use std::path::PathBuf;

use codex_config::permissions_toml::FilesystemPermissionToml;
use codex_config::permissions_toml::FilesystemPermissionsToml;
use codex_config::permissions_toml::NetworkToml;
use codex_config::permissions_toml::PermissionProfileToml;
use codex_config::permissions_toml::PermissionsToml;
use codex_config::types::SandboxWorkspaceWrite;
use codex_network_proxy::NetworkProxyConfig;
#[cfg(test)]
use codex_network_proxy::NetworkUnixSocketPermission as ProxyNetworkUnixSocketPermission;
use codex_protocol::config_types::WindowsSandboxLevel;
use codex_protocol::models::PermissionProfile;
use codex_protocol::permissions::FileSystemAccessMode;
use codex_protocol::permissions::FileSystemPath;
use codex_protocol::permissions::FileSystemSandboxEntry;
use codex_protocol::permissions::FileSystemSandboxPolicy;
use codex_protocol::permissions::FileSystemSpecialPath;
use codex_protocol::permissions::NetworkSandboxPolicy;
use codex_utils_absolute_path::AbsolutePathBuf;

use super::ProjectConfig;

pub(crate) const BUILT_IN_READ_ONLY_PROFILE: &str = ":read-only";
pub(crate) const BUILT_IN_WORKSPACE_PROFILE: &str = ":workspace";
pub(crate) const BUILT_IN_DANGER_NO_SANDBOX_PROFILE: &str = ":danger-no-sandbox";

pub(crate) fn default_builtin_permission_profile_name(
    active_project: &ProjectConfig,
    windows_sandbox_level: WindowsSandboxLevel,
) -> &'static str {
    if (active_project.is_trusted() || active_project.is_untrusted())
        && !(cfg!(target_os = "windows") && windows_sandbox_level == WindowsSandboxLevel::Disabled)
    {
        BUILT_IN_WORKSPACE_PROFILE
    } else {
        BUILT_IN_READ_ONLY_PROFILE
    }
}

pub(crate) fn is_builtin_permission_profile_name(profile_name: &str) -> bool {
    matches!(
        profile_name,
        BUILT_IN_READ_ONLY_PROFILE
            | BUILT_IN_WORKSPACE_PROFILE
            | BUILT_IN_DANGER_NO_SANDBOX_PROFILE
    )
}

pub(crate) fn builtin_permission_profile(
    profile_name: &str,
    workspace_write: Option<&SandboxWorkspaceWrite>,
) -> Option<PermissionProfile> {
    match profile_name {
        BUILT_IN_READ_ONLY_PROFILE => Some(PermissionProfile::read_only()),
        BUILT_IN_WORKSPACE_PROFILE => Some(match workspace_write {
            Some(SandboxWorkspaceWrite {
                writable_roots,
                network_access,
                exclude_tmpdir_env_var,
                exclude_slash_tmp,
            }) => PermissionProfile::workspace_write_with(
                writable_roots,
                if *network_access {
                    NetworkSandboxPolicy::Enabled
                } else {
                    NetworkSandboxPolicy::Restricted
                },
                *exclude_tmpdir_env_var,
                *exclude_slash_tmp,
            ),
            None => PermissionProfile::workspace_write(),
        }),
        BUILT_IN_DANGER_NO_SANDBOX_PROFILE => Some(PermissionProfile::Disabled),
        _ => None,
    }
}

pub(crate) fn validate_user_permission_profile_names(
    permissions: Option<&PermissionsToml>,
) -> io::Result<()> {
    let Some(permissions) = permissions else {
        return Ok(());
    };

    for profile_name in permissions.entries.keys() {
        if profile_name.starts_with(':') {
            return Err(io::Error::new(
                io::ErrorKind::InvalidInput,
                format!(
                    "permissions profile `{profile_name}` uses a reserved built-in profile prefix"
                ),
            ));
        }
    }

    Ok(())
}

pub(crate) fn network_proxy_config_from_profile_network(
    network: Option<&NetworkToml>,
) -> NetworkProxyConfig {
    let mut config = network.map_or_else(
        NetworkProxyConfig::default,
        NetworkToml::to_network_proxy_config,
    );
    // Profile `network.enabled` controls sandbox network access. Do not start a
    // managed proxy for that bit alone, but keep the proxy enabled when the
    // profile also supplied policy that only the proxy can enforce.
    config.network.enabled = network.is_some_and(profile_network_requires_proxy);
    config
}

fn profile_network_requires_proxy(network: &NetworkToml) -> bool {
    if network.enabled != Some(true) {
        return false;
    }

    network.proxy_url.is_some()
        || network.enable_socks5 == Some(true)
        || network.socks_url.is_some()
        || network.enable_socks5_udp == Some(true)
        || network.allow_upstream_proxy == Some(true)
        || network.dangerously_allow_non_loopback_proxy == Some(true)
        || network.dangerously_allow_all_unix_sockets == Some(true)
        || network.mode.is_some()
        || network
            .domains
            .as_ref()
            .is_some_and(|domains| !domains.is_empty())
        || network
            .unix_sockets
            .as_ref()
            .is_some_and(|unix_sockets| !unix_sockets.is_empty())
        || network.allow_local_binding == Some(true)
}

pub(crate) fn resolve_permission_profile<'a>(
    permissions: &'a PermissionsToml,
    profile_name: &str,
) -> io::Result<&'a PermissionProfileToml> {
    permissions.entries.get(profile_name).ok_or_else(|| {
        io::Error::new(
            io::ErrorKind::InvalidInput,
            format!("default_permissions refers to undefined profile `{profile_name}`"),
        )
    })
}

pub(crate) fn network_proxy_config_for_profile_selection(
    permissions: Option<&PermissionsToml>,
    profile_name: &str,
) -> io::Result<NetworkProxyConfig> {
    if is_builtin_permission_profile_name(profile_name) {
        return Ok(NetworkProxyConfig::default());
    }
    reject_unknown_builtin_permission_profile(profile_name)?;

    let permissions = permissions.ok_or_else(|| {
        io::Error::new(
            io::ErrorKind::InvalidInput,
            "default_permissions requires a `[permissions]` table",
        )
    })?;
    let profile = resolve_permission_profile(permissions, profile_name)?;
    Ok(network_proxy_config_from_profile_network(
        profile.network.as_ref(),
    ))
}

pub(crate) fn compile_permission_profile(
    permissions: &PermissionsToml,
    profile_name: &str,
    policy_cwd: &Path,
    startup_warnings: &mut Vec<String>,
) -> io::Result<(FileSystemSandboxPolicy, NetworkSandboxPolicy)> {
    let profile = resolve_permission_profile(permissions, profile_name)?;

    let mut entries = Vec::new();
    if let Some(filesystem) = profile.filesystem.as_ref() {
        if filesystem.is_empty() {
            push_warning(
                startup_warnings,
                missing_filesystem_entries_warning(profile_name),
            );
        } else {
            if cfg!(not(target_os = "macos")) {
                for pattern in unsupported_read_write_glob_paths(filesystem) {
                    push_warning(
                        startup_warnings,
                        format!(
                            "Filesystem glob `{pattern}` uses `read` or `write` access, which is not fully supported by this platform's sandboxing. Use an exact path or trailing `/**` subtree rule instead. `none` deny-read globs are supported."
                        ),
                    );
                }
                for pattern in unbounded_unreadable_globstar_paths(filesystem) {
                    push_warning(
                        startup_warnings,
                        format!(
                            "Filesystem deny-read glob `{pattern}` uses `**`. Non-macOS sandboxing does not support unbounded `**` natively; set `glob_scan_max_depth` in this filesystem profile to cap Linux glob expansion and silence this warning, or enumerate explicit depths such as `*.env`, `*/*.env`, and `*/*/*.env`."
                        ),
                    );
                }
            }
            for (path, permission) in &filesystem.entries {
                entries.extend(compile_filesystem_permission(
                    path,
                    permission,
                    policy_cwd,
                    startup_warnings,
                )?);
            }
        }
    } else {
        push_warning(
            startup_warnings,
            missing_filesystem_entries_warning(profile_name),
        );
    }
    let glob_scan_max_depth = validate_glob_scan_max_depth(
        profile
            .filesystem
            .as_ref()
            .and_then(|filesystem| filesystem.glob_scan_max_depth),
    )?;

    let network_sandbox_policy = compile_network_sandbox_policy(profile.network.as_ref());
    let mut file_system_sandbox_policy = FileSystemSandboxPolicy::restricted(entries);
    file_system_sandbox_policy.glob_scan_max_depth = glob_scan_max_depth;
    Ok((file_system_sandbox_policy, network_sandbox_policy))
}

pub(crate) fn compile_permission_profile_selection(
    permissions: Option<&PermissionsToml>,
    profile_name: &str,
    workspace_write: Option<&SandboxWorkspaceWrite>,
    policy_cwd: &Path,
    startup_warnings: &mut Vec<String>,
) -> io::Result<(FileSystemSandboxPolicy, NetworkSandboxPolicy)> {
    if let Some(permission_profile) = builtin_permission_profile(profile_name, workspace_write) {
        return Ok(permission_profile.to_runtime_permissions());
    }
    reject_unknown_builtin_permission_profile(profile_name)?;

    let permissions = permissions.ok_or_else(|| {
        io::Error::new(
            io::ErrorKind::InvalidInput,
            "default_permissions requires a `[permissions]` table",
        )
    })?;
    compile_permission_profile(permissions, profile_name, policy_cwd, startup_warnings)
}

fn reject_unknown_builtin_permission_profile(profile_name: &str) -> io::Result<()> {
    if profile_name.starts_with(':') {
        return Err(io::Error::new(
            io::ErrorKind::InvalidInput,
            format!("default_permissions refers to unknown built-in profile `{profile_name}`"),
        ));
    }

    Ok(())
}

/// Returns a list of paths that must be readable by shell tools in order
/// for Codex to function. These should always be added to the
/// `FileSystemSandboxPolicy` for a thread.
pub(crate) fn get_readable_roots_required_for_codex_runtime(
    codex_home: &Path,
    zsh_path: Option<&PathBuf>,
    main_execve_wrapper_exe: Option<&PathBuf>,
) -> Vec<AbsolutePathBuf> {
    let arg0_root = AbsolutePathBuf::from_absolute_path(codex_home.join("tmp").join("arg0")).ok();
    let zsh_path = zsh_path.and_then(|path| AbsolutePathBuf::from_absolute_path(path).ok());
    let execve_wrapper_root = main_execve_wrapper_exe.and_then(|path| {
        let path = AbsolutePathBuf::from_absolute_path(path).ok()?;
        if let Some(arg0_root) = arg0_root.as_ref()
            && path.as_path().starts_with(arg0_root.as_path())
        {
            path.parent()
        } else {
            Some(path)
        }
    });

    let mut readable_roots = Vec::new();
    if let Some(zsh_path) = zsh_path {
        readable_roots.push(zsh_path);
    }
    if let Some(execve_wrapper_root) = execve_wrapper_root {
        readable_roots.push(execve_wrapper_root);
    }
    readable_roots
}

fn compile_network_sandbox_policy(network: Option<&NetworkToml>) -> NetworkSandboxPolicy {
    let Some(network) = network else {
        return NetworkSandboxPolicy::Restricted;
    };

    match network.enabled {
        Some(true) => NetworkSandboxPolicy::Enabled,
        _ => NetworkSandboxPolicy::Restricted,
    }
}

fn compile_filesystem_permission(
    path: &str,
    permission: &FilesystemPermissionToml,
    policy_cwd: &Path,
    startup_warnings: &mut Vec<String>,
) -> io::Result<Vec<FileSystemSandboxEntry>> {
    let mut entries = Vec::new();
    match permission {
        FilesystemPermissionToml::Access(access) => {
            entries.push(FileSystemSandboxEntry {
                path: compile_filesystem_access_path(path, *access, startup_warnings)?,
                access: *access,
            });
        }
        FilesystemPermissionToml::Scoped(scoped_entries) => {
            for (subpath, access) in scoped_entries {
                let has_glob = contains_glob_chars(subpath);
                let can_compile_as_pattern = match parse_special_path(path) {
                    Some(FileSystemSpecialPath::ProjectRoots { .. }) | None => true,
                    Some(_) => false,
                };
                if has_glob && *access == FileSystemAccessMode::None && can_compile_as_pattern {
                    // Scoped glob syntax is a first-class filesystem policy
                    // pattern entry. Literal scoped paths continue through the
                    // exact-path parser so existing path semantics stay intact.
                    let entry = FileSystemSandboxEntry {
                        path: FileSystemPath::GlobPattern {
                            pattern: compile_scoped_filesystem_pattern(
                                path, subpath, *access, policy_cwd,
                            )?,
                        },
                        access: *access,
                    };
                    entries.push(entry);
                } else {
                    let subpath = compile_read_write_glob_path(subpath, *access)?;
                    entries.push(FileSystemSandboxEntry {
                        path: compile_scoped_filesystem_path(path, subpath, startup_warnings)?,
                        access: *access,
                    });
                }
            }
        }
    }
    Ok(entries)
}

fn compile_filesystem_access_path(
    path: &str,
    access: FileSystemAccessMode,
    startup_warnings: &mut Vec<String>,
) -> io::Result<FileSystemPath> {
    if !contains_glob_chars(path) {
        return compile_filesystem_path(path, startup_warnings);
    }

    if access == FileSystemAccessMode::None {
        // At this point `path` is an unscoped filesystem table key. Top-level
        // glob deny entries still go through the absolute-path parser before
        // becoming policy patterns; relative project-root glob syntax is
        // handled by `compile_scoped_filesystem_pattern`.
        return Ok(FileSystemPath::GlobPattern {
            pattern: parse_absolute_path(path)?.to_string_lossy().into_owned(),
        });
    }

    let path = compile_read_write_glob_path(path, access)?;
    compile_filesystem_path(path, startup_warnings)
}

fn compile_filesystem_path(
    path: &str,
    startup_warnings: &mut Vec<String>,
) -> io::Result<FileSystemPath> {
    if let Some(special) = parse_special_path(path) {
        maybe_push_unknown_special_path_warning(&special, startup_warnings);
        return Ok(FileSystemPath::Special { value: special });
    }

    let path = parse_absolute_path(path)?;
    Ok(FileSystemPath::Path { path })
}

fn compile_scoped_filesystem_path(
    path: &str,
    subpath: &str,
    startup_warnings: &mut Vec<String>,
) -> io::Result<FileSystemPath> {
    if subpath == "." {
        return compile_filesystem_path(path, startup_warnings);
    }

    if let Some(special) = parse_special_path(path) {
        let subpath = parse_relative_subpath(subpath)?;
        let special = match special {
            FileSystemSpecialPath::ProjectRoots { .. } => Ok(FileSystemPath::Special {
                value: FileSystemSpecialPath::project_roots(Some(subpath)),
            }),
            FileSystemSpecialPath::Unknown { path, .. } => Ok(FileSystemPath::Special {
                value: FileSystemSpecialPath::unknown(path, Some(subpath)),
            }),
            _ => Err(io::Error::new(
                io::ErrorKind::InvalidInput,
                format!("filesystem path `{path}` does not support nested entries"),
            )),
        }?;
        if let FileSystemPath::Special { value } = &special {
            maybe_push_unknown_special_path_warning(value, startup_warnings);
        }
        return Ok(special);
    }

    let subpath = parse_relative_subpath(subpath)?;
    let base = parse_absolute_path(path)?;
    let path = AbsolutePathBuf::resolve_path_against_base(&subpath, base.as_path());
    Ok(FileSystemPath::Path { path })
}

fn compile_scoped_filesystem_pattern(
    path: &str,
    subpath: &str,
    access: FileSystemAccessMode,
    policy_cwd: &Path,
) -> io::Result<String> {
    // Pattern entries currently mean deny-read only. Supporting broader access
    // modes here would imply glob-based read/write allow semantics that the
    // sandbox policy does not express yet.
    if access != FileSystemAccessMode::None {
        return Err(io::Error::new(
            io::ErrorKind::InvalidInput,
            format!("filesystem glob subpath `{subpath}` only supports `none` access"),
        ));
    }
    let subpath = parse_relative_subpath(subpath)?;

    match parse_special_path(path) {
        Some(FileSystemSpecialPath::ProjectRoots { .. }) => {
            // `:project_roots` is represented as a special path, but current
            // filesystem-policy resolution defines it relative to the session
            // cwd. Use the same policy cwd here so glob entries and exact
            // scoped entries resolve consistently.
            Ok(
                AbsolutePathBuf::resolve_path_against_base(&subpath, policy_cwd)
                    .to_string_lossy()
                    .to_string(),
            )
        }
        Some(_) => Err(io::Error::new(
            io::ErrorKind::InvalidInput,
            format!("filesystem path `{path}` does not support nested entries"),
        )),
        None => {
            let base = parse_absolute_path(path)?;
            Ok(base.join(&subpath).to_string_lossy().to_string())
        }
    }
}

fn compile_read_write_glob_path(path: &str, access: FileSystemAccessMode) -> io::Result<&str> {
    if !contains_glob_chars(path) {
        return Ok(path);
    }

    let path_without_trailing_glob = remove_trailing_glob_suffix(path);
    if !contains_glob_chars(path_without_trailing_glob) {
        return Ok(path_without_trailing_glob);
    }

    Err(io::Error::new(
        io::ErrorKind::InvalidInput,
        format!(
            "filesystem glob path `{path}` only supports `none` access; use an exact path or trailing `/**` for `{access}` subtree access"
        ),
    ))
}

fn unsupported_read_write_glob_paths(filesystem: &FilesystemPermissionsToml) -> Vec<String> {
    let mut patterns = Vec::new();
    for (path, permission) in &filesystem.entries {
        match permission {
            FilesystemPermissionToml::Access(access) => {
                if *access != FileSystemAccessMode::None
                    && contains_glob_chars(remove_trailing_glob_suffix(path))
                {
                    patterns.push(path.clone());
                }
            }
            FilesystemPermissionToml::Scoped(scoped_entries) => {
                for (subpath, access) in scoped_entries {
                    if *access != FileSystemAccessMode::None
                        && contains_glob_chars(remove_trailing_glob_suffix(subpath))
                    {
                        patterns.push(format!("{path}/{subpath}"));
                    }
                }
            }
        }
    }
    patterns
}

fn unbounded_unreadable_globstar_paths(filesystem: &FilesystemPermissionsToml) -> Vec<String> {
    if filesystem.glob_scan_max_depth.is_some() {
        return Vec::new();
    }

    let mut patterns = Vec::new();
    for (path, permission) in &filesystem.entries {
        match permission {
            FilesystemPermissionToml::Access(FileSystemAccessMode::None) => {
                if path.contains("**") {
                    patterns.push(path.clone());
                }
            }
            FilesystemPermissionToml::Access(_) => {}
            FilesystemPermissionToml::Scoped(scoped_entries) => {
                for (subpath, access) in scoped_entries {
                    if *access == FileSystemAccessMode::None && subpath.contains("**") {
                        patterns.push(format!("{path}/{subpath}"));
                    }
                }
            }
        }
    }
    patterns
}

fn validate_glob_scan_max_depth(max_depth: Option<usize>) -> io::Result<Option<usize>> {
    match max_depth {
        Some(0) => Err(io::Error::new(
            io::ErrorKind::InvalidInput,
            "glob_scan_max_depth must be at least 1",
        )),
        _ => Ok(max_depth),
    }
}

fn contains_glob_chars(path: &str) -> bool {
    contains_glob_chars_for_platform(path, cfg!(windows))
}

fn contains_glob_chars_for_platform(path: &str, is_windows: bool) -> bool {
    let normalized_windows_path = if is_windows {
        normalize_windows_device_path(path)
    } else {
        None
    };
    let path = normalized_windows_path.as_deref().unwrap_or(path);
    path.chars().any(|ch| matches!(ch, '*' | '?' | '[' | ']'))
}

fn remove_trailing_glob_suffix(path: &str) -> &str {
    path.strip_suffix("/**").unwrap_or(path)
}

// WARNING: keep this parser forward-compatible.
// Adding a new `:special_path` must not make older Codex versions reject the
// config. Unknown values intentionally round-trip through
// `FileSystemSpecialPath::Unknown` so they can be surfaced as warnings and
// ignored, rather than aborting config load.
fn parse_special_path(path: &str) -> Option<FileSystemSpecialPath> {
    match path {
        ":root" => Some(FileSystemSpecialPath::Root),
        ":minimal" => Some(FileSystemSpecialPath::Minimal),
        ":project_roots" => Some(FileSystemSpecialPath::project_roots(/*subpath*/ None)),
        ":tmpdir" => Some(FileSystemSpecialPath::Tmpdir),
        _ if path.starts_with(':') => {
            Some(FileSystemSpecialPath::unknown(path, /*subpath*/ None))
        }
        _ => None,
    }
}

fn parse_absolute_path(path: &str) -> io::Result<AbsolutePathBuf> {
    parse_absolute_path_for_platform(path, cfg!(windows))
}

fn parse_absolute_path_for_platform(path: &str, is_windows: bool) -> io::Result<AbsolutePathBuf> {
    let path_ref = normalize_absolute_path_for_platform(path, is_windows);
    if !is_absolute_path_for_platform(path, path_ref.as_ref(), is_windows)
        && path != "~"
        && !path.starts_with("~/")
    {
        return Err(io::Error::new(
            io::ErrorKind::InvalidInput,
            format!("filesystem path `{path}` must be absolute, use `~/...`, or start with `:`"),
        ));
    }
    AbsolutePathBuf::from_absolute_path(path_ref.as_ref())
}

fn is_absolute_path_for_platform(path: &str, normalized_path: &Path, is_windows: bool) -> bool {
    if is_windows {
        is_windows_absolute_path(path)
            || is_windows_absolute_path(&normalized_path.to_string_lossy())
    } else {
        normalized_path.is_absolute()
    }
}

fn normalize_absolute_path_for_platform(path: &str, is_windows: bool) -> Cow<'_, Path> {
    if !is_windows {
        return Cow::Borrowed(Path::new(path));
    }

    match normalize_windows_device_path(path) {
        Some(normalized) => Cow::Owned(PathBuf::from(normalized)),
        None => Cow::Borrowed(Path::new(path)),
    }
}

fn normalize_windows_device_path(path: &str) -> Option<String> {
    if let Some(unc) = path.strip_prefix(r"\\?\UNC\") {
        return Some(format!(r"\\{unc}"));
    }
    if let Some(unc) = path.strip_prefix(r"\\.\UNC\") {
        return Some(format!(r"\\{unc}"));
    }
    if let Some(path) = path.strip_prefix(r"\\?\")
        && is_windows_drive_absolute_path(path)
    {
        return Some(path.to_string());
    }
    if let Some(path) = path.strip_prefix(r"\\.\")
        && is_windows_drive_absolute_path(path)
    {
        return Some(path.to_string());
    }
    None
}

fn is_windows_absolute_path(path: &str) -> bool {
    is_windows_drive_absolute_path(path) || path.starts_with(r"\\")
}

fn is_windows_drive_absolute_path(path: &str) -> bool {
    let bytes = path.as_bytes();
    bytes.len() >= 3
        && bytes[0].is_ascii_alphabetic()
        && bytes[1] == b':'
        && matches!(bytes[2], b'\\' | b'/')
}

fn parse_relative_subpath(subpath: &str) -> io::Result<PathBuf> {
    let path = Path::new(subpath);
    if !subpath.is_empty()
        && path
            .components()
            .all(|component| matches!(component, Component::Normal(_)))
    {
        return Ok(path.to_path_buf());
    }

    Err(io::Error::new(
        io::ErrorKind::InvalidInput,
        format!(
            "filesystem subpath `{}` must be a descendant path without `.` or `..` components",
            path.display()
        ),
    ))
}

fn push_warning(startup_warnings: &mut Vec<String>, message: String) {
    tracing::warn!("{message}");
    startup_warnings.push(message);
}

fn missing_filesystem_entries_warning(profile_name: &str) -> String {
    format!(
        "Permissions profile `{profile_name}` does not define any recognized filesystem entries for this version of Codex. Filesystem access will remain restricted. Upgrade Codex if this profile expects filesystem permissions."
    )
}

fn maybe_push_unknown_special_path_warning(
    special: &FileSystemSpecialPath,
    startup_warnings: &mut Vec<String>,
) {
    let FileSystemSpecialPath::Unknown { path, subpath } = special else {
        return;
    };
    push_warning(
        startup_warnings,
        match subpath.as_deref() {
            Some(subpath) => format!(
                "Configured filesystem path `{path}` with nested entry `{}` is not recognized by this version of Codex and will be ignored. Upgrade Codex if this path is required.",
                subpath.display()
            ),
            None => format!(
                "Configured filesystem path `{path}` is not recognized by this version of Codex and will be ignored. Upgrade Codex if this path is required."
            ),
        },
    );
}

#[cfg(test)]
#[path = "permissions_tests.rs"]
mod tests;
