//! Pre-engine project validation, recent-project discovery, and sandbox scaffolding.

use std::collections::HashSet;
use std::path::{Component, Path, PathBuf};
use std::process::Command;

use bevy::app::AppExit;
use bevy::prelude::*;
use bevy::window::{CompositeAlphaMode, PresentMode, WindowPlugin};
use bevy_egui::{egui, EguiContext, EguiPlugin, EguiPrimaryContextPass, PrimaryEguiContext};
use settings::{
    save_project_settings_to_string, ProjectKind, ProjectSettings, DEFAULT_PROJECT_PATH,
    PROJECT_TEMPLATE_VERSION,
};

use crate::project_io::UserPreferences;
use crate::scene::recovery::atomic_write;
use crate::ui::theme::{
    apply_editor_theme, ACCENT, BORDER, CANVAS_BG, PANEL_BG, PANEL_BG_DARK, SELECTION, TEXT,
    TEXT_DIM, TEXT_MUTED,
};

const SANDBOX_DIRECTORIES: &[&str] = &[
    "assets/.index",
    "assets/.trash",
    "assets/audio",
    "assets/build_profiles",
    "assets/levels",
    "assets/materials",
    "assets/meshes/generated",
    "assets/models",
    "assets/post_fx",
    "assets/rendering_profiles",
    "assets/shaders",
    "assets/textures",
];

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProjectSummary {
    pub root: PathBuf,
    pub project_id: String,
    pub name: String,
    pub project_kind: ProjectKind,
    pub template_version: u32,
    pub capabilities: Vec<String>,
    pub default_level: PathBuf,
    pub last_scene: Option<PathBuf>,
}

#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct StartupArguments {
    pub project: Option<PathBuf>,
    pub help: bool,
}

#[derive(Resource)]
struct ProjectLauncherUi {
    recents: Vec<ProjectSummary>,
    sandbox_name: String,
    sandbox_destination: Option<PathBuf>,
    status: Option<String>,
}

impl Default for ProjectLauncherUi {
    fn default() -> Self {
        let mut preferences = crate::project_io::read_user_preferences();
        if let Ok(root) = std::env::current_dir().and_then(|root| root.canonicalize()) {
            crate::project_io::remember_project_root(&mut preferences, &root);
        }
        Self {
            recents: recent_project_summaries(&preferences),
            sandbox_name: "New Sandbox".to_string(),
            sandbox_destination: None,
            status: None,
        }
    }
}

pub fn run_project_launcher() {
    let mut app = App::new();
    app.add_plugins(DefaultPlugins.set(WindowPlugin {
        primary_window: Some(Window {
            title: "Blacksite Project Browser".to_string(),
            name: Some("blacksite-project-launcher".to_string()),
            resolution: (960, 620).into(),
            present_mode: PresentMode::AutoVsync,
            transparent: false,
            composite_alpha_mode: CompositeAlphaMode::Opaque,
            ..default()
        }),
        ..default()
    }))
    .add_plugins(EguiPlugin::default())
    .init_resource::<ProjectLauncherUi>()
    .add_systems(Startup, spawn_project_launcher_camera)
    .add_systems(EguiPrimaryContextPass, project_launcher_ui_system);
    app.run();
}

fn spawn_project_launcher_camera(mut commands: Commands) {
    commands.spawn((Camera2d, PrimaryEguiContext));
}

pub fn spawn_project_launcher_process() -> Result<(), String> {
    let executable = sibling_executable("project_launcher")?;
    let root = std::env::current_dir()
        .map_err(|error| format!("could not resolve active project root: {error}"))?;
    spawn_detached(&executable, &[], &root)
}

pub fn reveal_project_root(root: &Path) -> Result<(), String> {
    Command::new("xdg-open")
        .arg(root)
        .spawn()
        .map_err(|error| format!("could not reveal {}: {error}", root.display()))?;
    Ok(())
}

fn project_launcher_ui_system(world: &mut World) {
    let Ok(context) = world
        .query_filtered::<&mut EguiContext, With<PrimaryEguiContext>>()
        .single(world)
    else {
        return;
    };
    let mut context = context.clone();
    world.resource_scope(|world, mut state: Mut<ProjectLauncherUi>| {
        project_launcher_ui(world, context.get_mut(), &mut state);
    });
}

