use crate::resolved_permissions::ResolvedWindowsSandboxPermissions;
use dunce::canonicalize;
use std::collections::HashMap;
use std::collections::HashSet;
use std::path::Path;
use std::path::PathBuf;

#[derive(Debug, Default, PartialEq, Eq)]
pub struct AllowDenyPaths {
    pub allow: HashSet<PathBuf>,
    pub deny: HashSet<PathBuf>,
}

pub(crate) fn compute_allow_paths_for_permissions(
    permissions: &ResolvedWindowsSandboxPermissions,
    command_cwd: &Path,
    env_map: &HashMap<String, String>,
) -> AllowDenyPaths {
    let mut allow: HashSet<PathBuf> = HashSet::new();
    let mut deny: HashSet<PathBuf> = HashSet::new();

    let mut add_allow_path = |p: PathBuf| {
        if p.exists() {
            allow.insert(p);
        }
    };
    let mut add_deny_path = |p: PathBuf| {
        if p.exists() {
            deny.insert(p);
        }
    };

    for writable_root in permissions.writable_roots_for_cwd(command_cwd, env_map) {
        let canonical = canonicalize(&writable_root.root).unwrap_or(writable_root.root);
        add_allow_path(canonical);
        for read_only_subpath in writable_root.read_only_subpaths {
            add_deny_path(read_only_subpath);
        }
    }

    AllowDenyPaths { allow, deny }
}

#[cfg(test)]
mod tests {
    use super::*;
    use codex_protocol::models::PermissionProfile;
    use codex_protocol::permissions::NetworkSandboxPolicy;
    use codex_utils_absolute_path::AbsolutePathBuf;
    use std::fs;
    use tempfile::TempDir;

    fn workspace_write_profile(
        writable_roots: &[AbsolutePathBuf],
        exclude_tmpdir_env_var: bool,
        exclude_slash_tmp: bool,
    ) -> PermissionProfile {
        PermissionProfile::workspace_write_with(
            writable_roots,
            NetworkSandboxPolicy::Restricted,
            exclude_tmpdir_env_var,
            exclude_slash_tmp,
        )
    }

    fn workspace_roots_for(root: &Path) -> Vec<AbsolutePathBuf> {
        vec![AbsolutePathBuf::from_absolute_path(root).expect("absolute workspace root")]
    }

    fn compute_allow_paths(
        permission_profile: &PermissionProfile,
        workspace_roots: &[AbsolutePathBuf],
        command_cwd: &Path,
        env_map: &HashMap<String, String>,
    ) -> AllowDenyPaths {
        let permissions =
            ResolvedWindowsSandboxPermissions::try_from_permission_profile_for_workspace_roots(
                permission_profile,
                workspace_roots,
            )
            .expect("managed permission profile");
        compute_allow_paths_for_permissions(&permissions, command_cwd, env_map)
    }

    #[test]
    fn includes_additional_writable_roots() {
        let tmp = TempDir::new().expect("tempdir");
        let command_cwd = tmp.path().join("workspace");
        let extra_root = tmp.path().join("extra");
        let _ = fs::create_dir_all(&command_cwd);
        let _ = fs::create_dir_all(&extra_root);

        let writable_roots = vec![AbsolutePathBuf::try_from(extra_root.as_path()).unwrap()];
        let permission_profile = workspace_write_profile(
            &writable_roots,
            /*exclude_tmpdir_env_var*/ false,
            /*exclude_slash_tmp*/ false,
        );
        let workspace_roots = workspace_roots_for(command_cwd.as_path());

        let paths = compute_allow_paths(
            &permission_profile,
            workspace_roots.as_slice(),
            &command_cwd,
            &HashMap::new(),
        );

        assert!(
            paths
                .allow
                .contains(&dunce::canonicalize(&command_cwd).unwrap())
        );
        assert!(
            paths
                .allow
                .contains(&dunce::canonicalize(&extra_root).unwrap())
        );
        assert!(paths.deny.is_empty(), "no deny paths expected");
    }

