//! Normalized static mesh artifacts generated from model source files.

use std::fs;
use std::path::Path;
use std::time::UNIX_EPOCH;

use bevy::gltf::GltfAssetLabel;
use bevy::prelude::*;
use bevy_ufbx::label::FbxAssetLabel;
use bevy_ufbx::mesh::group_faces_by_material;
use bevy_ufbx::utils::convert_matrix;
use serde::{Deserialize, Serialize};

use crate::asset_db::{
    AssetRecord, ImportSettings, MaterialImportPolicy, ModelHierarchyMode, ModelPlacementMode,
};
use shared::{
    ComponentInstanceId, EditorAssetRef, MaterialRef, RendererMaterialSet, RendererMaterialSlot,
    StaticMeshRenderer, StaticMeshRendererEntry,
};

pub const STATIC_MESH_MANIFEST_SCHEMA: u32 = 3;
pub const STATIC_MESH_ARTIFACT_DIR: &str = "assets/meshes/generated";

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct StaticMeshManifest {
    pub schema_version: u32,
    pub asset_id: String,
    pub label: String,
    pub source: StaticMeshSource,
    pub import: StaticMeshImportSnapshot,
    pub metadata: StaticMeshMetadata,
    pub parts: Vec<StaticMeshPart>,
    pub warnings: Vec<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct StaticMeshSource {
    pub path: String,
    pub format: String,
    pub fingerprint: StaticMeshSourceFingerprint,
    pub dependencies: Vec<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct StaticMeshSourceFingerprint {
    pub byte_len: u64,
    pub modified_unix_secs: u64,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct StaticMeshImportSnapshot {
    pub scale: f32,
    pub generate_collider: bool,
    pub lod0_only: bool,
    pub placement_mode: ModelPlacementMode,
    pub hierarchy_mode: ModelHierarchyMode,
    pub material_policy: MaterialImportPolicy,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct StaticMeshMetadata {
    pub mesh_count: usize,
    pub material_count: usize,
    pub node_count: usize,
    pub animation_count: usize,
    pub skin_count: usize,
    pub light_count: usize,
    pub camera_count: usize,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct StaticMeshPart {
    #[serde(default)]
    pub id: String,
    pub name: String,
    pub mesh_label: String,
    #[serde(default)]
    pub material_id: Option<String>,
    pub material_slot_name: String,
    pub material_label: Option<String>,
    pub local_transform: Transform,
    pub source_node: Option<String>,
    pub source_mesh: Option<String>,
    pub source_material: Option<String>,
    /// Whether the source primitive is bound to a skin and therefore must never become a static
    /// renderer slot.
    #[serde(default)]
    pub skinned: bool,
}

pub fn static_mesh_manifest_path(asset_id: &str) -> String {
    format!("{STATIC_MESH_ARTIFACT_DIR}/{asset_id}.static_mesh.ron")
}

pub fn part_id_from_label(label: &str) -> String {
    format!("mesh:{}", stable_sub_asset_slug(label))
}

pub fn material_id_from_label(label: &str) -> String {
    format!("material:{}", stable_sub_asset_slug(label))
}

fn gltf_draw_id(node_index: Option<usize>, mesh_index: usize, primitive_index: usize) -> String {
    let node = node_index
        .map(|index| index.to_string())
        .unwrap_or_else(|| "unbound".into());
    format!("draw:scene0:node{node}:mesh{mesh_index}:primitive{primitive_index}")
}

fn fbx_draw_id(node_index: usize, material_index: usize) -> String {
    format!("draw:scene0:node{node_index}:material{material_index}")
}

fn stable_sub_asset_slug(label: &str) -> String {
    let mut slug = String::new();
    for ch in label.chars() {
        if ch.is_ascii_alphanumeric() {
            slug.push(ch.to_ascii_lowercase());
        } else if !slug.ends_with('_') {
            slug.push('_');
        }
    }
    slug.trim_matches('_').to_string()
}

pub fn refresh_static_mesh_artifact(
    record: &mut AssetRecord,
) -> Result<StaticMeshManifest, String> {
    let mut manifest = build_static_mesh_manifest(record)?;
    let path = static_mesh_manifest_path(&record.id.as_string());
    record.import_settings.static_mesh_manifest_path = Some(path.clone());
    manifest.source.dependencies.sort();
    manifest.source.dependencies.dedup();
    record.dependencies = manifest.source.dependencies.clone();

    let text = ron::ser::to_string_pretty(&manifest, ron::ser::PrettyConfig::default())
        .map_err(|err| format!("could not serialize static mesh manifest: {err}"))?;
    if let Some(parent) = Path::new(&path).parent() {
        fs::create_dir_all(parent)
            .map_err(|err| format!("could not create {}: {err}", parent.display()))?;
    }
    let should_write = fs::read_to_string(&path).ok().as_deref() != Some(text.as_str());
    if should_write {
        fs::write(&path, text)
            .map_err(|err| format!("could not write static mesh manifest {path}: {err}"))?;
        info!(
            "Static mesh manifest refreshed: source={} artifact={} parts={}",
            record.path,
            path,
            manifest.parts.len()
        );
    }

    Ok(manifest)
}

pub fn load_static_mesh_manifest(path: &str) -> Result<StaticMeshManifest, String> {
    let text = fs::read_to_string(path).map_err(|err| format!("could not read {path}: {err}"))?;
    ron::from_str(&text).map_err(|err| format!("could not parse {path}: {err}"))
}

pub fn renderer_from_manifest(
    manifest: &StaticMeshManifest,
    settings: &ImportSettings,
) -> StaticMeshRenderer {
    let use_source_materials = matches!(
        settings.material_policy,
        MaterialImportPolicy::SourceMaterials
    );

    let parts: Vec<_> = manifest
        .parts
        .iter()
        .filter(|part| !part.skinned && manifest.metadata.animation_count == 0)
        .map(|part| StaticMeshRendererEntry {
            id: ComponentInstanceId::new(part_effective_id(part)),
            name: part.name.clone(),
            mesh: EditorAssetRef::new(
                manifest.asset_id.clone(),
                part_effective_id(part),
                part.name.clone(),
            ),
            material_slot_id: ComponentInstanceId::new(material_slot_id(part)),
            material: use_source_materials
                .then(|| {
                    part_effective_material_id(part).map(|id| {
                        EditorAssetRef::new(
                            manifest.asset_id.clone(),
                            id,
                            part.material_slot_name.clone(),
                        )
                    })
                })
                .flatten(),
            local_transform: part.local_transform,
            visible: true,
            cast_shadows: true,
            receive_shadows: true,
        })
        .collect();
    let materials = RendererMaterialSet {
        slots: manifest
            .parts
            .iter()
            .filter(|part| !part.skinned && manifest.metadata.animation_count == 0)
            .map(|part| RendererMaterialSlot {
                id: ComponentInstanceId::new(material_slot_id(part)),
                name: part.material_slot_name.clone(),
                source_material: use_source_materials
                    .then(|| {
                        part_effective_material_id(part).map(|id| {
                            MaterialRef::new(EditorAssetRef::new(
                                manifest.asset_id.clone(),
                                id,
                                part.material_slot_name.clone(),
                            ))
                        })
                    })
                    .flatten(),
                material: None,
            })
            .collect(),
        orphaned_assignments: Vec::new(),
    };

    StaticMeshRenderer {
        slots: parts,
        materials,
    }
}

/// Builds the shared material slots for a full imported hierarchy. Unlike static renderer
/// construction this intentionally includes skin-bound and rigid animated draw bindings.
pub fn renderer_materials_from_manifest(
    manifest: &StaticMeshManifest,
    settings: &ImportSettings,
) -> RendererMaterialSet {
    let use_source_materials = matches!(
        settings.material_policy,
        MaterialImportPolicy::SourceMaterials
    );
    RendererMaterialSet {
        slots: manifest
            .parts
            .iter()
            .map(|part| RendererMaterialSlot {
                id: ComponentInstanceId::new(material_slot_id(part)),
                name: part.material_slot_name.clone(),
                source_material: use_source_materials
                    .then(|| {
                        part_effective_material_id(part).map(|id| {
                            MaterialRef::new(EditorAssetRef::new(
                                manifest.asset_id.clone(),
                                id,
                                part.material_slot_name.clone(),
                            ))
                        })
                    })
                    .flatten(),
                material: None,
            })
            .collect(),
        orphaned_assignments: Vec::new(),
    }
}

fn material_slot_id(part: &StaticMeshPart) -> String {
    format!("slot:{}", part_effective_id(part))
}

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

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

fn build_static_mesh_manifest(record: &AssetRecord) -> Result<StaticMeshManifest, String> {
    let format = source_format(&record.path)?;
    let fingerprint = source_fingerprint(&record.path)?;
    let import = StaticMeshImportSnapshot {
        scale: record.import_settings.scale,
        generate_collider: record.import_settings.generate_collider,
        lod0_only: record.import_settings.lod0_only,
        placement_mode: record.import_settings.placement_mode,
        hierarchy_mode: record.import_settings.hierarchy_mode,
        material_policy: record.import_settings.material_policy,
    };

    let mut manifest = match format.as_str() {
        "gltf" | "glb" => build_gltf_manifest(record, format, fingerprint, import)?,
        "fbx" => build_fbx_manifest(record, format, fingerprint, import)?,
        _ => return Err(format!("unsupported static mesh source format `{format}`")),
    };

    if manifest.parts.is_empty() {
        manifest
            .warnings
            .push("No renderable static mesh parts were found.".into());
    }

    Ok(manifest)
}

fn build_gltf_manifest(
    record: &AssetRecord,
    format: String,
    fingerprint: StaticMeshSourceFingerprint,
    import: StaticMeshImportSnapshot,
) -> Result<StaticMeshManifest, String> {
    let gltf = gltf::Gltf::open(&record.path)
        .map_err(|err| format!("could not parse glTF {}: {err}", record.path))?;
    let mut parts = Vec::new();
    let mut dependencies = Vec::new();
    let mut warnings = Vec::new();

    for buffer in gltf.document.buffers() {
        if let gltf::buffer::Source::Uri(uri) = buffer.source() {
            dependencies.push(resolve_dependency(&record.path, uri));
        }
    }
    for image in gltf.document.images() {
        if let gltf::image::Source::Uri { uri, .. } = image.source() {
            dependencies.push(resolve_dependency(&record.path, uri));
        }
    }

    if let Some(scene) = gltf
        .document
        .default_scene()
        .or_else(|| gltf.document.scenes().next())
    {
        for node in scene.nodes() {
            collect_gltf_node_parts(node, Mat4::IDENTITY, "", &mut parts);
        }
    } else {
        for mesh in gltf.document.meshes() {
            collect_gltf_mesh_parts(None, None, None, mesh, Mat4::IDENTITY, false, &mut parts);
        }
    }

    if gltf.document.animations().count() > 0 {
        warnings.push(
            "Animations are recorded as metadata; animated placement uses SkinnedMeshRenderer."
                .into(),
        );
    }
    if gltf.document.skins().count() > 0 {
        warnings.push(
            "Skinned primitives are excluded from StaticMeshRenderer and use SkinnedMeshRenderer."
                .into(),
        );
    }

    parts.sort_by(|a, b| a.mesh_label.cmp(&b.mesh_label).then(a.name.cmp(&b.name)));

    Ok(StaticMeshManifest {
        schema_version: STATIC_MESH_MANIFEST_SCHEMA,
        asset_id: record.id.as_string(),
        label: record.label.clone(),
        source: StaticMeshSource {
            path: record.path.clone(),
            format,
            fingerprint,
            dependencies,
        },
        import,
        metadata: StaticMeshMetadata {
            mesh_count: gltf.document.meshes().count(),
            material_count: gltf.document.materials().count(),
            node_count: gltf.document.nodes().count(),
            animation_count: gltf.document.animations().count(),
            skin_count: gltf.document.skins().count(),
            light_count: 0,
            camera_count: gltf.document.cameras().count(),
        },
        parts,
        warnings,
    })
}

fn collect_gltf_node_parts(
    node: gltf::Node<'_>,
    parent_transform: Mat4,
    parent_path: &str,
    parts: &mut Vec<StaticMeshPart>,
) {
    let local = Mat4::from_cols_array_2d(&node.transform().matrix());
    let world_transform = parent_transform * local;
    let segment = node
        .name()
        .map(str::to_string)
        .unwrap_or_else(|| format!("Node{}", node.index()));
    let node_path = if parent_path.is_empty() {
        segment
    } else {
        format!("{parent_path}/{segment}")
    };
    let skinned = node.skin().is_some();
    if let Some(mesh) = node.mesh() {
        collect_gltf_mesh_parts(
            node.name().map(str::to_string),
            Some(node.index()),
            Some(node_path.clone()),
            mesh,
            world_transform,
            skinned,
            parts,
        );
    }
    for child in node.children() {
        collect_gltf_node_parts(child, world_transform, &node_path, parts);
    }
}

fn collect_gltf_mesh_parts(
    node_name: Option<String>,
    node_index: Option<usize>,
    node_path: Option<String>,
    mesh: gltf::Mesh<'_>,
    transform: Mat4,
    skinned: bool,
    parts: &mut Vec<StaticMeshPart>,
) {
    let mesh_index = mesh.index();
    let mesh_name = mesh.name().map(str::to_string);
    for primitive in mesh.primitives() {
        let primitive_index = primitive.index();
        let mesh_label = GltfAssetLabel::Primitive {
            mesh: mesh_index,
            primitive: primitive_index,
        }
        .to_string();
        let material = primitive.material();
        let material_label = material
            .index()
            .map(|index| {
                GltfAssetLabel::Material {
                    index,
                    is_scale_inverted: false,
                }
                .to_string()
            })
            .or_else(|| Some(GltfAssetLabel::DefaultMaterial.to_string()));
        let material_name = material
            .name()
            .map(str::to_string)
            .unwrap_or_else(|| "Default Material".into());
        let name = node_name
            .clone()
            .or_else(|| mesh_name.clone())
            .unwrap_or_else(|| format!("Mesh {mesh_index}"));
        let source_node = node_path
            .clone()
            .or_else(|| node_index.map(|index| format!("Node{index}")));
        parts.push(StaticMeshPart {
            id: gltf_draw_id(node_index, mesh_index, primitive_index),
            name: format!("{name} / Primitive {primitive_index}"),
            material_id: material_label
                .as_ref()
                .map(|label| material_id_from_label(label)),
            mesh_label,
            material_slot_name: material_name.clone(),
            material_label,
            local_transform: Transform::from_matrix(transform),
            source_node,
            source_mesh: Some(format!("Mesh{mesh_index}")),
            source_material: Some(material_name),
            skinned,
        });
    }
}

fn build_fbx_manifest(
    record: &AssetRecord,
    format: String,
    fingerprint: StaticMeshSourceFingerprint,
    import: StaticMeshImportSnapshot,
) -> Result<StaticMeshManifest, String> {
    let bytes =
        fs::read(&record.path).map_err(|err| format!("could not read {}: {err}", record.path))?;
    let scene = ufbx::load_memory(
        &bytes,
        ufbx::LoadOpts {
            target_unit_meters: 1.0,
            target_axes: ufbx::CoordinateAxes::right_handed_y_up(),
            ..Default::default()
        },
    )
    .map_err(|err| format!("could not parse FBX {}: {err:?}", record.path))?;

    let mut parts = Vec::new();
    let mut warnings = Vec::new();
    for (node_index, node) in scene.nodes.as_ref().iter().enumerate() {
        let Some(mesh_ref) = node.mesh.as_ref() else {
            continue;
        };
        let mesh = mesh_ref.as_ref();
        if mesh.num_vertices == 0 || mesh.faces.as_ref().is_empty() {
            continue;
        }
        let skinned = !mesh.skin_deformers.as_ref().is_empty();

        let mut groups: Vec<(usize, Vec<u32>)> =
            group_faces_by_material(mesh).into_iter().collect();
        groups.sort_by_key(|(material_index, _)| *material_index);
        for (material_index, indices) in groups {
            if indices.is_empty() {
                continue;
            }
            let material = mesh.materials.get(material_index).map(|mat| mat.as_ref());
            let material_label = material
                .and_then(|mat| fbx_material_label(&scene, mat.element.element_id))
                .or_else(|| Some(FbxAssetLabel::DefaultMaterial.to_string()));
            let material_name = material
                .map(|mat| mat.element.name.to_string())
                .filter(|name| !name.is_empty())
                .unwrap_or_else(|| "Default Material".into());
            let node_name = if node.element.name.is_empty() {
                format!("Node {node_index}")
            } else {
                node.element.name.to_string()
            };
            let mesh_label = FbxAssetLabel::Mesh(node_index * 1000 + material_index).to_string();
            parts.push(StaticMeshPart {
                id: fbx_draw_id(node_index, material_index),
                name: format!("{node_name} / Material {material_index}"),
                material_id: material_label
                    .as_ref()
                    .map(|label| material_id_from_label(label)),
                mesh_label,
                material_slot_name: material_name.clone(),
                material_label,
                local_transform: Transform::from_matrix(convert_matrix(&node.geometry_to_world)),
                source_node: Some(format!("Node{node_index}")),
                source_mesh: Some(format!("Mesh{node_index}")),
                source_material: Some(material_name),
                skinned,
            });
        }
    }

    if !scene.anim_stacks.as_ref().is_empty() {
        warnings.push(
            "Animations are recorded as metadata; animated placement uses SkinnedMeshRenderer."
                .into(),
        );
    }
    if !scene.skin_deformers.as_ref().is_empty() {
        warnings.push(
            "Skinned primitives are excluded from StaticMeshRenderer and use SkinnedMeshRenderer."
                .into(),
        );
    }

    parts.sort_by(|a, b| a.mesh_label.cmp(&b.mesh_label).then(a.name.cmp(&b.name)));

    Ok(StaticMeshManifest {
        schema_version: STATIC_MESH_MANIFEST_SCHEMA,
        asset_id: record.id.as_string(),
        label: record.label.clone(),
        source: StaticMeshSource {
            path: record.path.clone(),
            format,
            fingerprint,
            dependencies: fbx_dependencies(&record.path),
        },
        import,
        metadata: StaticMeshMetadata {
            mesh_count: scene.meshes.as_ref().len(),
            material_count: scene.materials.as_ref().len(),
            node_count: scene.nodes.as_ref().len(),
            animation_count: scene.anim_stacks.as_ref().len(),
            skin_count: scene.skin_deformers.as_ref().len(),
            light_count: scene.lights.as_ref().len(),
            camera_count: scene.cameras.as_ref().len(),
        },
        parts,
        warnings,
    })
}

fn fbx_material_label(scene: &ufbx::Scene, element_id: u32) -> Option<String> {
    if element_id == 0 {
        return None;
    }
    scene
        .materials
        .as_ref()
        .iter()
        .position(|material| material.element.element_id == element_id)
        .map(|index| FbxAssetLabel::Material(index).to_string())
}

fn fbx_dependencies(source_path: &str) -> Vec<String> {
    let path = Path::new(source_path);
    let Some(stem) = path.file_stem().and_then(|stem| stem.to_str()) else {
        return Vec::new();
    };
    let Some(parent) = path.parent() else {
        return Vec::new();
    };
    let sidecar = parent.join(format!("{stem}.fbm"));
    if !sidecar.is_dir() {
        return Vec::new();
    }
    walkdir::WalkDir::new(sidecar)
        .follow_links(false)
        .into_iter()
        .filter_map(Result::ok)
        .filter(|entry| entry.file_type().is_file())
        .map(|entry| entry.path().to_string_lossy().replace('\\', "/"))
        .collect()
}

fn source_format(path: &str) -> Result<String, String> {
    Path::new(path)
        .extension()
        .and_then(|ext| ext.to_str())
        .map(|ext| ext.to_ascii_lowercase())
        .ok_or_else(|| format!("asset path `{path}` has no extension"))
}

fn source_fingerprint(path: &str) -> Result<StaticMeshSourceFingerprint, String> {
    let metadata = fs::metadata(path).map_err(|err| format!("could not stat {path}: {err}"))?;
    let modified_unix_secs = metadata
        .modified()
        .ok()
        .and_then(|time| time.duration_since(UNIX_EPOCH).ok())
        .map(|duration| duration.as_secs())
        .unwrap_or_default();
    Ok(StaticMeshSourceFingerprint {
        byte_len: metadata.len(),
        modified_unix_secs,
    })
}

fn resolve_dependency(source_path: &str, uri: &str) -> String {
    if uri.starts_with("data:") || uri.contains("://") {
        return uri.to_string();
    }
    Path::new(source_path)
        .parent()
        .unwrap_or_else(|| Path::new(""))
        .join(uri)
        .to_string_lossy()
        .replace('\\', "/")
}

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

    fn test_manifest() -> StaticMeshManifest {
        StaticMeshManifest {
            schema_version: STATIC_MESH_MANIFEST_SCHEMA,
            asset_id: "asset-1".into(),
            label: "Crate".into(),
            source: StaticMeshSource {
                path: "assets/models/crate.glb".into(),
                format: "glb".into(),
                fingerprint: StaticMeshSourceFingerprint {
                    byte_len: 42,
                    modified_unix_secs: 10,
                },
                dependencies: Vec::new(),
            },
            import: StaticMeshImportSnapshot {
                scale: 1.0,
                generate_collider: true,
                lod0_only: true,
                placement_mode: ModelPlacementMode::StaticAsset,
                hierarchy_mode: ModelHierarchyMode::SingleActor,
                material_policy: MaterialImportPolicy::SourceMaterials,
            },
            metadata: StaticMeshMetadata {
                mesh_count: 1,
                material_count: 1,
                node_count: 1,
                animation_count: 0,
                skin_count: 0,
                light_count: 0,
                camera_count: 0,
            },
            parts: vec![StaticMeshPart {
                id: "mesh:mesh0_primitive0".into(),
                name: "Crate / Primitive 0".into(),
                mesh_label: "Mesh0/Primitive0".into(),
                material_id: Some("material:material0".into()),
                material_slot_name: "Wood".into(),
                material_label: Some("Material0".into()),
                local_transform: Transform::from_xyz(1.0, 2.0, 3.0),
                source_node: Some("Node0".into()),
                source_mesh: Some("Mesh0".into()),
                source_material: Some("Wood".into()),
                skinned: false,
            }],
            warnings: Vec::new(),
        }
    }

    #[test]
    fn static_mesh_manifest_path_uses_asset_id() {
        assert_eq!(
            static_mesh_manifest_path("abc"),
            "assets/meshes/generated/abc.static_mesh.ron"
        );
    }

    #[test]
    fn renderer_from_manifest_preserves_labels_and_collider_policy() {
        let manifest = test_manifest();
        let settings = ImportSettings {
            generate_collider: true,
            ..Default::default()
        };

        let renderer = renderer_from_manifest(&manifest, &settings);

        assert_eq!(renderer.slots.len(), 1);
        let entry = &renderer.slots[0];
        assert_eq!(entry.mesh.asset_id, manifest.asset_id);
        assert_eq!(entry.mesh.sub_asset_id, "mesh:mesh0_primitive0");
        assert_eq!(
            entry
                .material
                .as_ref()
                .map(|material| material.sub_asset_id.as_str()),
            Some("material:material0")
        );
    }

    #[test]
    fn renderer_from_manifest_can_ignore_source_materials() {
        let manifest = test_manifest();
        let settings = ImportSettings {
            material_policy: MaterialImportPolicy::AuthoringOverride,
            ..Default::default()
        };

        let renderer = renderer_from_manifest(&manifest, &settings);

        assert!(renderer.slots[0].material.is_none());
    }

    #[test]
    fn renderer_from_manifest_excludes_skinned_primitives() {
        let mut manifest = test_manifest();
        manifest.parts[0].skinned = true;

        let renderer = renderer_from_manifest(&manifest, &ImportSettings::default());

        assert!(renderer.slots.is_empty());
    }

    #[test]
    fn renderer_from_manifest_excludes_node_animated_geometry() {
        let mut manifest = test_manifest();
        manifest.metadata.animation_count = 1;

        let renderer = renderer_from_manifest(&manifest, &ImportSettings::default());

        assert!(renderer.slots.is_empty());
    }

    #[test]
    fn committed_rigged_fixture_never_builds_static_renderer_slots() {
        let path = Path::new(env!("CARGO_MANIFEST_DIR"))
            .join("../../assets/models/robot_expressive.glb")
            .to_string_lossy()
            .into_owned();
        let record = AssetRecord {
            id: AssetId::new(),
            path,
            label: "Robot Expressive".into(),
            kind_tag: "Model".into(),
            import_settings: ImportSettings::default(),
            dependencies: Vec::new(),
        };

        let manifest = build_static_mesh_manifest(&record).unwrap();
        let renderer = renderer_from_manifest(&manifest, &record.import_settings);

        assert!(manifest.metadata.skin_count > 0);
        assert!(manifest.metadata.animation_count > 0);
        assert!(manifest.parts.iter().any(|part| part.skinned));
        assert!(renderer.slots.is_empty());
    }
}