#[expect(
    deprecated,
    reason = "bevy_egui exposes a Context during EguiPrimaryContextPass, requiring one top-level context-based panel"
)]
fn project_launcher_ui(world: &mut World, context: &egui::Context, state: &mut ProjectLauncherUi) {
    apply_editor_theme(context);
    egui::CentralPanel::default()
        .frame(
            egui::Frame::new()
                .fill(CANVAS_BG)
                .inner_margin(egui::Margin::same(24)),
        )
        .show(context, |ui| {
            let content_width = ui.available_width().min(1240.0);
            let side_space = ((ui.available_width() - content_width) * 0.5).max(0.0);
            ui.horizontal(|ui| {
                ui.add_space(side_space);
                ui.vertical(|ui| {
                    ui.set_width(content_width);
                    ui.horizontal(|ui| {
                        ui.vertical(|ui| {
                            ui.label(
                                egui::RichText::new("BLACKSITE")
                                    .size(22.0)
                                    .strong()
                                    .color(ACCENT),
                            );
                            ui.label(
                                egui::RichText::new("PROJECT BROWSER")
                                    .size(11.0)
                                    .color(TEXT_DIM),
                            );
                        });
                        ui.add_space(18.0);
                        ui.separator();
                        ui.add_space(10.0);
                        ui.label(
                            egui::RichText::new(
                                "Choose a validated workspace before the engine starts",
                            )
                            .color(TEXT_DIM),
                        );
                    });
                    ui.add_space(22.0);

                    ui.horizontal(|ui| {
                        ui.vertical(|ui| {
                            ui.set_width(340.0);
                            launcher_actions(world, ui, state);
                        });
                        ui.add_space(18.0);
                        ui.vertical(|ui| {
                            ui.set_width((content_width - 358.0).max(360.0));
                            recent_projects(world, ui, state);
                        });
                    });

                    if let Some(status) = state.status.as_deref() {
                        ui.add_space(12.0);
                        ui.separator();
                        ui.label(egui::RichText::new(status).color(TEXT_DIM));
                    }
                });
            });
        });
}

fn launcher_actions(world: &mut World, ui: &mut egui::Ui, state: &mut ProjectLauncherUi) {
    egui::Frame::new()
        .fill(PANEL_BG_DARK)
        .stroke(egui::Stroke::new(1.0, BORDER))
        .inner_margin(egui::Margin::same(16))
        .show(ui, |ui| {
            ui.set_min_height(455.0);
            ui.heading("Open Project");
            ui.label(
                egui::RichText::new("Select a root containing assets/project.ron").color(TEXT_DIM),
            );
            ui.add_space(8.0);
            if ui.button("Browse Existing...").clicked() {
                if let Some(root) = rfd::FileDialog::new().pick_folder() {
                    open_project_from_launcher(world, state, root);
                }
            }

            ui.add_space(24.0);
            ui.separator();
            ui.add_space(14.0);
            ui.heading("New Sandbox");
            ui.label(
                egui::RichText::new("Creates a clean content workspace with a valid empty level")
                    .color(TEXT_DIM),
            );
            ui.add_space(8.0);
            ui.label(egui::RichText::new("Project name").color(TEXT_DIM));
            ui.text_edit_singleline(&mut state.sandbox_name);
            ui.add_space(6.0);
            let destination = state
                .sandbox_destination
                .as_ref()
                .map(|path| path.display().to_string())
                .unwrap_or_else(|| "No destination selected".to_string());
            ui.add(
                egui::Label::new(
                    egui::RichText::new(destination)
                        .monospace()
                        .color(TEXT_MUTED),
                )
                .truncate(),
            );
            if ui.button("Choose Empty Folder...").clicked() {
                state.sandbox_destination = rfd::FileDialog::new().pick_folder();
            }
            let can_create =
                state.sandbox_destination.is_some() && !state.sandbox_name.trim().is_empty();
            if ui
                .add_enabled(can_create, egui::Button::new("Create And Open Sandbox"))
                .clicked()
            {
                let destination = state.sandbox_destination.clone().unwrap();
                match scaffold_sandbox_project(&destination, &state.sandbox_name) {
                    Ok(summary) => launch_editor_and_close(world, state, &summary),
                    Err(error) => state.status = Some(error),
                }
            }
        });
}

