use std::fs;
use std::path::{Path, PathBuf};

use bevy::prelude::*;
use settings::ProjectSettings;
use shared::{
    ActorKind, AuthoringLightKind, ColliderDesc, ColorDesc, EditorVisibility, LightDesc,
    MaterialDesc, ModelRef, PostProcessVolumeDesc, PrefabRef, Primitive, PrimitiveShape,
    RigidBodyDesc, AUDIO_CLIP_SUB_ASSET_ID,
};
use walkdir::WalkDir;

use crate::asset_db::{
    ensure_asset_record, find_asset_by_id, find_asset_by_path, find_asset_mut_by_path,
    AssetRegistry, MaterialImportPolicy, ModelPlacementMode,
};
use crate::assets::animation::load_animation_manifest;
use crate::assets::static_mesh::{load_static_mesh_manifest, renderer_from_manifest};
use crate::history::{spawn_with_history, EditorEntitySnapshot};

pub const BUILTINS_FOLDER: &str = "__builtins__";
pub const ASSETS_ROOT: &str = "assets";
pub const AUDIO_ASSET_EXTENSIONS: &[&str] = &["ogg", "oga", "spx", "wav", "mp3", "flac"];
pub const IMPORTABLE_ASSET_EXTENSIONS: &[&str] = &[
    "gltf", "glb", "fbx", "png", "jpg", "jpeg", "webp", "ktx2", "ogg", "oga", "spx", "wav", "mp3",
    "flac",
];

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum AssetSubAssetKind {
    Mesh,
    Material,
    Texture,
    Skeleton,
    AnimationClip,
}

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum AssetSelection {
    Builtin(String),
    File(String),
    SubAsset {
        parent_path: String,
        sub_asset_id: String,
        label: String,
        kind: AssetSubAssetKind,
        source_path: Option<String>,
    },
}

impl AssetSelection {
    pub fn from_asset(asset: &EditorAsset) -> Self {
        match &asset.path {
            Some(path) => AssetSelection::File(path.clone()),
            None => AssetSelection::Builtin(asset.label.clone()),
        }
    }

    pub fn parent_path(&self) -> Option<&str> {
        match self {
            AssetSelection::Builtin(_) => None,
            AssetSelection::File(path) => Some(path),
            AssetSelection::SubAsset { parent_path, .. } => Some(parent_path),
        }
    }