    #[test]
    fn uses_runtime_workspace_roots_for_workspace_root() {
        let tmp = TempDir::new().expect("tempdir");
        let workspace_root = tmp.path().join("workspace");
        let command_cwd = workspace_root.join("subdir");
        fs::create_dir_all(&command_cwd).expect("create command cwd");

        let permission_profile = workspace_write_profile(
            &[],
            /*exclude_tmpdir_env_var*/ true,
            /*exclude_slash_tmp*/ true,
        );
        let workspace_roots = workspace_roots_for(workspace_root.as_path());

        let paths = compute_allow_paths(
            &permission_profile,
            workspace_roots.as_slice(),
            &command_cwd,
            &HashMap::new(),
        );

        assert!(
            paths
                .allow
                .contains(&dunce::canonicalize(&workspace_root).unwrap())
        );
        assert!(
            !paths
                .allow
                .contains(&dunce::canonicalize(&command_cwd).unwrap())
        );
        assert!(paths.deny.is_empty(), "no deny paths expected");
    }

    #[test]
    fn excludes_tmp_env_vars_when_requested() {
        let tmp = TempDir::new().expect("tempdir");
        let command_cwd = tmp.path().join("workspace");
        let temp_dir = tmp.path().join("temp");
        let _ = fs::create_dir_all(&command_cwd);
        let _ = fs::create_dir_all(&temp_dir);

        let permission_profile = workspace_write_profile(
            &[],
            /*exclude_tmpdir_env_var*/ true,
            /*exclude_slash_tmp*/ false,
        );
        let mut env_map = HashMap::new();
        env_map.insert("TEMP".into(), temp_dir.to_string_lossy().to_string());
        env_map.insert("TMP".into(), temp_dir.to_string_lossy().to_string());
        let workspace_roots = workspace_roots_for(command_cwd.as_path());

        let paths = compute_allow_paths(
            &permission_profile,
            workspace_roots.as_slice(),
            &command_cwd,
            &env_map,
        );

        assert!(
            paths
                .allow
                .contains(&dunce::canonicalize(&command_cwd).unwrap())
        );
        assert!(
            !paths
                .allow
                .contains(&dunce::canonicalize(&temp_dir).unwrap())
        );
        assert!(paths.deny.is_empty(), "no deny paths expected");
    }

    #[test]
    fn includes_tmp_env_vars_when_requested() {
        let tmp = TempDir::new().expect("tempdir");
        let command_cwd = tmp.path().join("workspace");
        let temp_dir = tmp.path().join("temp");
        let _ = fs::create_dir_all(&command_cwd);
        let _ = fs::create_dir_all(&temp_dir);

        let permission_profile = workspace_write_profile(
            &[],
            /*exclude_tmpdir_env_var*/ false,
            /*exclude_slash_tmp*/ false,
        );
        let mut env_map = HashMap::new();
        env_map.insert("TEMP".into(), temp_dir.to_string_lossy().to_string());
        env_map.insert("TMP".into(), temp_dir.to_string_lossy().to_string());
        let workspace_roots = workspace_roots_for(command_cwd.as_path());

        let paths = compute_allow_paths(
            &permission_profile,
            workspace_roots.as_slice(),
            &command_cwd,
            &env_map,
        );

        let expected_allow: HashSet<PathBuf> = [
            dunce::canonicalize(&command_cwd).unwrap(),
            dunce::canonicalize(&temp_dir).unwrap(),
        ]
        .into_iter()
        .collect();

        assert_eq!(expected_allow, paths.allow);
        assert!(paths.deny.is_empty(), "no deny paths expected");
    }

    #[test]
    fn ignores_unix_slash_tmp_for_windows_allow_roots() {
        let tmp = TempDir::new().expect("tempdir");
        let command_cwd = tmp.path().join("workspace");
        let _ = fs::create_dir_all(&command_cwd);

        let permission_profile = workspace_write_profile(
            &[],
            /*exclude_tmpdir_env_var*/ true,
            /*exclude_slash_tmp*/ false,
        );
        let workspace_roots = workspace_roots_for(command_cwd.as_path());

        let paths = compute_allow_paths(
            &permission_profile,
            workspace_roots.as_slice(),
            &command_cwd,
            &HashMap::new(),
        );
        let expected_allow: HashSet<PathBuf> = [dunce::canonicalize(&command_cwd).unwrap()]
            .into_iter()
            .collect();

        assert_eq!(expected_allow, paths.allow);
        assert!(paths.deny.is_empty(), "no deny paths expected");
    }