fn recent_projects(world: &mut World, ui: &mut egui::Ui, state: &mut ProjectLauncherUi) {
    ui.horizontal(|ui| {
        ui.heading("Recent Projects");
        ui.add_space(8.0);
        ui.label(egui::RichText::new(state.recents.len().to_string()).color(TEXT_MUTED));
    });
    ui.label(
        egui::RichText::new("Missing and invalid projects are filtered automatically")
            .color(TEXT_DIM),
    );
    ui.add_space(10.0);
    if state.recents.is_empty() {
        egui::Frame::new()
            .fill(PANEL_BG)
            .stroke(egui::Stroke::new(1.0, BORDER))
            .inner_margin(egui::Margin::same(16))
            .show(ui, |ui| {
                ui.label(egui::RichText::new("No valid recent projects").color(TEXT));
                ui.label(
                    egui::RichText::new("Browse an existing project or create a sandbox.")
                        .color(TEXT_DIM),
                );
            });
        return;
    }

    let recents = state.recents.clone();
    egui::ScrollArea::vertical()
        .id_salt("project_launcher_recents")
        .show(ui, |ui| {
            for summary in recents {
                let response = egui::Frame::new()
                    .fill(PANEL_BG)
                    .stroke(egui::Stroke::new(1.0, BORDER))
                    .inner_margin(egui::Margin::same(14))
                    .show(ui, |ui| {
                        ui.horizontal(|ui| {
                            ui.vertical(|ui| {
                                ui.label(egui::RichText::new(&summary.name).strong().color(TEXT));
                                ui.label(
                                    egui::RichText::new(summary.root.display().to_string())
                                        .monospace()
                                        .size(11.0)
                                        .color(TEXT_MUTED),
                                );
                                let detail = summary.last_scene.as_ref().map_or_else(
                                    || format!("{:?}  |  default scene", summary.project_kind),
                                    |scene| {
                                        format!(
                                            "{:?}  |  {}",
                                            summary.project_kind,
                                            scene
                                                .strip_prefix(&summary.root)
                                                .unwrap_or(scene)
                                                .display()
                                        )
                                    },
                                );
                                ui.label(egui::RichText::new(detail).size(11.0).color(TEXT_DIM));
                            });
                            ui.with_layout(
                                egui::Layout::right_to_left(egui::Align::Center),
                                |ui| {
                                    if ui.button("Open").clicked() {
                                        launch_editor_and_close(world, state, &summary);
                                    }
                                },
                            );
                        });
                    });
                if response.response.hovered() {
                    ui.painter().rect_stroke(
                        response.response.rect,
                        0.0,
                        egui::Stroke::new(1.0, SELECTION),
                        egui::StrokeKind::Inside,
                    );
                }
                ui.add_space(8.0);
            }
        });
}

fn open_project_from_launcher(world: &mut World, state: &mut ProjectLauncherUi, root: PathBuf) {
    match inspect_project(&root, None) {
        Ok(summary) => launch_editor_and_close(world, state, &summary),
        Err(error) => state.status = Some(error),
    }
}

fn launch_editor_and_close(
    world: &mut World,
    state: &mut ProjectLauncherUi,
    summary: &ProjectSummary,
) {
    match spawn_editor_process(&summary.root) {
        Ok(()) => {
            state.status = Some(format!("Opening {}", summary.name));
            world.write_message(AppExit::Success);
        }
        Err(error) => state.status = Some(error),
    }
}

fn spawn_editor_process(root: &Path) -> Result<(), String> {
    let executable = sibling_executable("editor")?;
    spawn_detached(
        &executable,
        &[std::ffi::OsStr::new("--project"), root.as_os_str()],
        root,
    )
}