    pub fn display_label(&self) -> &str {
        match self {
            AssetSelection::Builtin(label) | AssetSelection::File(label) => label,
            AssetSelection::SubAsset { label, .. } => label,
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum EditorAssetKind {
    Primitive(PrimitiveShape),
    Light(AuthoringLightKind),
    Model,
    Texture,
    Material,
    AudioClip,
    Level,
    Prefab,
    PostProcessVolume,
    PostProcessEffect,
    RenderingProfile,
    ShaderSchema,
}

#[derive(Debug, Clone)]
pub struct EditorAsset {
    pub label: String,
    pub path: Option<String>,
    pub folder_path: String,
    pub kind: EditorAssetKind,
}

#[derive(Debug, Clone)]
pub struct AssetFolder {
    pub path: String,
    pub name: String,
    pub parent: Option<String>,
}

#[derive(Resource, Debug)]
pub struct EditorAssets {
    pub folders: Vec<AssetFolder>,
    pub assets: Vec<EditorAsset>,
    pub current_folder: String,
    pub selected: Option<AssetSelection>,
    pub dragging: Option<AssetSelection>,
    pub status: String,
}

impl Default for EditorAssets {
    fn default() -> Self {
        let mut assets = Self {
            folders: Vec::new(),
            assets: Vec::new(),
            current_folder: ASSETS_ROOT.to_string(),
            selected: None,
            dragging: None,
            status: "Assets not scanned yet".to_string(),
        };
        assets.refresh();
        assets
    }
}

impl EditorAssets {
    pub fn refresh(&mut self) {
        self.folders.clear();
        self.assets.clear();

        self.folders.push(AssetFolder {
            path: BUILTINS_FOLDER.to_string(),
            name: "Built-ins".to_string(),
            parent: None,
        });

        register_folder_tree(&mut self.folders, ASSETS_ROOT);
        self.assets.extend(builtin_assets());
        scan_assets_directory(&mut self.assets);

        if !self.folder_exists(&self.current_folder) {
            self.current_folder = ASSETS_ROOT.to_string();
        }

        self.selected = self
            .selected
            .take()
            .filter(|selection| self.asset_for_selection(selection).is_some());

        let folder_count = self.folders.len();
        self.status = format!(
            "Scanned {} assets in {} folders",
            self.assets.len(),
            folder_count
        );
    }

    fn folder_exists(&self, path: &str) -> bool {
        self.folders.iter().any(|folder| folder.path == path)
    }

    pub fn asset_for_selection(&self, selection: &AssetSelection) -> Option<&EditorAsset> {
        match selection {
            AssetSelection::Builtin(label) => self
                .assets
                .iter()
                .find(|asset| asset.path.is_none() && asset.label == *label),
            AssetSelection::File(path) => self
                .assets
                .iter()
                .find(|asset| asset.path.as_deref() == Some(path.as_str())),
            AssetSelection::SubAsset { parent_path, .. } => self
                .assets
                .iter()
                .find(|asset| asset.path.as_deref() == Some(parent_path.as_str())),
        }
    }

    pub fn selected_asset(&self) -> Option<&EditorAsset> {
        self.selected
            .as_ref()
            .and_then(|selection| self.asset_for_selection(selection))
    }

    pub fn dragging_asset(&self) -> Option<&EditorAsset> {
        self.dragging
            .as_ref()
            .and_then(|selection| self.asset_for_selection(selection))
    }

    pub fn dragging_selection(&self) -> Option<&AssetSelection> {
        self.dragging.as_ref()
    }

    pub fn select(&mut self, selection: AssetSelection) {
        if let Some(asset) = self.asset_for_selection(&selection).cloned() {
            let selected_label = match &selection {
                AssetSelection::SubAsset { label, kind, .. } => {
                    format!("{kind:?}: {label}")
                }
                _ => asset.label.clone(),
            };
            self.selected = Some(selection);
            self.status = format!("Selected {selected_label}");
        }
    }

    pub fn start_drag(&mut self, selection: AssetSelection) {
        if self.asset_for_selection(&selection).is_some() {
            self.dragging = Some(selection);
        }
    }

    pub fn clear_drag(&mut self) {
        self.dragging = None;
    }
}

pub struct EditorAssetsPlugin;

impl Plugin for EditorAssetsPlugin {
    fn build(&self, app: &mut App) {
        app.init_resource::<EditorAssets>()
            .init_resource::<super::prefab_overrides::PrefabHealthCache>()
            .add_observer(super::prefab_overrides::invalidate_prefab_health_on_ready)
            .add_systems(
                Update,
                (
                    refresh_assets_on_settings_change,
                    super::prefab_overrides::guard_prefab_reimports,
                ),
            );
    }
}

fn refresh_assets_on_settings_change(
    settings: Res<settings::ProjectSettings>,
    mut assets: ResMut<EditorAssets>,
) {
    if settings.is_changed() {
        assets.refresh();
    }
}

fn builtin_assets() -> Vec<EditorAsset> {
    vec![
        EditorAsset {
            label: "Cube".to_string(),
            path: None,
            folder_path: BUILTINS_FOLDER.to_string(),
            kind: EditorAssetKind::Primitive(PrimitiveShape::Box),
        },
        EditorAsset {
            label: "Sphere".to_string(),
            path: None,
            folder_path: BUILTINS_FOLDER.to_string(),
            kind: EditorAssetKind::Primitive(PrimitiveShape::Sphere),
        },
        EditorAsset {
            label: "Ramp".to_string(),
            path: None,
            folder_path: BUILTINS_FOLDER.to_string(),
            kind: EditorAssetKind::Primitive(PrimitiveShape::Ramp),
        },
        EditorAsset {
            label: "Point Light".to_string(),
            path: None,
            folder_path: BUILTINS_FOLDER.to_string(),
            kind: EditorAssetKind::Light(AuthoringLightKind::Point),
        },
        EditorAsset {
            label: "Spot Light".to_string(),
            path: None,
            folder_path: BUILTINS_FOLDER.to_string(),
            kind: EditorAssetKind::Light(AuthoringLightKind::Spot),
        },
        EditorAsset {
            label: "Directional Light".to_string(),
            path: None,
            folder_path: BUILTINS_FOLDER.to_string(),
            kind: EditorAssetKind::Light(AuthoringLightKind::Directional),
        },
        EditorAsset {
            label: "Post Process Volume".to_string(),
            path: None,
            folder_path: BUILTINS_FOLDER.to_string(),
            kind: EditorAssetKind::PostProcessVolume,
        },
    ]
}

fn register_folder_tree(folders: &mut Vec<AssetFolder>, root: &str) {
    let normalized = normalize_path(root);
    if folders.iter().any(|folder| folder.path == normalized) {
        return;
    }

    let parent = parent_folder_path(&normalized);
    if let Some(ref parent_path) = parent {
        if !folders.iter().any(|folder| folder.path == *parent_path) {
            register_folder_tree(folders, parent_path);
        }
    }

    folders.push(AssetFolder {
        name: folder_name(&normalized),
        path: normalized.clone(),
        parent,
    });

    if Path::new(&normalized).is_dir() {
        let Ok(read_dir) = fs::read_dir(&normalized) else {
            return;
        };
        for entry in read_dir.flatten() {
            if entry.path().is_dir() {
                register_folder_tree(folders, &entry.path().to_string_lossy());
            }
        }
    }
}

fn scan_assets_directory(assets: &mut Vec<EditorAsset>) {
    let root_path = Path::new(ASSETS_ROOT);
    if !root_path.exists() {
        return;
    }

    for entry in WalkDir::new(root_path)
        .follow_links(false)
        .into_iter()
        .filter_map(Result::ok)
    {
        if !entry.file_type().is_file() {
            continue;
        }
        let path = entry.path();
        if should_skip_asset_path(path) {
            continue;
        }
        if let Some(asset) = asset_from_file_path(path) {
            assets.push(asset);
        }
    }
}

fn should_skip_asset_path(path: &Path) -> bool {
    if path
        .file_name()
        .and_then(|name| name.to_str())
        .is_some_and(|name| name.starts_with('.'))
    {
        return true;
    }

    let normalized = normalize_asset_path(path);
    normalized == "assets/project.ron"
        || normalized.starts_with("assets/.index/")
        || normalized.starts_with("assets/.trash/")
        || normalized.starts_with(crate::assets::static_mesh::STATIC_MESH_ARTIFACT_DIR)
}

fn asset_from_file_path(path: &Path) -> Option<EditorAsset> {
    let extension = path.extension()?.to_str()?.to_ascii_lowercase();
    let normalized_path = normalize_asset_path(path);
    let (kinds, extensions) = if is_audio_asset_extension(&extension) {
        (
            vec![EditorAssetKind::AudioClip],
            AUDIO_ASSET_EXTENSIONS.to_vec(),
        )
    } else {
        match extension.as_str() {
            "gltf" | "glb" | "fbx" => (vec![EditorAssetKind::Model], vec!["gltf", "glb", "fbx"]),
            "png" | "jpg" | "jpeg" | "webp" | "ktx2" => (
                vec![EditorAssetKind::Texture],
                vec!["png", "jpg", "jpeg", "webp", "ktx2"],
            ),
            "ron" | "mat" | "material" => {
                if normalized_path.contains("/materials/") {
                    (
                        vec![EditorAssetKind::Material],
                        vec!["ron", "mat", "material"],
                    )
                } else if normalized_path.contains("/post_fx/") {
                    (vec![EditorAssetKind::PostProcessEffect], vec!["ron"])
                } else if normalized_path.contains("/rendering_profiles/") {
                    (vec![EditorAssetKind::RenderingProfile], vec!["ron"])
                } else if normalized_path.contains("/shaders/") {
                    (vec![EditorAssetKind::ShaderSchema], vec!["ron"])
                } else if normalized_path.ends_with(".scn.ron")
                    && (normalized_path.starts_with("assets/prefabs/")
                        || normalized_path.contains("/assets/prefabs/"))
                {
                    (vec![EditorAssetKind::Prefab], vec!["ron"])
                } else {
                    (vec![EditorAssetKind::Level], vec!["ron"])
                }
            }
            _ => return None,
        }
    };

    if !has_extension(path, &extensions) {
        return None;
    }

    let path_string = normalized_path;
    let folder_path = parent_folder_path(&path_string).unwrap_or_else(|| ASSETS_ROOT.to_string());
    let label = path
        .file_stem()
        .and_then(|stem| stem.to_str())
        .unwrap_or(&path_string)
        .to_string();
    let kind = kinds[0].clone();

    Some(EditorAsset {
        label,
        path: Some(path_string),
        folder_path,
        kind,
    })
}

fn normalize_path(path: &str) -> String {
    path.replace('\\', "/").trim_end_matches('/').to_string()
}

fn normalize_asset_path(path: &Path) -> String {
    let raw = path.to_string_lossy().replace('\\', "/");
    raw.strip_prefix("assets/")
        .or_else(|| raw.strip_prefix("./assets/"))
        .map(|rest| format!("assets/{rest}"))
        .unwrap_or(raw)
}

/// Path relative to Bevy's `assets/` folder for [`AssetServer::load`].
pub fn asset_server_path(catalog_path: &str) -> String {
    shared::asset_server_path(catalog_path)
}

fn parent_folder_path(path: &str) -> Option<String> {
    let normalized = normalize_path(path);
    normalized
        .rfind('/')
        .map(|index| normalized[..index].to_string())
}

fn folder_name(path: &str) -> String {
    Path::new(path)
        .file_name()
        .and_then(|name| name.to_str())
        .unwrap_or(path)
        .to_string()
}

fn has_extension(path: &Path, extensions: &[&str]) -> bool {
    path.extension()
        .and_then(|ext| ext.to_str())
        .is_some_and(|ext| {
            extensions
                .iter()
                .any(|candidate| ext.eq_ignore_ascii_case(candidate))
        })
}

fn is_audio_asset_extension(extension: &str) -> bool {
    AUDIO_ASSET_EXTENSIONS
        .iter()
        .any(|candidate| extension.eq_ignore_ascii_case(candidate))
}

pub fn audio_clip_ref_for_asset(
    world: &mut World,
    asset: &EditorAsset,
) -> Result<shared::EditorAssetRef, String> {
    if !matches!(asset.kind, EditorAssetKind::AudioClip) {
        return Err(format!("{} is not an audio clip", asset.label));
    }
    let path = asset
        .path
        .as_deref()
        .ok_or_else(|| format!("audio clip {} has no source path", asset.label))?;
    let record = world
        .get_resource_mut::<AssetRegistry>()
        .ok_or_else(|| "asset registry is unavailable".to_string())
        .and_then(|mut registry| {
            ensure_asset_record(
                &mut registry,
                path.to_string(),
                asset.label.clone(),
                "AudioClip",
            )
        })?;
    Ok(shared::EditorAssetRef::new(
        record.id.as_string(),
        AUDIO_CLIP_SUB_ASSET_ID,
        asset.label.clone(),
    )
    .with_source_path(path))
}

pub fn audio_source_desc_for_asset(
    world: &mut World,
    asset: &EditorAsset,
) -> Result<shared::AudioSourceDesc, String> {
    Ok(shared::AudioSourceDesc {
        clip: Some(audio_clip_ref_for_asset(world, asset)?),
        ..Default::default()
    })
}

fn empty_snapshot_for_asset(asset: &EditorAsset, translation: Vec3) -> EditorEntitySnapshot {
    EditorEntitySnapshot {
        actor_id: None,
        actor_kind: ActorKind::Empty,
        actor_name: None,
        name: Some(asset.label.clone()),
        transform: Transform::from_translation(translation),
        primitive: None,
        brush: None,
        static_mesh_renderer: None,
        skinned_mesh_renderer: None,
        material: None,
        material_override: None,
        rigid_body: None,
        collider: None,
        physics: None,
        light: None,
        animation_controller: None,
        audio_source: None,
        audio_listener: None,
        player_spawn: false,
        model: None,
        prefab: None,
        prefab_instance: None,
        weapon_spawn: None,
        trigger_volume: None,
        post_process_volume: None,
        team_spawn: None,
        objective: None,

        navigation_bounds: None,

        navigation_obstacle: None,

        navigation_area: None,

        navigation_link: None,
        hierarchy_sibling_index: 0,
        editor_visibility: EditorVisibility::default(),
        inspector_order: None,
        component_states: None,
        children: Vec::new(),
    }
}

pub fn snapshot_for_asset(asset: &EditorAsset, translation: Vec3) -> Option<EditorEntitySnapshot> {
    let mut snapshot = empty_snapshot_for_asset(asset, translation);

    match &asset.kind {
        EditorAssetKind::Primitive(shape) => {
            snapshot.actor_kind = ActorKind::StaticMesh;
            let size = match shape {
                PrimitiveShape::Box => Vec3::ONE,
                PrimitiveShape::Sphere => Vec3::splat(1.5),
                PrimitiveShape::Ramp => Vec3::new(4.0, 0.4, 5.0),
            };
            snapshot.primitive = Some(match shape {
                PrimitiveShape::Box => Primitive::cuboid(size),
                PrimitiveShape::Sphere => Primitive::sphere(size.x * 0.5),
                PrimitiveShape::Ramp => Primitive::ramp(size),
            });
            snapshot.material = Some(default_material_for(shape));
            snapshot.rigid_body = Some(RigidBodyDesc::default());
            snapshot.collider = Some(match shape {
                PrimitiveShape::Sphere => ColliderDesc::static_sphere(size.x * 0.5),
                _ => ColliderDesc::static_cuboid(size),
            });
            if matches!(shape, PrimitiveShape::Ramp) {
                snapshot.transform.rotation = Quat::from_rotation_x(-std::f32::consts::PI / 9.0);
                snapshot.transform.translation.y += 1.0;
            } else {
                snapshot.transform.translation.y += size.y * 0.5;
            }
        }
        EditorAssetKind::Light(kind) => {
            snapshot.actor_kind = ActorKind::Light;
            snapshot.light = Some(LightDesc::for_kind(*kind));
            snapshot.transform.translation.y += 3.0;
        }
        EditorAssetKind::PostProcessVolume => {
            snapshot.actor_kind = ActorKind::PostProcessVolume;
            snapshot.post_process_volume = Some(PostProcessVolumeDesc {
                half_extents: Vec3::new(4.0, 2.0, 4.0),
                ..Default::default()
            });
            snapshot.transform.translation.y += 2.0;
        }
        EditorAssetKind::Model => {
            snapshot.actor_kind = ActorKind::ImportedModel;
            let path = asset.path.as_ref()?;
            snapshot.model = Some(ModelRef::new(path.clone()));
            snapshot.transform.translation.y += 0.1;
        }
        EditorAssetKind::Prefab => {
            snapshot.actor_kind = ActorKind::PrefabAnchor;
            let path = asset.path.as_ref()?;
            snapshot.prefab = Some(PrefabRef::new(path.clone()));
        }
        EditorAssetKind::Level => return None,
        EditorAssetKind::Texture
        | EditorAssetKind::Material
        | EditorAssetKind::AudioClip
        | EditorAssetKind::PostProcessEffect
        | EditorAssetKind::RenderingProfile
        | EditorAssetKind::ShaderSchema => return None,
    }

    Some(snapshot)
}

pub fn spawn_asset_at(world: &mut World, asset: &EditorAsset, translation: Vec3) -> Option<Entity> {
    let mut snapshot = if matches!(asset.kind, EditorAssetKind::AudioClip) {
        let descriptor = match audio_source_desc_for_asset(world, asset) {
            Ok(descriptor) => descriptor,
            Err(error) => {
                warn!("Audio clip placement failed for {}: {error}", asset.label);
                if let Some(mut scene_io) = world.get_resource_mut::<crate::scene_io::SceneIo>() {
                    scene_io.status = format!("Audio placement failed: {error}");
                }
                return None;
            }
        };
        let mut snapshot = empty_snapshot_for_asset(asset, translation);
        snapshot.actor_kind = ActorKind::AudioSource;
        snapshot.audio_source = Some(descriptor);
        snapshot
    } else {
        snapshot_for_asset(asset, translation)?
    };
    if let Some(light) = snapshot.light.as_mut() {
        if matches!(light.kind, AuthoringLightKind::Directional) {
            light.intensity = world
                .resource::<ProjectSettings>()
                .rendering
                .sun_illuminance;
        }
    }
    if let Some(path) = asset.path.as_ref() {
        let mut record = world
            .get_resource::<AssetRegistry>()
            .and_then(|registry| find_asset_by_path(registry, path));
        if matches!(asset.kind, EditorAssetKind::Prefab) && record.is_none() {
            record = world
                .get_resource_mut::<AssetRegistry>()
                .and_then(|mut registry| {
                    ensure_asset_record(&mut registry, path.clone(), asset.label.clone(), "Prefab")
                        .map_err(|error| {
                            error!("Could not register prefab asset {path}: {error}");
                        })
                        .ok()
                });
        }
        if matches!(asset.kind, EditorAssetKind::Prefab) && record.is_none() {
            if let Some(mut scene_io) = world.get_resource_mut::<crate::scene_io::SceneIo>() {
                scene_io.status = format!("Prefab placement failed: could not register {path}");
            }
            return None;
        }
        if let Some(record) = record {
            match asset.kind {
                EditorAssetKind::Model => {
                    if let Some(model) = snapshot.model.as_mut() {
                        model.asset_id = record.id.as_string();
                    }
                    snapshot.transform.scale *= record.import_settings.scale;
                    if matches!(
                        record.import_settings.placement_mode,
                        ModelPlacementMode::StaticAsset
                    ) {
                        if let Some((renderer, requires_skinned_renderer)) =
                            static_mesh_renderer_for_asset(world, path)
                        {
                            if requires_skinned_renderer {
                                apply_skinned_mesh_placement_mode_with_materials(
                                    &mut snapshot,
                                    renderer.materials.clone(),
                                );
                                match default_animation_controller_for_record(&record) {
                                    Ok(controller) => snapshot.animation_controller = controller,
                                    Err(error) => warn!(
                                        "Default animation was not applied while placing {}: {error}",
                                        record.path
                                    ),
                                }
                            } else {
                                let collider = record
                                    .import_settings
                                    .generate_collider
                                    .then(|| static_mesh_collider_for_renderer(&renderer));
                                apply_static_mesh_placement_mode(
                                    &mut snapshot,
                                    renderer,
                                    collider,
                                    record.import_settings.hierarchy_mode,
                                );
                            }
                        } else {
                            warn!(
                                "Static mesh placement fell back to SceneRoot for {} because no renderer manifest was available",
                                path
                            );
                        }
                    }
                }
                EditorAssetKind::Prefab => {
                    snapshot.prefab_instance =
                        Some(super::prefab_overrides::new_tracked_prefab_instance(
                            world,
                            record.id.as_string(),
                            path.clone(),
                        ));
                }
                _ => {}
            }
        }
    }
    Some(spawn_with_history(world, snapshot))
}

pub fn validate_prefab_asset(world: &World, asset: &EditorAsset) -> Result<(), String> {
    if !matches!(asset.kind, EditorAssetKind::Prefab) {
        return Ok(());
    }
    let source_path = asset
        .path
        .as_deref()
        .ok_or_else(|| "Prefab asset has no source path".to_string())?;
    let project_root = world
        .get_resource::<crate::project_io::ProjectWorkspace>()
        .map(|workspace| PathBuf::from(&workspace.root))
        .ok_or_else(|| "Project workspace is unavailable".to_string())?;
    scene::validate_prefab_graph(&project_root.join(source_path), &project_root)
        .map_err(|error| format!("Prefab dependency validation failed: {error}"))
}

pub fn spawn_subasset_at(
    world: &mut World,
    selection: &AssetSelection,
    translation: Vec3,
) -> Option<Entity> {
    if matches!(
        selection,
        AssetSelection::SubAsset {
            kind: AssetSubAssetKind::AnimationClip,
            ..
        }
    ) {
        return spawn_animation_clip_at(world, selection, translation);
    }

    let AssetSelection::SubAsset {
        parent_path,
        sub_asset_id,
        label,
        kind: AssetSubAssetKind::Mesh,
        ..
    } = selection
    else {
        return None;
    };

    let record = world
        .get_resource::<AssetRegistry>()
        .and_then(|registry| find_asset_by_path(registry, parent_path))?;
    let manifest_path = record
        .import_settings
        .static_mesh_manifest_path
        .as_deref()?;
    let manifest = load_static_mesh_manifest(manifest_path).ok()?;
    let part = manifest
        .parts
        .iter()
        .find(|part| part_effective_id_for_selection(part) == *sub_asset_id)?;
    let part_requires_skinned_renderer = manifest.metadata.animation_count > 0
        || part.skinned
        || (manifest.schema_version < 2
            && crate::assets::gltf_skinned_primitive_labels(parent_path)
                .contains(&part.mesh_label));

    let mesh_ref = shared::EditorAssetRef::new(
        manifest.asset_id.clone(),
        sub_asset_id.clone(),
        label.clone(),
    );
    let material = matches!(
        record.import_settings.material_policy,
        MaterialImportPolicy::SourceMaterials
    )
    .then(|| {
        part_effective_material_id_for_selection(part).map(|id| {
            shared::EditorAssetRef::new(
                manifest.asset_id.clone(),
                id,
                part.material_slot_name.clone(),
            )
        })
    })
    .flatten();
    let slot = shared::StaticMeshRendererEntry {
        id: shared::ComponentInstanceId::new("slot:0"),
        name: label.clone(),
        mesh: mesh_ref.clone(),
        material_slot_id: shared::ComponentInstanceId::new(format!(
            "slot:{}",
            part_effective_id_for_selection(part)
        )),
        material,
        local_transform: part.local_transform,
        visible: true,
        cast_shadows: true,
        receive_shadows: true,
    };

    let mut snapshot = EditorEntitySnapshot {
        actor_id: None,
        actor_kind: ActorKind::StaticMesh,
        actor_name: None,
        name: Some(label.clone()),
        transform: Transform::from_translation(translation + Vec3::Y * 0.1),
        primitive: None,
        brush: None,
        static_mesh_renderer: Some(shared::StaticMeshRenderer::single(slot)),
        skinned_mesh_renderer: None,
        material: None,
        material_override: None,
        rigid_body: None,
        collider: None,
        physics: None,
        light: None,
        animation_controller: None,
        audio_source: None,
        audio_listener: None,
        player_spawn: false,
        model: None,
        prefab: None,
        prefab_instance: None,
        weapon_spawn: None,
        trigger_volume: None,
        post_process_volume: None,
        team_spawn: None,
        objective: None,

        navigation_bounds: None,

        navigation_obstacle: None,

        navigation_area: None,

        navigation_link: None,
        hierarchy_sibling_index: 0,
        editor_visibility: EditorVisibility::default(),
        inspector_order: None,
        component_states: None,
        children: Vec::new(),
    };
    snapshot.transform.scale *= record.import_settings.scale;
    if part_requires_skinned_renderer {
        snapshot.actor_kind = ActorKind::SkinnedMesh;
        snapshot.static_mesh_renderer = None;
        snapshot.skinned_mesh_renderer = Some(shared::SkinnedMeshRenderer {
            materials: crate::assets::static_mesh::renderer_materials_from_manifest(
                &manifest,
                &record.import_settings,
            ),
            ..shared::SkinnedMeshRenderer::new(parent_path.clone())
                .with_asset_id(record.id.as_string())
        });
        match default_animation_controller_for_record(&record) {
            Ok(controller) => snapshot.animation_controller = controller,
            Err(error) => warn!(
                "Default animation was not applied while placing {}: {error}",
                record.path
            ),
        }
        return Some(spawn_with_history(world, snapshot));
    }
    if record.import_settings.generate_collider {
        snapshot.rigid_body = Some(RigidBodyDesc::default());
        snapshot.collider = Some(ColliderDesc::static_mesh(vec![mesh_ref]));
    }

    Some(spawn_with_history(world, snapshot))
}

#[derive(Debug, Clone)]
pub struct AnimationClipAuthoringData {
    pub model_asset_id: String,
    pub model_path: String,
    pub model_label: String,
    pub model_scale: f32,
    pub skeleton: shared::EditorAssetRef,
    pub skeleton_signature: shared::AnimationSkeletonSignature,
    pub state: shared::AnimationStateDesc,
    pub renderer_materials: shared::RendererMaterialSet,
}

pub fn animation_clip_authoring_data(
    world: &World,
    selection: &AssetSelection,
) -> Result<AnimationClipAuthoringData, String> {
    let AssetSelection::SubAsset {
        parent_path,
        sub_asset_id,
        kind: AssetSubAssetKind::AnimationClip,
        ..
    } = selection
    else {
        return Err("the selected sub-asset is not an animation clip".into());
    };
    let record = world
        .get_resource::<AssetRegistry>()
        .and_then(|registry| find_asset_by_path(registry, parent_path))
        .ok_or_else(|| format!("model `{parent_path}` is missing from the asset registry"))?;
    let manifest_path = record
        .import_settings
        .animation_manifest_path
        .as_deref()
        .ok_or_else(|| format!("model `{parent_path}` has no generated animation manifest"))?;
    let manifest = load_animation_manifest(manifest_path)?;
    if !manifest.runtime_supported {
        return Err(format!(
            "model `{parent_path}` is not supported by the animation runtime; animation v1 requires glTF/GLB"
        ));
    }
    let clip = manifest
        .clips
        .iter()
        .find(|clip| clip.id == *sub_asset_id)
        .ok_or_else(|| {
            format!(
                "animation clip `{sub_asset_id}` is missing from `{manifest_path}`; reimport the model"
            )
        })?;
    let target_signature = clip.target_skeleton_signature.as_ref().ok_or_else(|| {
        format!(
            "animation clip `{}` has no uniquely resolved target skeleton",
            clip.label
        )
    })?;
    let skeleton = manifest
        .skeletons
        .iter()
        .find(|skeleton| &skeleton.signature == target_signature)
        .ok_or_else(|| {
            format!(
                "animation clip `{}` targets skeleton `{}` but no exact match exists in `{parent_path}`",
                clip.label, target_signature.0
            )
        })?;
    let asset_id = record.id.as_string();
    let skeleton_ref = shared::EditorAssetRef::new(
        asset_id.clone(),
        skeleton.id.clone(),
        skeleton.label.clone(),
    )
    .with_source_path(parent_path);
    let clip_ref = shared::EditorAssetRef::new(asset_id, clip.id.clone(), clip.label.clone())
        .with_source_path(parent_path);
    let state_id = animation_state_id(&clip.label, clip.source_index);
    let renderer_materials = record
        .import_settings
        .static_mesh_manifest_path
        .as_deref()
        .and_then(|path| load_static_mesh_manifest(path).ok())
        .map(|render_manifest| {
            crate::assets::static_mesh::renderer_materials_from_manifest(
                &render_manifest,
                &record.import_settings,
            )
        })
        .unwrap_or_default();
    Ok(AnimationClipAuthoringData {
        model_asset_id: record.id.as_string(),
        model_path: parent_path.clone(),
        model_label: record.label,
        model_scale: record.import_settings.scale,
        skeleton: skeleton_ref,
        skeleton_signature: skeleton.signature.clone(),
        state: shared::AnimationStateDesc {
            id: state_id,
            label: clip.label.clone(),
            clip: clip_ref,
            looping: true,
            speed: 1.0,
            range: shared::AnimationPlaybackRange::default(),
        },
        renderer_materials,
    })
}

pub fn animation_skeleton_signature_for_ref(
    world: &World,
    reference: &shared::EditorAssetRef,
) -> Result<shared::AnimationSkeletonSignature, String> {
    let record = world
        .get_resource::<AssetRegistry>()
        .and_then(|registry| find_asset_by_id(registry, &reference.asset_id))
        .ok_or_else(|| {
            format!(
                "skeleton asset `{}` is missing from the asset registry",
                reference.asset_id
            )
        })?;
    let manifest_path = record
        .import_settings
        .animation_manifest_path
        .as_deref()
        .ok_or_else(|| format!("model `{}` has no animation manifest", record.path))?;
    let manifest = load_animation_manifest(manifest_path)?;
    manifest
        .skeletons
        .iter()
        .find(|skeleton| skeleton.id == reference.sub_asset_id)
        .map(|skeleton| skeleton.signature.clone())
        .ok_or_else(|| {
            format!(
                "skeleton `{}` is missing from `{manifest_path}`; reimport the model",
                reference.sub_asset_id
            )
        })
}

pub fn compatible_animation_skeleton_for_model(
    world: &World,
    model: &shared::SkinnedMeshRenderer,
    signature: &shared::AnimationSkeletonSignature,
) -> Result<shared::EditorAssetRef, String> {
    let record = world
        .get_resource::<AssetRegistry>()
        .and_then(|registry| {
            if model.asset_id.trim().is_empty() {
                find_asset_by_path(registry, &model.path)
            } else {
                find_asset_by_id(registry, &model.asset_id)
            }
        })
        .ok_or_else(|| {
            format!(
                "model `{}` ({}) is missing from the asset registry",
                model.path, model.asset_id
            )
        })?;
    let model_path = record.path.as_str();
    let manifest_path = record
        .import_settings
        .animation_manifest_path
        .as_deref()
        .ok_or_else(|| format!("model `{model_path}` has no animation manifest"))?;
    let manifest = load_animation_manifest(manifest_path)?;
    let skeleton = manifest
        .skeletons
        .iter()
        .find(|skeleton| &skeleton.signature == signature)
        .ok_or_else(|| {
            format!(
                "model `{model_path}` has no skeleton matching animation rig `{}`",
                signature.0
            )
        })?;
    Ok(shared::EditorAssetRef::new(
        record.id.as_string(),
        skeleton.id.clone(),
        skeleton.label.clone(),
    )
    .with_source_path(model_path))
}

fn animation_state_id(label: &str, source_index: usize) -> String {
    let mut id = String::new();
    for character in label.chars() {
        if character.is_ascii_alphanumeric() {
            id.push(character.to_ascii_lowercase());
        } else if !id.ends_with('_') {
            id.push('_');
        }
    }
    let id = id.trim_matches('_');
    if id.is_empty() {
        format!("clip_{source_index}")
    } else {
        id.to_string()
    }
}

fn default_animation_controller_for_record(
    record: &crate::asset_db::AssetRecord,
) -> Result<Option<shared::AnimationControllerDesc>, String> {
    let Some(manifest_path) = record.import_settings.animation_manifest_path.as_deref() else {
        return Ok(None);
    };
    let manifest = load_animation_manifest(manifest_path)?;
    if manifest.default_animation_clip_id != record.import_settings.default_animation_clip_id {
        return Err(
            "model import settings and generated animation manifest disagree; reimport the model"
                .into(),
        );
    }
    let Some(default_clip_id) = manifest
        .default_animation_clip_id
        .as_deref()
        .filter(|id| !id.trim().is_empty())
    else {
        return Ok(None);
    };
    if !manifest.runtime_supported {
        return Err(format!(
            "configured clip `{default_clip_id}` belongs to a model unsupported by the animation runtime"
        ));
    }
    let clip = manifest
        .clips
        .iter()
        .find(|clip| clip.id == default_clip_id)
        .ok_or_else(|| {
            format!(
                "configured clip `{default_clip_id}` is missing from {manifest_path}; choose an existing clip or Imported rest pose"
            )
        })?;
    let signature = clip.target_skeleton_signature.as_ref().ok_or_else(|| {
        format!(
            "configured clip `{}` has no uniquely resolved target skeleton",
            clip.label
        )
    })?;
    let skeleton = manifest
        .skeletons
        .iter()
        .find(|skeleton| &skeleton.signature == signature)
        .ok_or_else(|| {
            format!(
                "configured clip `{}` targets skeleton `{}` but no exact match exists",
                clip.label, signature.0
            )
        })?;
    let state_id = animation_state_id(&clip.label, clip.source_index);
    let source_path = manifest.source.path.clone();
    let asset_id = manifest.asset_id.clone();
    Ok(Some(shared::AnimationControllerDesc {
        skeleton: Some(
            shared::EditorAssetRef::new(
                asset_id.clone(),
                skeleton.id.clone(),
                skeleton.label.clone(),
            )
            .with_source_path(&source_path),
        ),
        states: vec![shared::AnimationStateDesc {
            id: state_id.clone(),
            label: clip.label.clone(),
            clip: shared::EditorAssetRef::new(asset_id, clip.id.clone(), clip.label.clone())
                .with_source_path(source_path),
            looping: true,
            speed: 1.0,
            range: shared::AnimationPlaybackRange::default(),
        }],
        default_state: state_id,
        ..default()
    }))
}

fn spawn_animation_clip_at(
    world: &mut World,
    selection: &AssetSelection,
    translation: Vec3,
) -> Option<Entity> {
    let authored = match animation_clip_authoring_data(world, selection) {
        Ok(authored) => authored,
        Err(error) => {
            warn!("Animation clip placement failed: {error}");
            if let Some(mut scene_io) = world.get_resource_mut::<crate::scene_io::SceneIo>() {
                scene_io.status = format!("Animation placement failed: {error}");
            }
            return None;
        }
    };
    let default_state = authored.state.id.clone();
    let snapshot = EditorEntitySnapshot {
        actor_id: None,
        actor_kind: ActorKind::SkinnedMesh,
        actor_name: None,
        name: Some(authored.model_label),
        transform: Transform {
            translation: translation + Vec3::Y * 0.1,
            scale: Vec3::splat(authored.model_scale),
            ..default()
        },
        primitive: None,
        brush: None,
        static_mesh_renderer: None,
        skinned_mesh_renderer: Some(shared::SkinnedMeshRenderer {
            materials: authored.renderer_materials,
            ..shared::SkinnedMeshRenderer::new(authored.model_path)
                .with_asset_id(authored.model_asset_id)
        }),
        material: None,
        material_override: None,
        rigid_body: None,
        collider: None,
        physics: None,
        light: None,
        animation_controller: Some(shared::AnimationControllerDesc {
            skeleton: Some(authored.skeleton),
            states: vec![authored.state],
            default_state,
            ..default()
        }),
        audio_source: None,
        audio_listener: None,
        player_spawn: false,
        model: None,
        prefab: None,
        prefab_instance: None,
        weapon_spawn: None,
        trigger_volume: None,
        post_process_volume: None,
        team_spawn: None,
        objective: None,

        navigation_bounds: None,

        navigation_obstacle: None,

        navigation_area: None,

        navigation_link: None,
        hierarchy_sibling_index: 0,
        editor_visibility: EditorVisibility::default(),
        inspector_order: None,
        component_states: None,
        children: Vec::new(),
    };
    Some(spawn_with_history(world, snapshot))
}

fn apply_static_mesh_placement_mode(
    snapshot: &mut EditorEntitySnapshot,
    renderer: shared::StaticMeshRenderer,
    collider: Option<ColliderDesc>,
    hierarchy_mode: crate::asset_db::ModelHierarchyMode,
) {
    snapshot.model = None;
    match hierarchy_mode {
        crate::asset_db::ModelHierarchyMode::SingleActor => {
            snapshot.actor_kind = ActorKind::StaticMesh;
            snapshot.static_mesh_renderer = Some(renderer);
            if let Some(collider) = collider {
                snapshot.rigid_body = Some(RigidBodyDesc::default());
                snapshot.collider = Some(collider);
            }
        }
        crate::asset_db::ModelHierarchyMode::SourceHierarchy => {
            snapshot.actor_kind = ActorKind::Empty;
            snapshot.static_mesh_renderer = None;
            snapshot.children = renderer
                .slots
                .into_iter()
                .enumerate()
                .map(|(index, mut entry)| {
                    let transform = entry.local_transform;
                    let collider = collider
                        .as_ref()
                        .map(|_| ColliderDesc::static_mesh(vec![entry.mesh.clone()]));
                    entry.local_transform = Transform::default();
                    EditorEntitySnapshot {
                        actor_id: None,
                        actor_kind: ActorKind::StaticMesh,
                        actor_name: None,
                        name: Some(if entry.name.trim().is_empty() {
                            format!("Mesh Part {index}")
                        } else {
                            entry.name.clone()
                        }),
                        transform,
                        primitive: None,
                        brush: None,
                        static_mesh_renderer: Some(shared::StaticMeshRenderer::single(entry)),
                        skinned_mesh_renderer: None,
                        material: None,
                        material_override: None,
                        rigid_body: collider.as_ref().map(|_| RigidBodyDesc::default()),
                        collider,
                        physics: None,
                        light: None,
                        animation_controller: None,
                        audio_source: None,
                        audio_listener: None,
                        player_spawn: false,
                        model: None,
                        prefab: None,
                        prefab_instance: None,
                        weapon_spawn: None,
                        trigger_volume: None,
                        post_process_volume: None,
                        team_spawn: None,
                        objective: None,

                        navigation_bounds: None,

                        navigation_obstacle: None,

                        navigation_area: None,

                        navigation_link: None,
                        hierarchy_sibling_index: index as i32,
                        editor_visibility: EditorVisibility::default(),
                        inspector_order: None,
                        component_states: None,
                        children: Vec::new(),
                    }
                })
                .collect();
        }
    }
}

#[cfg(test)]
fn apply_skinned_mesh_placement_mode(snapshot: &mut EditorEntitySnapshot) {
    apply_skinned_mesh_placement_mode_with_materials(
        snapshot,
        shared::RendererMaterialSet::default(),
    );
}

fn apply_skinned_mesh_placement_mode_with_materials(
    snapshot: &mut EditorEntitySnapshot,
    materials: shared::RendererMaterialSet,
) {
    let Some(model) = snapshot.model.take() else {
        return;
    };
    snapshot.actor_kind = ActorKind::SkinnedMesh;
    snapshot.static_mesh_renderer = None;
    snapshot.skinned_mesh_renderer = Some(shared::SkinnedMeshRenderer {
        asset_id: model.asset_id,
        path: model.path,
        scene_index: model.scene_index,
        materials,
    });
    // Triangle-mesh colliders are a static-geometry concern. Animated collision must be authored
    // explicitly (capsules, hitboxes, or another gameplay-specific representation).
    snapshot.rigid_body = None;
    snapshot.collider = None;
}

fn static_mesh_collider_for_renderer(renderer: &shared::StaticMeshRenderer) -> ColliderDesc {
    ColliderDesc::static_mesh(
        renderer
            .slots
            .iter()
            .map(|slot| slot.mesh.clone())
            .collect(),
    )
}

fn part_effective_id_for_selection(part: &crate::assets::static_mesh::StaticMeshPart) -> String {
    if part.id.trim().is_empty() {
        crate::assets::static_mesh::part_id_from_label(&part.mesh_label)
    } else {
        part.id.clone()
    }
}

fn part_effective_material_id_for_selection(
    part: &crate::assets::static_mesh::StaticMeshPart,
) -> Option<String> {
    part.material_id.clone().or_else(|| {
        part.material_label
            .as_ref()
            .map(|label| crate::assets::static_mesh::material_id_from_label(label))
    })
}

fn static_mesh_renderer_for_asset(
    world: &mut World,
    path: &str,
) -> Option<(shared::StaticMeshRenderer, bool)> {
    let mut manifest_path = None;
    if let Some(mut registry) = world.get_resource_mut::<AssetRegistry>() {
        let mut renderer = None;
        if let Some(record) = find_asset_mut_by_path(&mut registry, path) {
            match super::refresh_model_artifacts(record) {
                Ok(manifest) => {
                    let requires_skinned_renderer = manifest.metadata.animation_count > 0
                        || manifest.metadata.skin_count > 0
                        || manifest.parts.iter().any(|part| part.skinned);
                    let mut authored_renderer =
                        renderer_from_manifest(&manifest, &record.import_settings);
                    if requires_skinned_renderer {
                        authored_renderer.materials =
                            crate::assets::static_mesh::renderer_materials_from_manifest(
                                &manifest,
                                &record.import_settings,
                            );
                    }
                    renderer = Some((authored_renderer, requires_skinned_renderer));
                    registry.index_dirty = true;
                }
                Err(error) => {
                    warn!("Model artifact refresh failed for {path}: {error}");
                    manifest_path = record.import_settings.static_mesh_manifest_path.clone();
                }
            }
        }
        if renderer.is_some() {
            if let Err(error) = crate::asset_db::save_registry(&registry) {
                warn!("Asset registry save failed after static mesh refresh: {error}");
            } else {
                registry.index_dirty = false;
            }
            return renderer;
        }
    }

    let manifest_path = manifest_path?;
    let registry = world.get_resource::<AssetRegistry>()?;
    let record = find_asset_by_path(registry, path)?;
    let manifest = load_static_mesh_manifest(&manifest_path).ok()?;
    let requires_skinned_renderer = manifest.metadata.animation_count > 0
        || manifest.metadata.skin_count > 0
        || manifest.parts.iter().any(|part| part.skinned);
    let mut renderer = renderer_from_manifest(&manifest, &record.import_settings);
    if requires_skinned_renderer {
        renderer.materials = crate::assets::static_mesh::renderer_materials_from_manifest(
            &manifest,
            &record.import_settings,
        );
    }
    Some((renderer, requires_skinned_renderer))
}

pub fn import_external_assets(paths: &[PathBuf]) -> Result<usize, String> {
    let mut copied = 0;
    for source in paths {
        let Some(file_name) = source.file_name() else {
            continue;
        };
        let Some(extension) = source.extension().and_then(|ext| ext.to_str()) else {
            continue;
        };
        let dest_dir = import_dir_for_extension(extension);
        if extension.eq_ignore_ascii_case("fbx") {
            super::import::copy_fbx_with_sidecar(source, Path::new(dest_dir))?;
            copied += 1;
            continue;
        }
        fs::create_dir_all(dest_dir)
            .map_err(|err| format!("could not create {dest_dir}: {err}"))?;
        let dest = Path::new(dest_dir).join(file_name);
        fs::copy(source, &dest).map_err(|err| {
            format!(
                "could not copy {} to {}: {err}",
                source.display(),
                dest.display()
            )
        })?;
        copied += 1;
    }
    Ok(copied)
}

fn import_dir_for_extension(extension: &str) -> &'static str {
    if is_audio_asset_extension(extension) {
        return "assets/audio";
    }
    match extension.to_ascii_lowercase().as_str() {
        "gltf" | "glb" | "fbx" => "assets/models",
        "png" | "jpg" | "jpeg" | "webp" | "ktx2" => "assets/textures",
        "ron" => "assets/levels",
        "mat" | "material" => "assets/materials",
        _ => "assets",
    }
}

fn default_material_for(shape: &PrimitiveShape) -> MaterialDesc {
    match shape {
        PrimitiveShape::Box => MaterialDesc::default(),
        PrimitiveShape::Sphere => MaterialDesc::new(ColorDesc::srgb(0.9, 0.9, 0.92), 1.0, 0.25),
        PrimitiveShape::Ramp => MaterialDesc::new(ColorDesc::srgb(0.30, 0.45, 0.70), 0.05, 0.55),
    }
}

pub fn asset_cache_key(asset: &EditorAsset) -> String {
    match &asset.path {
        Some(path) => path.clone(),
        None => format!("builtin:{}", asset.label),
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::asset_db::{AssetId, AssetRecord};
    use uuid::Uuid;

    #[test]
    fn scene_assets_are_classified_by_authoring_folder() {
        let level = asset_from_file_path(Path::new("assets/levels/arena.scn.ron")).unwrap();
        let prefab = asset_from_file_path(Path::new("assets/prefabs/crate.scn.ron")).unwrap();

        assert_eq!(level.kind, EditorAssetKind::Level);
        assert_eq!(prefab.kind, EditorAssetKind::Prefab);
    }

    #[test]
    fn bevy_audio_extensions_are_classified_and_imported_to_audio() {
        for extension in AUDIO_ASSET_EXTENSIONS {
            let path = format!("assets/audio/impact.{extension}");
            let asset = asset_from_file_path(Path::new(&path)).unwrap();
            assert_eq!(asset.kind, EditorAssetKind::AudioClip);
            assert_eq!(asset.folder_path, "assets/audio");
            assert_eq!(import_dir_for_extension(extension), "assets/audio");
            assert!(IMPORTABLE_ASSET_EXTENSIONS.contains(extension));
        }

        assert!(asset_from_file_path(Path::new("assets/audio/impact.m4a")).is_none());
        assert_eq!(import_dir_for_extension("WAV"), "assets/audio");
    }

    #[test]
    fn audio_clip_reference_keeps_stable_registry_identity_and_runtime_path() {
        let asset = EditorAsset {
            label: "Impact".into(),
            path: Some("assets/audio/impact.ogg".into()),
            folder_path: "assets/audio".into(),
            kind: EditorAssetKind::AudioClip,
        };
        let stable_id = AssetId(Uuid::nil());
        let mut world = World::new();
        world.insert_resource(AssetRegistry {
            records: vec![AssetRecord {
                id: stable_id.clone(),
                path: asset.path.clone().unwrap(),
                label: asset.label.clone(),
                kind_tag: "AudioClip".into(),
                import_settings: Default::default(),
                dependencies: Vec::new(),
            }],
            index_dirty: false,
        });
        world.init_resource::<crate::history::EditorHistory>();
        world.init_resource::<crate::selection::SelectedEntity>();

        let descriptor = audio_source_desc_for_asset(&mut world, &asset).unwrap();
        let reference = descriptor.clip.unwrap();
        assert_eq!(reference.asset_id, stable_id.as_string());
        assert_eq!(reference.sub_asset_id, AUDIO_CLIP_SUB_ASSET_ID);
        assert_eq!(reference.label, "Impact");
        assert_eq!(reference.source_path.as_deref(), asset.path.as_deref());
        assert_eq!(
            audio_clip_ref_for_asset(&mut world, &asset)
                .unwrap()
                .asset_id,
            reference.asset_id
        );

        let entity = spawn_asset_at(&mut world, &asset, Vec3::new(1.0, 2.0, 3.0)).unwrap();
        assert_eq!(
            world.get::<ActorKind>(entity),
            Some(&ActorKind::AudioSource)
        );
        let placed = world.get::<shared::AudioSourceDesc>(entity).unwrap();
        assert_eq!(placed.clip.as_ref(), Some(&reference));
        assert_eq!(
            world.get::<Transform>(entity).unwrap().translation,
            Vec3::new(1.0, 2.0, 3.0)
        );
        assert!(world.resource::<crate::history::EditorHistory>().can_undo());
    }

    #[test]
    fn skinned_model_placement_replaces_static_and_generic_model_renderers() {
        let asset = EditorAsset {
            label: "Hero".into(),
            path: Some("assets/models/hero.glb".into()),
            folder_path: "assets/models".into(),
            kind: EditorAssetKind::Model,
        };
        let mut snapshot = snapshot_for_asset(&asset, Vec3::ZERO).unwrap();
        snapshot.model.as_mut().unwrap().asset_id = "hero-id".into();
        snapshot.static_mesh_renderer = Some(shared::StaticMeshRenderer::default());
        snapshot.rigid_body = Some(RigidBodyDesc::default());
        snapshot.collider = Some(ColliderDesc::static_cuboid(Vec3::ONE));

        apply_skinned_mesh_placement_mode(&mut snapshot);

        assert_eq!(snapshot.actor_kind, ActorKind::SkinnedMesh);
        assert!(snapshot.model.is_none());
        assert!(snapshot.static_mesh_renderer.is_none());
        assert!(snapshot.rigid_body.is_none());
        assert!(snapshot.collider.is_none());
        assert_eq!(
            snapshot.skinned_mesh_renderer,
            Some(
                shared::SkinnedMeshRenderer::new("assets/models/hero.glb").with_asset_id("hero-id")
            )
        );
    }

    #[test]
    fn model_default_clip_builds_one_explicit_controller_state() {
        let key = Uuid::new_v4();
        let manifest_path =
            std::env::temp_dir().join(format!("blacksite-default-animation-{key}.animation.ron"));
        let asset_id = key.to_string();
        let signature = shared::AnimationSkeletonSignature::new("rig-signature");
        let clip_id = shared::animation_clip_sub_asset_id(8, "Standing");
        let manifest = shared::AnimationManifest {
            schema_version: shared::ANIMATION_MANIFEST_SCHEMA_VERSION,
            asset_id: asset_id.clone(),
            label: "Robot".into(),
            default_animation_clip_id: Some(clip_id.clone()),
            source: shared::AnimationManifestSource {
                path: "assets/models/robot.glb".into(),
                format: "glb".into(),
                fingerprint: shared::AnimationSourceFingerprint {
                    byte_len: 1,
                    modified_unix_secs: 0,
                    content_hash: "fixture".into(),
                },
                dependencies: Vec::new(),
            },
            runtime_supported: true,
            skeletons: vec![shared::AnimationSkeletonRecord {
                id: shared::animation_skeleton_sub_asset_id(0, "Rig"),
                label: "Rig".into(),
                source_index: 0,
                signature: signature.clone(),
                joint_paths: vec!["Root".into()],
            }],
            clips: vec![shared::AnimationClipRecord {
                id: clip_id.clone(),
                label: "Standing".into(),
                source_index: 8,
                duration_seconds: 0.4,
                target_skeleton_signature: Some(signature),
                events: Vec::new(),
            }],
            diagnostics: Vec::new(),
        };
        std::fs::write(
            &manifest_path,
            ron::ser::to_string_pretty(&manifest, ron::ser::PrettyConfig::default()).unwrap(),
        )
        .unwrap();
        let record = AssetRecord {
            id: AssetId(key),
            path: "assets/models/robot.glb".into(),
            label: "Robot".into(),
            kind_tag: "Model".into(),
            import_settings: crate::asset_db::ImportSettings {
                animation_manifest_path: Some(manifest_path.to_string_lossy().into_owned()),
                default_animation_clip_id: Some(clip_id.clone()),
                ..Default::default()
            },
            dependencies: Vec::new(),
        };

        let controller = default_animation_controller_for_record(&record)
            .unwrap()
            .expect("explicit default should produce a controller");
        assert_eq!(controller.default_state, "standing");
        assert_eq!(controller.states.len(), 1);
        assert_eq!(controller.states[0].clip.sub_asset_id, clip_id);
        assert_eq!(
            controller.skeleton.unwrap().sub_asset_id,
            shared::animation_skeleton_sub_asset_id(0, "Rig")
        );

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