    #[test]
    fn denies_git_dir_inside_writable_root() {
        let tmp = TempDir::new().expect("tempdir");
        let command_cwd = tmp.path().join("workspace");
        let git_dir = command_cwd.join(".git");
        let _ = fs::create_dir_all(&git_dir);

        let permission_profile = workspace_write_profile(
            &[],
            /*exclude_tmpdir_env_var*/ true,
            /*exclude_slash_tmp*/ false,
        );
        let workspace_roots = workspace_roots_for(command_cwd.as_path());

        let paths = compute_allow_paths(
            &permission_profile,
            workspace_roots.as_slice(),
            &command_cwd,
            &HashMap::new(),
        );
        let expected_allow: HashSet<PathBuf> = [dunce::canonicalize(&command_cwd).unwrap()]
            .into_iter()
            .collect();
        let expected_deny: HashSet<PathBuf> = [dunce::canonicalize(&git_dir).unwrap()]
            .into_iter()
            .collect();

        assert_eq!(expected_allow, paths.allow);
        assert_eq!(expected_deny, paths.deny);
    }

    #[test]
    fn denies_git_file_inside_writable_root() {
        let tmp = TempDir::new().expect("tempdir");
        let command_cwd = tmp.path().join("workspace");
        let git_file = command_cwd.join(".git");
        let _ = fs::create_dir_all(&command_cwd);
        let _ = fs::write(&git_file, "gitdir: .git/worktrees/example");

        let permission_profile = workspace_write_profile(
            &[],
            /*exclude_tmpdir_env_var*/ true,
            /*exclude_slash_tmp*/ false,
        );
        let workspace_roots = workspace_roots_for(command_cwd.as_path());

        let paths = compute_allow_paths(
            &permission_profile,
            workspace_roots.as_slice(),
            &command_cwd,
            &HashMap::new(),
        );
        let expected_allow: HashSet<PathBuf> = [dunce::canonicalize(&command_cwd).unwrap()]
            .into_iter()
            .collect();
        let expected_deny: HashSet<PathBuf> = [dunce::canonicalize(&git_file).unwrap()]
            .into_iter()
            .collect();

        assert_eq!(expected_allow, paths.allow);
        assert_eq!(expected_deny, paths.deny);
    }

    #[test]
    fn denies_codex_and_agents_inside_writable_root() {
        let tmp = TempDir::new().expect("tempdir");
        let command_cwd = tmp.path().join("workspace");
        let codex_dir = command_cwd.join(".codex");
        let agents_dir = command_cwd.join(".agents");
        let _ = fs::create_dir_all(&codex_dir);
        let _ = fs::create_dir_all(&agents_dir);

        let permission_profile = workspace_write_profile(
            &[],
            /*exclude_tmpdir_env_var*/ true,
            /*exclude_slash_tmp*/ false,
        );
        let workspace_roots = workspace_roots_for(command_cwd.as_path());

        let paths = compute_allow_paths(
            &permission_profile,
            workspace_roots.as_slice(),
            &command_cwd,
            &HashMap::new(),
        );
        let expected_allow: HashSet<PathBuf> = [dunce::canonicalize(&command_cwd).unwrap()]
            .into_iter()
            .collect();
        let expected_deny: HashSet<PathBuf> = [
            dunce::canonicalize(&codex_dir).unwrap(),
            dunce::canonicalize(&agents_dir).unwrap(),
        ]
        .into_iter()
        .collect();

        assert_eq!(expected_allow, paths.allow);
        assert_eq!(expected_deny, paths.deny);
    }

    #[test]
    fn skips_protected_subdirs_when_missing() {
        let tmp = TempDir::new().expect("tempdir");
        let command_cwd = tmp.path().join("workspace");
        let _ = fs::create_dir_all(&command_cwd);

        let permission_profile = workspace_write_profile(
            &[],
            /*exclude_tmpdir_env_var*/ true,
            /*exclude_slash_tmp*/ false,
        );
        let workspace_roots = workspace_roots_for(command_cwd.as_path());

        let paths = compute_allow_paths(
            &permission_profile,
            workspace_roots.as_slice(),
            &command_cwd,
            &HashMap::new(),
        );
        assert_eq!(paths.allow.len(), 1);
        assert!(
            paths.deny.is_empty(),
            "no deny when protected dirs are absent"
        );
    }
}