#[cfg(target_os = "linux")]
fn spawn_detached(
    executable: &Path,
    arguments: &[&std::ffi::OsStr],
    cwd: &Path,
) -> Result<(), String> {
    let mut command = Command::new("systemd-run");
    command
        .args(["--user", "--collect", "--quiet", "--service-type=exec"])
        .arg(format!("--working-directory={}", cwd.display()));
    for variable in [
        "PATH",
        "WAYLAND_DISPLAY",
        "DISPLAY",
        "XAUTHORITY",
        "XDG_RUNTIME_DIR",
        "HYPRLAND_INSTANCE_SIGNATURE",
        "DBUS_SESSION_BUS_ADDRESS",
        "XDG_CURRENT_DESKTOP",
        "XDG_SESSION_DESKTOP",
        "XDG_SESSION_TYPE",
        "XDG_DATA_HOME",
        "XDG_CONFIG_HOME",
        "XDG_STATE_HOME",
        "GDK_BACKEND",
        "LD_LIBRARY_PATH",
        "WGPU_VALIDATION",
        "BEVY_FPS_HDR",
    ] {
        if let Some(value) = std::env::var_os(variable) {
            command.arg(format!("--setenv={variable}={}", value.to_string_lossy()));
        }
    }
    let status = command
        .arg(executable)
        .args(arguments)
        .status()
        .map_err(|error| format!("could not invoke systemd-run: {error}"))?;
    status.success().then_some(()).ok_or_else(|| {
        format!(
            "systemd-run could not start {} (status {status})",
            executable.display()
        )
    })
}

#[cfg(not(target_os = "linux"))]
fn spawn_detached(
    executable: &Path,
    arguments: &[&std::ffi::OsStr],
    cwd: &Path,
) -> Result<(), String> {
    Command::new(executable)
        .args(arguments)
        .current_dir(cwd)
        .spawn()
        .map_err(|error| format!("could not start {}: {error}", executable.display()))?;
    Ok(())
}

fn sibling_executable(name: &str) -> Result<PathBuf, String> {
    let current = std::env::current_exe()
        .map_err(|error| format!("could not locate current executable: {error}"))?;
    let extension = current
        .extension()
        .map(|extension| extension.to_os_string());
    let mut executable = current.with_file_name(name);
    if let Some(extension) = extension {
        executable.set_extension(extension);
    }
    executable
        .is_file()
        .then_some(executable.clone())
        .ok_or_else(|| format!("required executable is not built: {}", executable.display()))
}

pub fn parse_startup_arguments(
    arguments: impl IntoIterator<Item = String>,
) -> Result<StartupArguments, String> {
    let mut parsed = StartupArguments::default();
    let mut arguments = arguments.into_iter();
    while let Some(argument) = arguments.next() {
        match argument.as_str() {
            "--project" => {
                if parsed.project.is_some() {
                    return Err("--project may only be provided once".to_string());
                }
                let path = arguments
                    .next()
                    .ok_or_else(|| "--project requires a project root path".to_string())?;
                parsed.project = Some(PathBuf::from(path));
            }
            "--help" | "-h" => parsed.help = true,
            unknown => return Err(format!("unknown editor argument `{unknown}`")),
        }
    }
    Ok(parsed)
}

pub fn command_line_help() -> &'static str {
    "Blacksite Editor\n\nUSAGE:\n    editor [--project <root>]\n\nOPTIONS:\n    --project <root>  Open a validated Blacksite project before engine startup\n    -h, --help        Print this help"
}

pub fn activate_project_root(root: &Path) -> Result<ProjectSummary, String> {
    let summary = inspect_project(root, None)?;
    std::env::set_current_dir(&summary.root).map_err(|error| {
        format!(
            "could not activate project root {}: {error}",
            summary.root.display()
        )
    })?;
    Ok(summary)
}

pub fn inspect_project(root: &Path, last_scene: Option<&Path>) -> Result<ProjectSummary, String> {
    let root = root
        .canonicalize()
        .map_err(|error| format!("could not open project root {}: {error}", root.display()))?;
    if !root.is_dir() {
        return Err(format!(
            "project root is not a directory: {}",
            root.display()
        ));
    }
    let manifest_path = root.join(DEFAULT_PROJECT_PATH);
    let text = std::fs::read_to_string(&manifest_path)
        .map_err(|error| format!("could not read {}: {error}", manifest_path.display()))?;
    let manifest: ProjectSettings = ron::from_str(&text).map_err(|error| {
        format!(
            "invalid project manifest {}: {error}",
            manifest_path.display()
        )
    })?;
    if manifest.name.trim().is_empty() {
        return Err(format!(
            "project name is empty in {}",
            manifest_path.display()
        ));
    }
    manifest.audio.validate().map_err(|error| {
        format!(
            "invalid audio settings in {}: {error}",
            manifest_path.display()
        )
    })?;
    let default_reference = Path::new(&manifest.default_level);
    if !default_reference.starts_with("assets/levels")
        || !manifest.default_level.ends_with(".scn.ron")
    {
        return Err("default_level must be a .scn.ron scene under assets/levels/".into());
    }
    let default_level = safe_project_path(&root, default_reference, "default_level")?;
    let default_metadata = std::fs::symlink_metadata(&default_level).ok();
    if !default_metadata
        .as_ref()
        .is_some_and(std::fs::Metadata::is_file)
        || default_metadata.is_some_and(|metadata| metadata.file_type().is_symlink())
    {
        return Err(format!(
            "default level is missing or is not a regular file: {}",
            default_level.display()
        ));
    }
    let default_level_text = std::fs::read_to_string(&default_level).map_err(|error| {
        format!(
            "could not read default level {}: {error}",
            default_level.display()
        )
    })?;
    scene::validate_level_text(&default_level_text).map_err(|error| {
        format!(
            "default level failed validation {}: {error}",
            default_level.display()
        )
    })?;
    scene::validate_composition_graph(&default_level, &root).map_err(|error| {
        format!(
            "default level composition failed validation {}: {error}",
            default_level.display()
        )
    })?;

    let last_scene = last_scene
        .and_then(|path| safe_project_path(&root, path, "last_scene").ok())
        .filter(|path| path.is_file());
    let project_id = if manifest.project_id.trim().is_empty() {
        format!("legacy:{}", root.display())
    } else {
        manifest.project_id
    };
    Ok(ProjectSummary {
        root,
        project_id,
        name: manifest.name,
        project_kind: manifest.project_kind,
        template_version: manifest.template_version,
        capabilities: manifest.capabilities,
        default_level,
        last_scene,
    })
}

pub fn recent_project_summaries(preferences: &UserPreferences) -> Vec<ProjectSummary> {
    let mut seen = HashSet::new();
    preferences
        .recent_projects
        .iter()
        .filter_map(|root| {
            let root = PathBuf::from(root);
            let last_scene = preferences
                .recent_levels
                .iter()
                .map(PathBuf::from)
                .find(|scene| root.join(scene).is_file());
            inspect_project(&root, last_scene.as_deref()).ok()
        })
        .filter(|summary| seen.insert(summary.root.clone()))
        .collect()
}

pub fn scaffold_sandbox_project(destination: &Path, name: &str) -> Result<ProjectSummary, String> {
    let name = name.trim();
    if name.is_empty() {
        return Err("sandbox project name cannot be empty".to_string());
    }
    if destination.exists() {
        let mut entries = std::fs::read_dir(destination)
            .map_err(|error| format!("could not inspect {}: {error}", destination.display()))?;
        if entries.next().is_some() {
            return Err(format!(
                "sandbox destination must be empty: {}",
                destination.display()
            ));
        }
    }
    for directory in SANDBOX_DIRECTORIES {
        std::fs::create_dir_all(destination.join(directory)).map_err(|error| {
            format!(
                "could not create sandbox directory {}: {error}",
                destination.join(directory).display()
            )
        })?;
    }

    let default_level = "assets/levels/main.scn.ron";
    let empty_scene = format!(
        "(schema_version: {},\n  resources: {{}},\n  entities: {{}},\n)\n",
        scene::CURRENT_SCENE_SCHEMA_VERSION
    );
    scene::validate_level_text(&empty_scene)?;
    atomic_write(&destination.join(default_level), empty_scene.as_bytes())?;

    let manifest = ProjectSettings {
        project_id: uuid::Uuid::new_v4().to_string(),
        name: name.to_string(),
        project_kind: ProjectKind::Sandbox,
        template_version: PROJECT_TEMPLATE_VERSION,
        capabilities: vec![
            "editor".into(),
            "gameplay".into(),
            "rendering".into(),
            "audio".into(),
        ],
        default_level: default_level.to_string(),
        asset_roots: vec![
            "assets/audio".into(),
            "assets/levels".into(),
            "assets/materials".into(),
            "assets/meshes".into(),
            "assets/models".into(),
            "assets/textures".into(),
        ],
        ..ProjectSettings::default()
    };
    let manifest_text = save_project_settings_to_string(&manifest)
        .map_err(|error| format!("could not serialize sandbox manifest: {error}"))?;
    atomic_write(
        &destination.join(DEFAULT_PROJECT_PATH),
        manifest_text.as_bytes(),
    )?;
    for (name, profile) in sandbox_build_profiles(default_level) {
        atomic_write(
            &destination.join("assets/build_profiles").join(name),
            profile.as_bytes(),
        )?;
    }
    inspect_project(destination, None)
}

fn sandbox_build_profiles(default_level: &str) -> [(&'static str, String); 3] {
    [
        (
            "development.ron",
            format!(
                "(version:1,name:\"Development\",cargo_profile:Development,target:None,features:[],rendering_tier:\"development\",default_scene:Some(\"{default_level}\"),output_dir:\"dist/development\")\n"
            ),
        ),
        (
            "qa.ron",
            format!(
                "(version:1,name:\"QA\",cargo_profile:Qa,target:None,features:[],rendering_tier:\"representative\",default_scene:Some(\"{default_level}\"),output_dir:\"dist/qa\")\n"
            ),
        ),
        (
            "release.ron",
            format!(
                "(version:1,name:\"Release\",cargo_profile:Release,target:None,features:[],rendering_tier:\"shipping\",default_scene:Some(\"{default_level}\"),output_dir:\"dist/release\")\n"
            ),
        ),
    ]
}

fn safe_project_path(root: &Path, path: impl AsRef<Path>, field: &str) -> Result<PathBuf, String> {
    let path = path.as_ref();
    if path.as_os_str().is_empty() || path.is_absolute() {
        return Err(format!("{field} must be a non-empty project-relative path"));
    }
    if path.components().any(|component| {
        matches!(
            component,
            Component::ParentDir | Component::RootDir | Component::Prefix(_)
        )
    }) {
        return Err(format!("{field} cannot escape the project root"));
    }
    Ok(root.join(path))
}

#[cfg(test)]
mod tests {
    use super::*;

    fn temp_root(label: &str) -> PathBuf {
        std::env::temp_dir().join(format!("blacksite-{label}-{}", uuid::Uuid::new_v4()))
    }

    #[test]
    fn startup_arguments_parse_project_and_reject_unknown_flags() {
        let parsed =
            parse_startup_arguments(["--project".to_string(), "/tmp/project".to_string()]).unwrap();
        assert_eq!(parsed.project, Some(PathBuf::from("/tmp/project")));
        assert!(parse_startup_arguments(["--mystery".to_string()]).is_err());
        assert!(parse_startup_arguments(["--project".to_string()]).is_err());
    }

    #[test]
    fn sandbox_scaffold_can_be_inspected_and_reloaded() {
        let root = temp_root("sandbox");

        let created = scaffold_sandbox_project(&root, "Prototype").unwrap();
        let reopened =
            inspect_project(&root, Some(Path::new("assets/levels/main.scn.ron"))).unwrap();

        assert_eq!(created.project_id, reopened.project_id);
        assert_eq!(reopened.name, "Prototype");
        assert_eq!(reopened.project_kind, ProjectKind::Sandbox);
        assert_eq!(reopened.template_version, PROJECT_TEMPLATE_VERSION);
        assert!(reopened.capabilities.iter().any(|item| item == "audio"));
        let manifest = settings::load_project_settings_from_path(
            root.join(DEFAULT_PROJECT_PATH).to_string_lossy().as_ref(),
        );
        assert!(manifest.audio.validate().is_ok());
        assert!(manifest.audio.bus(settings::AUDIO_BUS_SFX_ID).is_some());
        assert_eq!(
            reopened.last_scene,
            Some(root.join("assets/levels/main.scn.ron"))
        );
        for directory in SANDBOX_DIRECTORIES {
            assert!(root.join(directory).is_dir(), "missing {directory}");
        }
        for profile in ["development.ron", "qa.ron", "release.ron"] {
            let text =
                std::fs::read_to_string(root.join("assets/build_profiles").join(profile)).unwrap();
            assert!(text.contains("assets/levels/main.scn.ron"), "{profile}");
            assert!(text.contains("output_dir"), "{profile}");
        }

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

    #[test]
    fn recent_projects_filter_missing_invalid_and_duplicate_roots() {
        let valid = temp_root("recent-valid");
        let invalid = temp_root("recent-invalid");
        scaffold_sandbox_project(&valid, "Valid").unwrap();
        std::fs::create_dir_all(&invalid).unwrap();
        let preferences = UserPreferences {
            recent_projects: vec![
                valid.display().to_string(),
                "/definitely/missing/blacksite-project".to_string(),
                invalid.display().to_string(),
                valid.display().to_string(),
            ],
            recent_levels: vec!["assets/levels/main.scn.ron".to_string()],
            ..Default::default()
        };

        let summaries = recent_project_summaries(&preferences);

        assert_eq!(summaries.len(), 1);
        assert_eq!(summaries[0].name, "Valid");
        assert!(summaries[0].last_scene.is_some());
        std::fs::remove_dir_all(valid).unwrap();
        std::fs::remove_dir_all(invalid).unwrap();
    }

    #[test]
    fn project_inspection_rejects_invalid_audio_bus_graphs() {
        let root = temp_root("invalid-audio-buses");
        scaffold_sandbox_project(&root, "Invalid Audio").unwrap();
        let manifest_path = root.join(DEFAULT_PROJECT_PATH);
        let mut manifest =
            settings::load_project_settings_from_path(manifest_path.to_string_lossy().as_ref());
        manifest
            .audio
            .bus_mut(settings::AUDIO_BUS_SFX_ID)
            .unwrap()
            .parent = Some("missing".into());
        std::fs::write(
            &manifest_path,
            save_project_settings_to_string(&manifest).unwrap(),
        )
        .unwrap();

        let error = inspect_project(&root, None).unwrap_err();

        assert!(error.contains("invalid audio settings"), "{error}");
        std::fs::remove_dir_all(root).unwrap();
    }

    #[test]
    fn sandbox_scaffold_refuses_nonempty_destination_and_path_escape() {
        let root = temp_root("sandbox-nonempty");
        std::fs::create_dir_all(&root).unwrap();
        std::fs::write(root.join("keep.txt"), "user data").unwrap();

        assert!(scaffold_sandbox_project(&root, "Unsafe").is_err());
        assert!(safe_project_path(&root, "../outside", "test").is_err());
        assert_eq!(
            std::fs::read_to_string(root.join("keep.txt")).unwrap(),
            "user data"
        );

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

    #[test]
    fn project_default_level_must_use_the_audited_levels_tree() {
        let root = temp_root("default-level-contract");
        scaffold_sandbox_project(&root, "Contract").unwrap();
        std::fs::create_dir_all(root.join("assets/custom")).unwrap();
        std::fs::write(
            root.join("assets/custom/start.scn.ron"),
            format!(
                "(schema_version:{},resources:{{}},entities:{{}})",
                scene::CURRENT_SCENE_SCHEMA_VERSION
            ),
        )
        .unwrap();
        let mut manifest = settings::load_project_settings_from_path(
            root.join(DEFAULT_PROJECT_PATH).to_string_lossy().as_ref(),
        );
        manifest.default_level = "assets/custom/start.scn.ron".into();
        std::fs::write(
            root.join(DEFAULT_PROJECT_PATH),
            save_project_settings_to_string(&manifest).unwrap(),
        )
        .unwrap();

        let error = inspect_project(&root, None).unwrap_err();

        assert!(error.contains("assets/levels"), "{error}");
        std::fs::remove_dir_all(root).unwrap();
    }
}
